diff --git a/.gitignore b/.gitignore index d08d18f47c6..957ed059b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.DS_Store +*.swp + # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a @@ -22,10 +25,35 @@ _testmain.go *.exe out/ -release/ +release/* +!release/index.html *.iml *.zpi *.zwi *.go-e + +*.log + +.idea/ + +tmp/ + +.hg/ + +*.test +tags + +*.coverprofile + +#Compiled Plugins +fixtures/plugins/test_1 +fixtures/plugins/test_2 +fixtures/plugins/empty_plugin +fixtures/config/plugin-config/.cf/plugins/test_1 +fixtures/config/plugin-config/.cf/plugins/test_2 +fixtures/config/plugin-config/.cf/plugins/empty_plugin + +# NEVER commit the resources files! +cf/resources/i18n_resources.go diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 26de55cdf77..00000000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "src/github.com/stretchr/testify"] - path = src/github.com/stretchr/testify - url = https://github.com/stretchr/testify.git -[submodule "src/github.com/cloudfoundry/loggregatorlib"] - path = src/github.com/cloudfoundry/loggregatorlib - url = https://github.com/cloudfoundry/loggregatorlib.git -[submodule "src/code.google.com/p/gogoprotobuf"] - path = src/code.google.com/p/gogoprotobuf - url = https://code.google.com/p/gogoprotobuf/ -[submodule "src/github.com/codegangsta/cli"] - path = src/github.com/codegangsta/cli - url = https://github.com/codegangsta/cli.git diff --git a/.travis.yml b/.travis.yml index bf8445e2b79..438376f8c09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ language: go go: -- 1.1.2 -before_install: -- git submodule update --init --recursive -install: ./bin/build -script: ./bin/test + - 1.2.2 +install: + - go get -v code.google.com/p/go.tools/cmd/vet + - go get -v github.com/tools/godep + - go get -v github.com/onsi/ginkgo/ginkgo +script: bin/test --compilers=2 branches: only: - - master \ No newline at end of file + - master diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 00000000000..a006e8bb006 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,98 @@ +Building Cloud Foundry CLI +========================== + +For developing on unix systems: + +1. Run `./bin/build` +1. The binary will be built into the `./out` directory. + +Optionally, you can use `bin/run` to compile and run the executable in one step. + +For developing on windows with powershell.exe: +1. $Env:GODEP_PATH=C:\path\to\go-path\src\github.com\cloudfoundry\cli\Godeps\_workspace; +1. $Env:GOPATH = $Env:GODEP_PATH + ";" + "C:\path\to\go-path\" + +Building Installers and Cross Compiling On Unix Systems +======================================================= +1. [Configure your go installation for cross compilation](https://stackoverflow.com/questions/12168873/cross-compile-go-on-osx) +1. Run `bin/build-all.sh` +1. Run `ci/scripts/build-installers` +1. Installers will all be in the `release` dir + +How We Test, Build, and Release The CLI +======================================= + +High Level Overview +------------------- +Every push to the master branch goes through a CI pipeline that consists of + +* unit tests +* integration tests + +We run all of our tests on multiple platforms (e.g.: Linux, OS X, Windows) and +on multiple architectures (eg: 32bit, 64bit). Edge builds and tagged releases +are only released when all tests pass. + +Unit Tests +---------- +The first stage of every build is to run `bin/test` on all unix platforms (e.g.: 64 and 32bit Linux and OS X) and to +run an equivalent `go test` command on Windows. The executables produced by `go build` from this stage are uploaded +so that they can be run through integration tests and ultimately packaged into installers. This ensures that the +final products are fully tested and known to have passed our entire CI process. + +The `ci/scripts` directory contains scripts that run tests and save the executable for each platform-architecture combination. + +CATS +---- +The [cf-acceptance-tests](https://github.com/cloudfoundry/cf-acceptance-tests) (eg: C.A.T.S.) are a suite of integration tests that +drive the `cf` cli along with a real CF deployment to verify the entire system works. We have some moderate tooling +to run these on different platforms, refer to the `herd-cats-$PLATFORM-$ARCH` scripts in `ci/scripts` for more +information. + + +GATS +---- +The CLI team identified a need for integration tests *similar* to the CATS that we maintain; we call these tests +[GATS](https://github.com/tjarratt/GATS) (e.g.: GCF Acceptance Test Suite). These are run after the CATS tests, +and are fairly simple to run: + +``` +cd path/to/GATS + +export API=http://api.some.ip.v4.address.xip.io +export ADMIN_USER=admin-user +export ADMIN_PASSWORD=admin-password +export CF_USER=user-name +export CF_USER_PASSWORD=user-password +export ORG=org-name +export SPACE=space-name +export APP_HOST=persistent-app-host + +bin/configure +bin/test +``` + + +Build and Release to S3 +----------------------- +At the very end of our pipeline, assuming all tests have passed, we run a fairly simple script that uploads our +binaries and installers to the appropriate bucket on S3. + +``` +export AWS_SECRET_ACCESS_KEY=SECRET_KEY_IS_SECRET +export AWS_ACCESS_KEY_ID=WINK + +ci/scripts/build-and-release +``` + +This script fetches the binaries that were produced earlier, generates installers for our supported platforms +and then uploads the final artifacts to S3. + +Tagged Releases On Github +------------------------- +Every time we push to the master branch, a release is created in a directory in the go-cli bucket on our S3 account. +We make these URLs public so that people can try the edge builds. Refer to our README for the URLs for some of these artifacts. + +Commits that have a release tag on them (e.g.: v6.1.0) go into special directories that have the release name in them. + +e.g.: http://go-cli.s3-website-us-east-1.amazonaws.com/releases/v6.1.0/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..6debfeb42b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,398 @@ +##v6.7.0 +* Display correct information about app in copy-source -Restart app.Start/Stop/Restart/WatchStaging by passing org and +space name instead of assuming config contained correct information [finishes #81219748] + +* Change initial output for copy-source [finishes #82171880] + +* Add crypto/sha512 to import to solve unkown authority bug [Fixes #82254112] + +* Fixes bug where null json value caused panic [Fixes #82292538] + +* Merge pull request #290 from haydonryan/master Correcting status message + +* Correcting status message previously space was set to org and vice versa, correcting. + +* Fix french wording https://github.com/cloudfoundry/cli/pull/279 [finishes #81865644] + +* Update application.PackageUpdatedAt to marshal json as time.Time [#82138922] + +* Decolorize output for plugin to parse. [Finishes #82051672] + +* Fix issue when making requests without a body [#79025838] + +* move plugin cli invocations to a struct, which is passed into Run(...) + +* Testing interval output printing - add PrintCapturingNoOutput to ui object to avoid using stdout in net +package tests +- make sure we rewrite entire string during interval output printing by +printing a long line of empty spaces [finish #79025838] + +* Progress inidicated during uploads (push and create/update buildpack) [Finishes #79025838] + +* Correcting status message previously space was set to org and vice versa, correcting. + +* Terminal output can be silenced when invoke cli command from a plugin [#81867022] + +* Add plugin_examples and README [finishes #78236438] + +* Remove errant text from copy-source help output [Finishes #81813144] + +* Exit 1 when a plugin exits nonzero or panics [#81633932] + +* plugins have names defined by method + +* `cf org` now displays space quotas. [Finishes #77390184] + +* Merge pull request #280 from cloudfoundry/missing-service-instance-error-message update-service shows an error if the instance is missing and no plan is ... + +* update-service shows an error if the instance is missing and no plan is provided + +* Add `cf check-route` command [finishes #78473792] + +* Plugins now have access to stdin (can be interactive) [finishes #81210182] + +* Cli checks command shortname during plugin install - Cli also checks short names for commands when determining execution. + Useful to prevent people from mucking with plugin configs by hand. [Finishes #80842550] + +* Merge branch 'thecadams-honor-keepalive' +* Merge branch 'honor-keepalive' of github.com:thecadams/cli + +* Improve error message return when refresh token has expired [finishes #78130846] + +* Disable service access proprly queries for organization. [Finishes #80867298] + +* plugns receive output from core cli commands + +* Display most recent package uploaded time for cf app [finishes #78427862] + +* Add CF_PLUGIN_HOME to help text output [finishes #81147420] + +* Set MinVersion for ssl to TLS1, removing support for SSLV3 [#81218916] + +* Add VCAP_APPLICATION to cf env output [finishes #78533524] + +* Update `cf env` to grab booleans and integers. [Finishes #79059944] + +* Implement update_service command [#76633662] + +* Wait to output OK until app is started in start command + +* Update help text for create-user-provided-service [finishes: #75171038] + +* All arguments/flags are passed to plugin when plugin command invoked [finishes #78234552] + +* Provide error when install_plugin plugin collides with other plugin -Update error message for collision with core cli command [finishes #79400494] + +* Implement command `cf oauth-token` [Finishes #77587324] + +* Use cached plugin config data instead of rpcing the plugin + +* Cf help shows plugin info based on plugin_config [#78234404] + +* update plugin config to store data for each command +* install handles conflicting commands +* validate plugin binary upon install + +* Update `cf env APPNAME` to display running/staging env variables. - Refactor GetEnv api call to use counterfiter fake [Finishes #79059944] + +* cf exit gracefully when i18n.T() is not initialized for configurations [Finishes #80759488] + +##v6.6.2 +* Bump version to 6.6.2 +* Update usage text for install/uninstall-plugin [finishes #80770062][finishes #80701742] +* Move test setup into beforeEach of plan_builder_test +* Fix install_plugin usage text [finshes #80701742] +* security group commands show tip about changes requiring restart [Finishes #75375696] +* Remove unused scripts (moved for gocd) [#78508732] +* update correct fixture path in test code +* update transaltions for uninstall plugin description text +* stop translating commands, add missed translated strings +* Tar exectutables before uploading artifacts from gocd +* Update build-and-release-gocd tooling +* Potential fix for windows gocd timeout. +* Fix for flakey tests in rpc package. +* Use 32 bit binary to get version when building installers +* Revert "Get version from 32bit binary, since the agent is 32bit" This reverts commit 8f7ff830b48f0926215adb60e8512e023e942ba5. +* Implemented plugins advertising their own name. - Name space with plugin name instead of binary name. +- Expose plugins directory as part of plugin configuration object +- Cli and plugins ping each other for availability. If the ping fails, + they will stop the servers after 1 second. [Finishes #79964866] +* Refacto plugin/rpc to setup bidirectional communication [#79964866] +* Refactor install plugin to use counterfeiter fake. [#79964866] +* Plugin pings cf when it is ready to accept commands. - removes sleep from cf. [#79964866] +* refactor ServeCommand calls +* Change fake_word_generator to a counterfeiter fake [#74259334] +* add gi18n-checkup to bin/test [Finishes #80173376] +* Improve spacing for help output in create/update-space-quota [finishes #80052722] +* Add scripts for build-and-release for gocd +* Sync words.go with the word list [#80335818] +* Update error text on invalid json format. [Finishes #77391788] +* Improve help text for create-security-group command [Finishes #77391788] +* help will run as a core command instead of calling plugin commands [Finishes #78234942] +* plugin server runs on randomly chosen port +* consolodate plugin port configuration +* cf help includes plugin commands +* attempt to fix install paths for windows +* fix windows test failures by naming binaries with .exe extension +* close test file before deleting +* Fix error message for login w/ -a when ssl-cert invalid [#69644266] +* Finished refactor of configuration repository. [#78234942] +* Refactor plugin commands into rpc package -Also increase locales_test timeout +-Add empty_plugin executable to gitignore [#78234942] +* Refactoring plugins to include common code for rpc model. - plugins/rpc contains everything main used to contain. +- new interface for listing commands through rpc. +* Implement 'plugins' to list all installed plugin methods and the executable they belong to. [Finishes #78235118] +* go get godep before tests +* Revert "Use filepath instead of path where possible" This reverts commit 49beccf7726887211cfb05a20f6bbc175ec5847e. +- Failed on CI +* Use filepath instead of path where possible -Path does not always work well with windows [#79748230] +* Append .exe to config.json for plugin-config +* Name test binaries w/ .exe so windows WORKS +* Use filepath instead of path in main_suite_test -Add more debugging as well +* Add debugging statements to building plugin in main_suite_test +* Revert "Update GOPATH var in windows bat scripts" This reverts commit d311d8d4e71db7f8aad7d39d2ab0e1e26394aac2. +* Update GOPATH var in windows bat scripts +* Add debugging info to the main test +* Add ginkgo defer to allow us to see error message -This is when the main_suite_test fails before running +the main_test +* Skip checking filemode for instal-plugin on windows +* Retry request on tcp connection error. [Finishes #79151504] +* Added tests for the package main on windows during ci +* Added defaults for create-space-quota's help [Finishes #77394232] +* Improve testing with plugins and fix install-plugin bug -Chmod plugin binary after copying to the CF_HOME directory +-Test that all plugins work when multiple are successfully installed [finishes #78399080] [finishes #79487120] +* Refactor app instances to use a counterfeiter fake +* Fix tests relating to plugins and polution caused by them -Reduce sleep time when waiting for plugin to start +-Have main_test use plugin config the whole time in case of +invalid config in the home directory (the real home dir) [finishes #79305568] +* Wip commit for plugins with multiple commands +* Wip commit for plugins with multiple commands +* Add missing fixtures plugin command file. +* Compile test plugin every run. -This gives us a cross-platform test suite. +-Refactoring stuff out of main will make the test suite faster.. +* Update changelog +* First pass at rpc model - have hardcoded port 20001 +- sleep for 3 seconds waiting for rpc server [Finishes #78397654] + +##v6.6.1 +* Bump version to 6.6.1 +* fix argument in callCoreCommand() +* Fix http_test.go to be OS independent [#79151902] +* Update flag descriptions for enable/disable service access [#79151902] +* show help when `cf` is input [#78233706] +Signed-off-by: Daniel Lavine +* Up tcp timeout to 10 seconds and log errors more effectively -Upping the timeout to deal with possible architecture issues, but +this should not be increased any more than 10 seconds +[#79151504] +* User can specify go program as a plugin in config.json [#78233706] +* Bump Goderps +* Dont pull from a locked SHA +* Lock CATS to a known good SHA (for now) +* Brought app_files repo into alignment with our new patterns. [#74259334] +* Revert "Update herd-cats-linux64 script to dynamically generate config" This reverts commit 7a74e5a3bfbb4e975eee4aedcc5a1471939070fc. +* Update herd-cats-linux64 script to dynamically generate config +* Move integration tests into main_test suite -Go 1.3 changes the way tests are built +* Move app_events repo into its own package. [#74259334] +* Upgrade to Go 1.3.1 - Go 1.3.x no longer orders maps, so we had to compensate in some of our + tests. +- The fake server is a little smarter about "q" params now. +[Finishes #73583562] + +* Bump Godeps for jibber-jabber. - Pull in Windows XP fix. + +[Finishes #78489056] + +* Remove -u option and clean up symlink in the build script. +* Bump Goderps +* Another attempt to fix unit tests on Windows +* Attempt to fix unit tests on Windows +* Change fake and refactor app_bits repo. - App bits repo is much more tightly scoped +- The App Bits repo has a counterfeiter fake, and lives in its own + package +- Some callbacks met their demise +- We now have a push actor +- Former responsibilities of the App Bits repo have been divided between + the App Bits repo, the push command, and the push actor. +- All this should make the future implementation of an "upload bits" + command much easier/possible. +[#74259334] +* Change "-1" to "unlimited" in space-quotas. [#77830744] +* Change '-1' to 'unlimited' in space-quota. [#77830744] +* Display "unlimited" instead of "-1" in quota. [#77830744] +* Display "unlimited" instead of "-1" in quotas. [#77830744] +* Make Windows recognize PATH update and don't append on reinstall. [#78348074] +* Chmod the Inno Setup script. [#78348074] +* Change Windows installer build process to use Inno Setup. [#78348074] + +## v6.6.0 +* Modify set-running-environment-variable-group command usage to show example. [Finishes #77830856] +* Modify set-staging-environment-variable-group usage to show example of JSON. [Finishes #77837402] +* Add -i parameter for create-quota in usage. [Finishes #78111444] +* Can set locale using `cf config --locale LOCALE` - can clear locale providing CLEAR as flag argument. [Finishes #74651616] +* Implement set-running-environment-variable-group command. [Finishes #77830856] +* Implement "set-staging-environment-variable-group" command. [Finishes #77837402] +* Implement staging-environment-variable-group command. [Finishes #77829608] +* Implement running-environment-variable-group command. [Finishes #76840940] +* Make help for start timeouts on push more explicit. [Finishes #75071698] +* Implement disable-feature-flag command. [Finishes #77676754] +* Accept a bare -1 as instance memory on updating quotas. [#77765852] +* Implement enable-feature-flag command. [Finishes #77665980] +* Implement "feature-flag" command. Finishes #77222224] +* Can create organization with specified quota. [Finishes #75915142] +* Implement feature-flags command. [Finishes #77665936] +* Correctly accept a -1 value for creating quotas. [Fixes #77765852] +* Correctly display instance memory limit field for quotas. [Fixes #77765166] + +## v6.5.1 +* Revert changes to update-service-broker. This cause a breaking change by mistake. + +## v6.5.0 +* Implement Space Quota commands (create, update, delete, list, assignment) +* Change cf space command to show information on the quota associated with the space. [#77389658] +* Tweak help text for "push" [#76417956] +* Remove default async timeout. [#76995182] +* Change update-service-broker to take in optional flags. [#63480754] +* Update plan visibility search to take advantage of API queries [#76753494] +* Add instance memory to quota, quotas, and update-quota. [#76292608] + +## v6.4.0 +* Implement service-access command. +* Implement enable-service-access command. +* Implement disable-service-access command. +* Merge pull request #237 from sykesm/hm-unknown-instances Use '?' instead of '-1' when running instances is unknown [#76461268] +* Merge pull request #239 from johannespetzold/loggregator-debug-printer CF_TRACE option for cf logs +* Stop using deprecated endpoints for domains. [#76723550] +* Refresh auth token on all service-access commands. [#76831670] +* Stop CLI from hanging when Loggregator keeps returning errors. [#76545800] +* Merge pull request #234 from fraenkel/cfignoreIgnored Copy cfignore to upload directory to properly ignore files +* Pass in ProxyFromEnvironment function to loggregator_consumer. [#75343416] +* Merge pull request #227 from XenoPhex/master By Grabthar hammer, by the sons of Worvan, you shall be avenged. Also, sorting. +* Add cli version to the "aww shucks" messsage. [#75131050] +* Merge pull request #223 from fraenkel:connectTimeout Use a connect timeout whenever making connections +* Merge pull request #225 from cloudfoundry/flush-log-messages Fix inter-woven output during start +* Merge pull request #222 from fraenkel/closeBody Close the response body +* Merge pull request #221 from jpalermo/master Fix base64 padding + +## v6.3.2 +* Provides "pretty printed" output of config JSON. [#74664516] +* Undo recursive copy of files [#75530934] +* Merge all translations into monolithic files. [#74408246] +* Remove some words from dictionary [#75469600] +* Merge pull request #210 from wdneto/pt_br Initial pt-br translation [#75083626] + +## v6.3.1 +* Remove Korean as a supported language. - goi18n does not currently support it, so it is in the same boat as Russian. +* Forcing default domain to be the first shared domain. Closes #209 [#75067850] +* The ru_RU locale is not supported. The go-i18n tool that we use does not support this locale at the moment and thus we should not be offering translation until such time as that changes. Closes #208 [#75021420] +* Adding in tool to fix json formatting +* Fixes spacing and file permissions for all JSON files. Spacing i/s now a standard 3 spaces. Permissions are now 0644. +* Merges Spanish Translations. Thanks, @bonzofenix! Merge pr/207 [#74857552] +* Merge Chinese Translations from a lot of effort by @wayneeseguin. Thanks also to @tsjsdbd, @isuperbb, @shenyefeng, @hujie5592427, @haojun, @wsxiaozhang and @Kaixiang! Closes #205 [#74772500] +* Travis-CI builds should run i18n tests Also, fail if any of those other commands fail + +## v6.3.0 +* Add commands for managing security groups +* Push no longer uses deprecated endpoint for domains. [#74737286] +* `cf` always returns exit code 1 on error [#74565136] +* Json is interpreted properly for create/update user-provided-service. Fixes issue #193 [#73971288] +* Made '--help' flag match the help text from the 'help' command [Finishes #73655496] + +## v6.2.0 +* Internationalize the CLI [#70551274](https://www.pivotaltracker.com/story/show/70551274), [#71441196](https://www.pivotaltracker.com/story/show/71441196), [#72633034](https://www.pivotaltracker.com/story/show/72633034), [#72633034](https://www.pivotaltracker.com/story/show/72633034), [#72633036](https://www.pivotaltracker.com/story/show/72633036), [#72633038](https://www.pivotaltracker.com/story/show/72633038), [#72633042](https://www.pivotaltracker.com/story/show/72633042), [#72633044](https://www.pivotaltracker.com/story/show/72633044), [#72633056](https://www.pivotaltracker.com/story/show/72633056), [#72633062](https://www.pivotaltracker.com/story/show/72633062), [#72633064](https://www.pivotaltracker.com/story/show/72633064), [#72633066](https://www.pivotaltracker.com/story/show/72633066), [#72633068](https://www.pivotaltracker.com/story/show/72633068), [#72633070](https://www.pivotaltracker.com/story/show/72633070), [#72633074](https://www.pivotaltracker.com/story/show/72633074), [#72633080](https://www.pivotaltracker.com/story/show/72633080), [#72633084](https://www.pivotaltracker.com/story/show/72633084), [#72633086](https://www.pivotaltracker.com/story/show/72633086), [#72633088](https://www.pivotaltracker.com/story/show/72633088), [#72633090](https://www.pivotaltracker.com/story/show/72633090), [#72633090](https://www.pivotaltracker.com/story/show/72633090), [#72633096](https://www.pivotaltracker.com/story/show/72633096), [#72633100](https://www.pivotaltracker.com/story/show/72633100), [#72633102](https://www.pivotaltracker.com/story/show/72633102), [#72633112](https://www.pivotaltracker.com/story/show/72633112), [#72633116](https://www.pivotaltracker.com/story/show/72633116), [#72633118](https://www.pivotaltracker.com/story/show/72633118), [#72633126](https://www.pivotaltracker.com/story/show/72633126), [#72633128](https://www.pivotaltracker.com/story/show/72633128), [#72633130](https://www.pivotaltracker.com/story/show/72633130), [#70551274](https://www.pivotaltracker.com/story/show/70551274), [#71347218](https://www.pivotaltracker.com/story/show/71347218), [#71441196](https://www.pivotaltracker.com/story/show/71441196), [#71594662](https://www.pivotaltracker.com/story/show/71594662), [#71801388](https://www.pivotaltracker.com/story/show/71801388), [#72250906](https://www.pivotaltracker.com/story/show/72250906), [#72543282](https://www.pivotaltracker.com/story/show/72543282), [#72543404](https://www.pivotaltracker.com/story/show/72543404), [#72543994](https://www.pivotaltracker.com/story/show/72543994), [#72548944](https://www.pivotaltracker.com/story/show/72548944), [#72633064](https://www.pivotaltracker.com/story/show/72633064), [#72633108](https://www.pivotaltracker.com/story/show/72633108), [#72663452](https://www.pivotaltracker.com/story/show/72663452), [#73216920](https://www.pivotaltracker.com/story/show/73216920), [#73351056](https://www.pivotaltracker.com/story/show/73351056), [#73351056](https://www.pivotaltracker.com/story/show/73351056)] +* 'purge-service-offering' should fail if the request fails [[#73009140](https://www.pivotaltracker.com/story/show/73009140)] +* Pretty print JSON for `cf curl` [[#71425006](https://www.pivotaltracker.com/story/show/71425006)] +* CURL output can be directed to file via parameter `--output`. [[#72659362](https://www.pivotaltracker.com/story/show/72659362)] +* Fix a source of flakiness in start [[#71778246](https://www.pivotaltracker.com/story/show/71778246)] +* Add build date time to the `--version` message, `cf --version` now reports [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) date [[#71446932](https://www.pivotaltracker.com/story/show/71446932)] +* Show system environment variables with `cf env` [[#71250896](https://www.pivotaltracker.com/story/show/71250896)] +* Fix double confirm prompt bug [[#70960378](https://www.pivotaltracker.com/story/show/70960378)] +* Fix create-buildpack from local directory [[#70766292](https://www.pivotaltracker.com/story/show/70766292)] +* Gateway respects user-defined Async timeout [[#71039042](https://www.pivotaltracker.com/story/show/71039042)] +* Bump async timeout to 10 minutes [[#70242130](https://www.pivotaltracker.com/story/show/70242130)] +* Trace should also respect the user config setting [[#71045364](https://www.pivotaltracker.com/story/show/71045364)] +* Add a 'cf config' command [[#70242276](https://www.pivotaltracker.com/story/show/70242276)] + - Uses --color value to enable/disable/ignore coloring [[#71045474](https://www.pivotaltracker.com/story/show/71045474), [#68903282](https://www.pivotaltracker.com/story/show/68903282)] + - Add config --trace flag [[#68903318](https://www.pivotaltracker.com/story/show/68903318)] + +## v6.1.2 +* Added BUILDING.md document to describe our CI / build process +* Fixed regression where the last few log messages received would never be shown + - affected commands include `cf start`, `cf logs` and `cf push` +* Fixed a bug in `cf push` related to windows and empty directories [#70470232] [#157](https://github.com/cloudfoundry/cli/issues/157) +* Fixed a bug in `cf space-users` and `cf org-users` that would incorrectly show all users +* `cf org $ORG_NAME` now displays the quota assigned to the org +* Fixed a bug where no log messages would be received if your access token had expired [#66242222] + +## v6.1.1 +- New quota CRUD commands for admins +- Only ignore `manifest.yml` at the app root directory [#70044992] +- Updating loggregator library experimental support for proxies [#70022322] +- Provide a `--sso` flag to `cf login` for SAML [#69963402, #69963432] +- Do not use deprecated domain endpoints in `cf push` [#69827262] +- Display `X-Cf-Warnings` at the end of all commands [#69300730] +* Add an `actor` column to the `cf events` table [#68771710] + +## v6.1.0 +* Refresh auth token at the beginning of `cf push` [#69034628] +* `cf routes` should have an org and space requirement [#68917070] +* Fix a bug with binding services in manifests [#68768046] +* Make delete confirmation messages more consistent [#62852994] +* Don`t upload manifest.yml by default [#68952284] +* Ignore mercurial metadata from app upload [#68952326] +* Make delete commands output more consistent [#62283088] +* Make `cf create-user` idempotent [#67241604] +* Allow `cf unset-env` to remove the last env var an app has [#68879028] +* Add a datetime for when the binary was built [#68515588] +* Omit application files when CC reports all files are staged [#68290696] +* Show actual error message from server on async job failure [#65222140] +* Use new domains endpoints based on API version [#64525814] +* Use different events APIs based on API version [#64525814] +* Updated help text and messaging +* Events commands only shows last 50 events in reverse chronological order [#67248400, #63488318, #66900178] +* Add -r flag to `cf delete` for deleting all the routes mapped to the app [#65781990] +* Scope route listed to the current space [#59926924] +* Include empty directories when pushing apps [#63163454] +* Fetch UAA endpoint in auth command [#68035332] +* Improve error message when memory/disk is given w/o unit [#64359068] +* Only allow positive instances, memory or disk for `cf push` and `cf scale` [#66799710] +* Allow passing "null" as a buildpack url for "cf push" [#67054262] +* Add disk quota flag to push cmd [#65444560] +* Add a script for updating links to stable release [#67993678] +* Suggest using random-route when route is already taken [#66791058] +* Prompt user for all password-type credentials in login [#67864534] +* Add random-route property to manifests (push treats this the same as the --random-hostname flag) [#62086514] +* Add --random-route flag to `cf push` [#62086514] +* Fix create-user when UAA is being directly used as auth server (if the authorization server doesn`t return an UAA endpoint link, assume that the auth server is the UAA, and use it for user management) [#67477014] +* `cf create-user` hides private data in `CF_TRACE` [#67055200] +* Persist SSLDisabled flag on config [#66528632] +* Respect --skip-ssl-validation flag [#66528632] +* Hide passwords in `CF_TRACE` [#67055218] +* Improve `cf api` and `cf login` error message around SSL validation errors [#67048868] +* In `cf api`, fail if protocol not specified and ssl cert invalid [#67048868] +* Clear session at beginning of `cf auth` [#66638776] +* When renaming targetted org, update org name in config file [#63087464] +* Make `cf target` clear org and space when necessary [#66713898] +* Add a -f flag to scale to force [#64067896] +* Add a confirmation prompt to `cf scale` [#64067896] +* Verify SSL certs when fetching buildpacks [#66365558] +* OS X installer errors out when attempting to install on pre 10.7 [#66547206] +* Add ability to scale app`s disk limit [#65444078] +* Switch out Gamble for candied yaml [#66181944] + +## v6.0.2 +* Fixed `cf push -p path/to/app.zip` on windows with zip files (eg: .zip, .war, .jar) + +## v6.0.1 +* Added purge-service-offering and migrate-service-instances commands +* Added -a flag to `cf org-users` that makes the command display all users, rather than only privileged users (#46) +* Fixed a bug when manifest.yml was zero bytes +* Improved error messages for commands that reference users (#79) +* Fixed crash when a manifest didn`t contain environment variables but there were environment variables set for the app previously +* Improved error messages for commands that require an API endpoint to be set +* Added timeout to all asynchronous requests +* Fixed `bad file descriptor` crash when API token expired before file upload +* Added timestamps and version information to request logs when `CF_TRACE` is enabled +* Added fallback to default log server endpoint for compatibility with older CF deployments +* Improved error messages for services and target commands +* Added support for URLs as arguments to create-buildpack command +* Added a homebrew recipe for cf -- usage: brew install cloudfoundry-cli diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 00000000000..7eba2c0471a --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,70 @@ +{ + "ImportPath": "github.com/cloudfoundry/cli", + "GoVersion": "go1.2.2", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "code.google.com/p/go.crypto/ssh/terminal", + "Comment": "null-223", + "Rev": "31393df5baea9ed8d6778396e7f3896070ea5c04" + }, + { + "ImportPath": "code.google.com/p/go.net/websocket", + "Comment": "null-145", + "Rev": "81cc697d50c43e5525cf90ae3fa1699a51e02324" + }, + { + "ImportPath": "code.google.com/p/gogoprotobuf/proto", + "Rev": "6c980277330804e94257ac7ef70a3adbe1641059" + }, + { + "ImportPath": "github.com/cloudfoundry-incubator/candiedyaml", + "Rev": "39fb5c673e898bc3343f306217f91f485c40a831" + }, + { + "ImportPath": "github.com/cloudfoundry/gofileutils/fileutils", + "Rev": "a4a0cae5ff8a342ba01fce700b9e41c6942c0e5e" + }, + { + "ImportPath": "github.com/cloudfoundry/loggregator_consumer", + "Rev": "a35b4068c1a008dce5b2a78460ff9828e61b164b" + }, + { + "ImportPath": "github.com/cloudfoundry/loggregatorlib/logmessage", + "Rev": "59e4fb600f6a950861ecf3de2f99c71b15cf83d9" + }, + { + "ImportPath": "github.com/cloudfoundry/loggregatorlib/signature", + "Rev": "59e4fb600f6a950861ecf3de2f99c71b15cf83d9" + }, + { + "ImportPath": "github.com/codegangsta/cli", + "Comment": "1.2.0-24-g7381bc4", + "Rev": "7381bc4e62942763475703c7edd405f1e42adf4f" + }, + { + "ImportPath": "github.com/gorilla/websocket", + "Rev": "a6f041ac33b84488bb44137d6866cd3c8706d792" + }, + { + "ImportPath": "github.com/nicksnyder/go-i18n/i18n", + "Rev": "fdd9ce0eff0447ddec6b10625512f49a726418c1" + }, + { + "ImportPath": "github.com/onsi/ginkgo", + "Comment": "v1.1.0-28-g7d3d52b", + "Rev": "7d3d52b3a3a79d745c2a03ee81b5bc2086642201" + }, + { + "ImportPath": "github.com/onsi/gomega", + "Comment": "v1.0-12-g3ca2515", + "Rev": "3ca2515afb00bc39bc77d27cdbce85a7caaf103e" + }, + { + "ImportPath": "github.com/pivotal-cf-experimental/jibber_jabber", + "Rev": "901694860a59e63e1d392e6597ca919364f6abd1" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 00000000000..4cdaa53d56d --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 00000000000..f037d684ef2 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal.go new file mode 100644 index 00000000000..123de5ed32a --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal.go @@ -0,0 +1,811 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package terminal + +import ( + "io" + "sync" + "unicode/utf8" +) + +// EscapeCodes contains escape sequences that can be written to the terminal in +// order to achieve different styles of text. +type EscapeCodes struct { + // Foreground colors + Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte + + // Reset all attributes + Reset []byte +} + +var vt100EscapeCodes = EscapeCodes{ + Black: []byte{keyEscape, '[', '3', '0', 'm'}, + Red: []byte{keyEscape, '[', '3', '1', 'm'}, + Green: []byte{keyEscape, '[', '3', '2', 'm'}, + Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, + Blue: []byte{keyEscape, '[', '3', '4', 'm'}, + Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, + Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, + White: []byte{keyEscape, '[', '3', '7', 'm'}, + + Reset: []byte{keyEscape, '[', '0', 'm'}, +} + +// Terminal contains the state for running a VT100 terminal that is capable of +// reading lines of input. +type Terminal struct { + // AutoCompleteCallback, if non-null, is called for each keypress with + // the full input line and the current position of the cursor (in + // bytes, as an index into |line|). If it returns ok=false, the key + // press is processed normally. Otherwise it returns a replacement line + // and the new cursor position. + AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) + + // Escape contains a pointer to the escape codes for this terminal. + // It's always a valid pointer, although the escape codes themselves + // may be empty if the terminal doesn't support them. + Escape *EscapeCodes + + // lock protects the terminal and the state in this object from + // concurrent processing of a key press and a Write() call. + lock sync.Mutex + + c io.ReadWriter + prompt []rune + + // line is the current line being entered. + line []rune + // pos is the logical position of the cursor in line + pos int + // echo is true if local echo is enabled + echo bool + + // cursorX contains the current X value of the cursor where the left + // edge is 0. cursorY contains the row number where the first row of + // the current line is 0. + cursorX, cursorY int + // maxLine is the greatest value of cursorY so far. + maxLine int + + termWidth, termHeight int + + // outBuf contains the terminal data to be sent. + outBuf []byte + // remainder contains the remainder of any partial key sequences after + // a read. It aliases into inBuf. + remainder []byte + inBuf [256]byte + + // history contains previously entered commands so that they can be + // accessed with the up and down keys. + history stRingBuffer + // historyIndex stores the currently accessed history entry, where zero + // means the immediately previous entry. + historyIndex int + // When navigating up and down the history it's possible to return to + // the incomplete, initial line. That value is stored in + // historyPending. + historyPending string +} + +// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is +// a local terminal, that terminal must first have been put into raw mode. +// prompt is a string that is written at the start of each input line (i.e. +// "> "). +func NewTerminal(c io.ReadWriter, prompt string) *Terminal { + return &Terminal{ + Escape: &vt100EscapeCodes, + c: c, + prompt: []rune(prompt), + termWidth: 80, + termHeight: 24, + echo: true, + historyIndex: -1, + } +} + +const ( + keyCtrlD = 4 + keyCtrlU = 21 + keyEnter = '\r' + keyEscape = 27 + keyBackspace = 127 + keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota + keyUp + keyDown + keyLeft + keyRight + keyAltLeft + keyAltRight + keyHome + keyEnd + keyDeleteWord + keyDeleteLine + keyClearScreen +) + +// bytesToKey tries to parse a key sequence from b. If successful, it returns +// the key and the remainder of the input. Otherwise it returns utf8.RuneError. +func bytesToKey(b []byte) (rune, []byte) { + if len(b) == 0 { + return utf8.RuneError, nil + } + + switch b[0] { + case 1: // ^A + return keyHome, b[1:] + case 5: // ^E + return keyEnd, b[1:] + case 8: // ^H + return keyBackspace, b[1:] + case 11: // ^K + return keyDeleteLine, b[1:] + case 12: // ^L + return keyClearScreen, b[1:] + case 23: // ^W + return keyDeleteWord, b[1:] + } + + if b[0] != keyEscape { + if !utf8.FullRune(b) { + return utf8.RuneError, b + } + r, l := utf8.DecodeRune(b) + return r, b[l:] + } + + if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { + switch b[2] { + case 'A': + return keyUp, b[3:] + case 'B': + return keyDown, b[3:] + case 'C': + return keyRight, b[3:] + case 'D': + return keyLeft, b[3:] + } + } + + if len(b) >= 3 && b[0] == keyEscape && b[1] == 'O' { + switch b[2] { + case 'H': + return keyHome, b[3:] + case 'F': + return keyEnd, b[3:] + } + } + + if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { + switch b[5] { + case 'C': + return keyAltRight, b[6:] + case 'D': + return keyAltLeft, b[6:] + } + } + + // If we get here then we have a key that we don't recognise, or a + // partial sequence. It's not clear how one should find the end of a + // sequence without knowing them all, but it seems that [a-zA-Z] only + // appears at the end of a sequence. + for i, c := range b[0:] { + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { + return keyUnknown, b[i+1:] + } + } + + return utf8.RuneError, b +} + +// queue appends data to the end of t.outBuf +func (t *Terminal) queue(data []rune) { + t.outBuf = append(t.outBuf, []byte(string(data))...) +} + +var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} +var space = []rune{' '} + +func isPrintable(key rune) bool { + isInSurrogateArea := key >= 0xd800 && key <= 0xdbff + return key >= 32 && !isInSurrogateArea +} + +// moveCursorToPos appends data to t.outBuf which will move the cursor to the +// given, logical position in the text. +func (t *Terminal) moveCursorToPos(pos int) { + if !t.echo { + return + } + + x := visualLength(t.prompt) + pos + y := x / t.termWidth + x = x % t.termWidth + + up := 0 + if y < t.cursorY { + up = t.cursorY - y + } + + down := 0 + if y > t.cursorY { + down = y - t.cursorY + } + + left := 0 + if x < t.cursorX { + left = t.cursorX - x + } + + right := 0 + if x > t.cursorX { + right = x - t.cursorX + } + + t.cursorX = x + t.cursorY = y + t.move(up, down, left, right) +} + +func (t *Terminal) move(up, down, left, right int) { + movement := make([]rune, 3*(up+down+left+right)) + m := movement + for i := 0; i < up; i++ { + m[0] = keyEscape + m[1] = '[' + m[2] = 'A' + m = m[3:] + } + for i := 0; i < down; i++ { + m[0] = keyEscape + m[1] = '[' + m[2] = 'B' + m = m[3:] + } + for i := 0; i < left; i++ { + m[0] = keyEscape + m[1] = '[' + m[2] = 'D' + m = m[3:] + } + for i := 0; i < right; i++ { + m[0] = keyEscape + m[1] = '[' + m[2] = 'C' + m = m[3:] + } + + t.queue(movement) +} + +func (t *Terminal) clearLineToRight() { + op := []rune{keyEscape, '[', 'K'} + t.queue(op) +} + +const maxLineLength = 4096 + +func (t *Terminal) setLine(newLine []rune, newPos int) { + if t.echo { + t.moveCursorToPos(0) + t.writeLine(newLine) + for i := len(newLine); i < len(t.line); i++ { + t.writeLine(space) + } + t.moveCursorToPos(newPos) + } + t.line = newLine + t.pos = newPos +} + +func (t *Terminal) advanceCursor(places int) { + t.cursorX += places + t.cursorY += t.cursorX / t.termWidth + if t.cursorY > t.maxLine { + t.maxLine = t.cursorY + } + t.cursorX = t.cursorX % t.termWidth + + if places > 0 && t.cursorX == 0 { + // Normally terminals will advance the current position + // when writing a character. But that doesn't happen + // for the last character in a line. However, when + // writing a character (except a new line) that causes + // a line wrap, the position will be advanced two + // places. + // + // So, if we are stopping at the end of a line, we + // need to write a newline so that our cursor can be + // advanced to the next line. + t.outBuf = append(t.outBuf, '\n') + } +} + +func (t *Terminal) eraseNPreviousChars(n int) { + if n == 0 { + return + } + + if t.pos < n { + n = t.pos + } + t.pos -= n + t.moveCursorToPos(t.pos) + + copy(t.line[t.pos:], t.line[n+t.pos:]) + t.line = t.line[:len(t.line)-n] + if t.echo { + t.writeLine(t.line[t.pos:]) + for i := 0; i < n; i++ { + t.queue(space) + } + t.advanceCursor(n) + t.moveCursorToPos(t.pos) + } +} + +// countToLeftWord returns then number of characters from the cursor to the +// start of the previous word. +func (t *Terminal) countToLeftWord() int { + if t.pos == 0 { + return 0 + } + + pos := t.pos - 1 + for pos > 0 { + if t.line[pos] != ' ' { + break + } + pos-- + } + for pos > 0 { + if t.line[pos] == ' ' { + pos++ + break + } + pos-- + } + + return t.pos - pos +} + +// countToRightWord returns then number of characters from the cursor to the +// start of the next word. +func (t *Terminal) countToRightWord() int { + pos := t.pos + for pos < len(t.line) { + if t.line[pos] == ' ' { + break + } + pos++ + } + for pos < len(t.line) { + if t.line[pos] != ' ' { + break + } + pos++ + } + return pos - t.pos +} + +// visualLength returns the number of visible glyphs in s. +func visualLength(runes []rune) int { + inEscapeSeq := false + length := 0 + + for _, r := range runes { + switch { + case inEscapeSeq: + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + inEscapeSeq = false + } + case r == '\x1b': + inEscapeSeq = true + default: + length++ + } + } + + return length +} + +// handleKey processes the given key and, optionally, returns a line of text +// that the user has entered. +func (t *Terminal) handleKey(key rune) (line string, ok bool) { + switch key { + case keyBackspace: + if t.pos == 0 { + return + } + t.eraseNPreviousChars(1) + case keyAltLeft: + // move left by a word. + t.pos -= t.countToLeftWord() + t.moveCursorToPos(t.pos) + case keyAltRight: + // move right by a word. + t.pos += t.countToRightWord() + t.moveCursorToPos(t.pos) + case keyLeft: + if t.pos == 0 { + return + } + t.pos-- + t.moveCursorToPos(t.pos) + case keyRight: + if t.pos == len(t.line) { + return + } + t.pos++ + t.moveCursorToPos(t.pos) + case keyHome: + if t.pos == 0 { + return + } + t.pos = 0 + t.moveCursorToPos(t.pos) + case keyEnd: + if t.pos == len(t.line) { + return + } + t.pos = len(t.line) + t.moveCursorToPos(t.pos) + case keyUp: + entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) + if !ok { + return "", false + } + if t.historyIndex == -1 { + t.historyPending = string(t.line) + } + t.historyIndex++ + runes := []rune(entry) + t.setLine(runes, len(runes)) + case keyDown: + switch t.historyIndex { + case -1: + return + case 0: + runes := []rune(t.historyPending) + t.setLine(runes, len(runes)) + t.historyIndex-- + default: + entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + if ok { + t.historyIndex-- + runes := []rune(entry) + t.setLine(runes, len(runes)) + } + } + case keyEnter: + t.moveCursorToPos(len(t.line)) + t.queue([]rune("\r\n")) + line = string(t.line) + ok = true + t.line = t.line[:0] + t.pos = 0 + t.cursorX = 0 + t.cursorY = 0 + t.maxLine = 0 + case keyDeleteWord: + // Delete zero or more spaces and then one or more characters. + t.eraseNPreviousChars(t.countToLeftWord()) + case keyDeleteLine: + // Delete everything from the current cursor position to the + // end of line. + for i := t.pos; i < len(t.line); i++ { + t.queue(space) + t.advanceCursor(1) + } + t.line = t.line[:t.pos] + t.moveCursorToPos(t.pos) + case keyCtrlD: + // Erase the character under the current position. + // The EOF case when the line is empty is handled in + // readLine(). + if t.pos < len(t.line) { + t.pos++ + t.eraseNPreviousChars(1) + } + case keyCtrlU: + t.eraseNPreviousChars(t.pos) + case keyClearScreen: + // Erases the screen and moves the cursor to the home position. + t.queue([]rune("\x1b[2J\x1b[H")) + t.queue(t.prompt) + t.cursorX, t.cursorY = 0, 0 + t.advanceCursor(visualLength(t.prompt)) + t.setLine(t.line, t.pos) + default: + if t.AutoCompleteCallback != nil { + prefix := string(t.line[:t.pos]) + suffix := string(t.line[t.pos:]) + + t.lock.Unlock() + newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) + t.lock.Lock() + + if completeOk { + t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) + return + } + } + if !isPrintable(key) { + return + } + if len(t.line) == maxLineLength { + return + } + if len(t.line) == cap(t.line) { + newLine := make([]rune, len(t.line), 2*(1+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+1] + copy(t.line[t.pos+1:], t.line[t.pos:]) + t.line[t.pos] = key + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + } + return +} + +func (t *Terminal) writeLine(line []rune) { + for len(line) != 0 { + remainingOnLine := t.termWidth - t.cursorX + todo := len(line) + if todo > remainingOnLine { + todo = remainingOnLine + } + t.queue(line[:todo]) + t.advanceCursor(visualLength(line[:todo])) + line = line[todo:] + } +} + +func (t *Terminal) Write(buf []byte) (n int, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + if t.cursorX == 0 && t.cursorY == 0 { + // This is the easy case: there's nothing on the screen that we + // have to move out of the way. + return t.c.Write(buf) + } + + // We have a prompt and possibly user input on the screen. We + // have to clear it first. + t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) + t.cursorX = 0 + t.clearLineToRight() + + for t.cursorY > 0 { + t.move(1 /* up */, 0, 0, 0) + t.cursorY-- + t.clearLineToRight() + } + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + + if n, err = t.c.Write(buf); err != nil { + return + } + + t.writeLine(t.prompt) + if t.echo { + t.writeLine(t.line) + } + + t.moveCursorToPos(t.pos) + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + return +} + +// ReadPassword temporarily changes the prompt and reads a password, without +// echo, from the terminal. +func (t *Terminal) ReadPassword(prompt string) (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + oldPrompt := t.prompt + t.prompt = []rune(prompt) + t.echo = false + + line, err = t.readLine() + + t.prompt = oldPrompt + t.echo = true + + return +} + +// ReadLine returns a line of input from the terminal. +func (t *Terminal) ReadLine() (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + return t.readLine() +} + +func (t *Terminal) readLine() (line string, err error) { + // t.lock must be held at this point + + if t.cursorX == 0 && t.cursorY == 0 { + t.writeLine(t.prompt) + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + } + + for { + rest := t.remainder + lineOk := false + for !lineOk { + var key rune + key, rest = bytesToKey(rest) + if key == utf8.RuneError { + break + } + if key == keyCtrlD { + if len(t.line) == 0 { + return "", io.EOF + } + } + line, lineOk = t.handleKey(key) + } + if len(rest) > 0 { + n := copy(t.inBuf[:], rest) + t.remainder = t.inBuf[:n] + } else { + t.remainder = nil + } + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + if lineOk { + if t.echo { + t.historyIndex = -1 + t.history.Add(line) + } + return + } + + // t.remainder is a slice at the beginning of t.inBuf + // containing a partial key sequence + readBuf := t.inBuf[len(t.remainder):] + var n int + + t.lock.Unlock() + n, err = t.c.Read(readBuf) + t.lock.Lock() + + if err != nil { + return + } + + t.remainder = t.inBuf[:n+len(t.remainder)] + } + + panic("unreachable") // for Go 1.0. +} + +// SetPrompt sets the prompt to be used when reading subsequent lines. +func (t *Terminal) SetPrompt(prompt string) { + t.lock.Lock() + defer t.lock.Unlock() + + t.prompt = []rune(prompt) +} + +func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { + // Move cursor to column zero at the start of the line. + t.move(t.cursorY, 0, t.cursorX, 0) + t.cursorX, t.cursorY = 0, 0 + t.clearLineToRight() + for t.cursorY < numPrevLines { + // Move down a line + t.move(0, 1, 0, 0) + t.cursorY++ + t.clearLineToRight() + } + // Move back to beginning. + t.move(t.cursorY, 0, 0, 0) + t.cursorX, t.cursorY = 0, 0 + + t.queue(t.prompt) + t.advanceCursor(visualLength(t.prompt)) + t.writeLine(t.line) + t.moveCursorToPos(t.pos) +} + +func (t *Terminal) SetSize(width, height int) error { + t.lock.Lock() + defer t.lock.Unlock() + + oldWidth := t.termWidth + t.termWidth, t.termHeight = width, height + + switch { + case width == oldWidth || len(t.line) == 0: + // If the width didn't change then nothing else needs to be + // done. + return nil + case width < oldWidth: + // Some terminals (e.g. xterm) will truncate lines that were + // too long when shinking. Others, (e.g. gnome-terminal) will + // attempt to wrap them. For the former, repainting t.maxLine + // works great, but that behaviour goes badly wrong in the case + // of the latter because they have doubled every full line. + + // We assume that we are working on a terminal that wraps lines + // and adjust the cursor position based on every previous line + // wrapping and turning into two. This causes the prompt on + // xterms to move upwards, which isn't great, but it avoids a + // huge mess with gnome-terminal. + t.cursorY *= 2 + t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) + case width > oldWidth: + // If the terminal expands then our position calculations will + // be wrong in the future because we think the cursor is + // |t.pos| chars into the string, but there will be a gap at + // the end of any wrapped line. + // + // But the position will actually be correct until we move, so + // we can move back to the beginning and repaint everything. + t.clearAndRepaintLinePlusNPrevious(t.maxLine) + } + + _, err := t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + return err +} + +// stRingBuffer is a ring buffer of strings. +type stRingBuffer struct { + // entries contains max elements. + entries []string + max int + // head contains the index of the element most recently added to the ring. + head int + // size contains the number of elements in the ring. + size int +} + +func (s *stRingBuffer) Add(a string) { + if s.entries == nil { + const defaultNumEntries = 100 + s.entries = make([]string, defaultNumEntries) + s.max = defaultNumEntries + } + + s.head = (s.head + 1) % s.max + s.entries[s.head] = a + if s.size < s.max { + s.size++ + } +} + +// NthPreviousEntry returns the value passed to the nth previous call to Add. +// If n is zero then the immediately prior value is returned, if one, then the +// next most recent, and so on. If such an element doesn't exist then ok is +// false. +func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { + if n >= s.size { + return "", false + } + index := s.head - n + if index < 0 { + index += s.max + } + return s.entries[index], true +} diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal_test.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal_test.go new file mode 100644 index 00000000000..fb42d7601c3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/terminal_test.go @@ -0,0 +1,225 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package terminal + +import ( + "io" + "testing" +) + +type MockTerminal struct { + toSend []byte + bytesPerRead int + received []byte +} + +func (c *MockTerminal) Read(data []byte) (n int, err error) { + n = len(data) + if n == 0 { + return + } + if n > len(c.toSend) { + n = len(c.toSend) + } + if n == 0 { + return 0, io.EOF + } + if c.bytesPerRead > 0 && n > c.bytesPerRead { + n = c.bytesPerRead + } + copy(data, c.toSend[:n]) + c.toSend = c.toSend[n:] + return +} + +func (c *MockTerminal) Write(data []byte) (n int, err error) { + c.received = append(c.received, data...) + return len(data), nil +} + +func TestClose(t *testing.T) { + c := &MockTerminal{} + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if line != "" { + t.Errorf("Expected empty line but got: %s", line) + } + if err != io.EOF { + t.Errorf("Error should have been EOF but got: %s", err) + } +} + +var keyPressTests = []struct { + in string + line string + err error + throwAwayLines int +}{ + { + err: io.EOF, + }, + { + in: "\r", + line: "", + }, + { + in: "foo\r", + line: "foo", + }, + { + in: "a\x1b[Cb\r", // right + line: "ab", + }, + { + in: "a\x1b[Db\r", // left + line: "ba", + }, + { + in: "a\177b\r", // backspace + line: "b", + }, + { + in: "\x1b[A\r", // up + }, + { + in: "\x1b[B\r", // down + }, + { + in: "line\x1b[A\x1b[B\r", // up then down + line: "line", + }, + { + in: "line1\rline2\x1b[A\r", // recall previous line. + line: "line1", + throwAwayLines: 1, + }, + { + // recall two previous lines and append. + in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r", + line: "line1xxx", + throwAwayLines: 2, + }, + { + // Ctrl-A to move to beginning of line followed by ^K to kill + // line. + in: "a b \001\013\r", + line: "", + }, + { + // Ctrl-A to move to beginning of line, Ctrl-E to move to end, + // finally ^K to kill nothing. + in: "a b \001\005\013\r", + line: "a b ", + }, + { + in: "\027\r", + line: "", + }, + { + in: "a\027\r", + line: "", + }, + { + in: "a \027\r", + line: "", + }, + { + in: "a b\027\r", + line: "a ", + }, + { + in: "a b \027\r", + line: "a ", + }, + { + in: "one two thr\x1b[D\027\r", + line: "one two r", + }, + { + in: "\013\r", + line: "", + }, + { + in: "a\013\r", + line: "a", + }, + { + in: "ab\x1b[D\013\r", + line: "a", + }, + { + in: "Ξεσκεπάζω\r", + line: "Ξεσκεπάζω", + }, + { + in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace. + line: "", + throwAwayLines: 1, + }, + { + in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter. + line: "£", + throwAwayLines: 1, + }, + { + // Ctrl-D at the end of the line should be ignored. + in: "a\004\r", + line: "a", + }, + { + // a, b, left, Ctrl-D should erase the b. + in: "ab\x1b[D\004\r", + line: "a", + }, + { + // a, b, c, d, left, left, ^U should erase to the beginning of + // the line. + in: "abcd\x1b[D\x1b[D\025\r", + line: "cd", + }, +} + +func TestKeyPresses(t *testing.T) { + for i, test := range keyPressTests { + for j := 1; j < len(test.in); j++ { + c := &MockTerminal{ + toSend: []byte(test.in), + bytesPerRead: j, + } + ss := NewTerminal(c, "> ") + for k := 0; k < test.throwAwayLines; k++ { + _, err := ss.ReadLine() + if err != nil { + t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err) + } + } + line, err := ss.ReadLine() + if line != test.line { + t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) + break + } + if err != test.err { + t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) + break + } + } + } +} + +func TestPasswordNotSaved(t *testing.T) { + c := &MockTerminal{ + toSend: []byte("password\r\x1b[A\r"), + bytesPerRead: 1, + } + ss := NewTerminal(c, "> ") + pw, _ := ss.ReadPassword("> ") + if pw != "password" { + t.Fatalf("failed to read password, got %s", pw) + } + line, _ := ss.ReadLine() + if len(line) > 0 { + t.Fatalf("password was saved in history") + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util.go new file mode 100644 index 00000000000..0763c9a9789 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util.go @@ -0,0 +1,128 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "io" + "syscall" + "unsafe" +) + +// State contains the state of a terminal. +type State struct { + termios syscall.Termios +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF + newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + return err +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + var dimensions [4]uint16 + + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { + return -1, -1, err + } + return int(dimensions[1]), int(dimensions[0]), nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + var oldState syscall.Termios + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState + newState.Lflag &^= syscall.ECHO + newState.Lflag |= syscall.ICANON | syscall.ISIG + newState.Iflag |= syscall.ICRNL + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + defer func() { + syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) + }() + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(fd, buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_bsd.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_bsd.go new file mode 100644 index 00000000000..9c1ffd145a7 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_bsd.go @@ -0,0 +1,12 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd netbsd openbsd + +package terminal + +import "syscall" + +const ioctlReadTermios = syscall.TIOCGETA +const ioctlWriteTermios = syscall.TIOCSETA diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_linux.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_linux.go new file mode 100644 index 00000000000..5883b22d780 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_linux.go @@ -0,0 +1,11 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package terminal + +// These constants are declared here, rather than importing +// them from the syscall package as some syscall packages, even +// on linux, for example gccgo, do not declare them. +const ioctlReadTermios = 0x5401 // syscall.TCGETS +const ioctlWriteTermios = 0x5402 // syscall.TCSETS diff --git a/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_windows.go b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_windows.go new file mode 100644 index 00000000000..0a454e0eb9c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal/util_windows.go @@ -0,0 +1,171 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "io" + "syscall" + "unsafe" +) + +const ( + enableLineInput = 2 + enableEchoInput = 4 + enableProcessedInput = 1 + enableWindowInput = 8 + enableMouseInput = 16 + enableInsertMode = 32 + enableQuickEditMode = 64 + enableExtendedFlags = 128 + enableAutoPosition = 256 + enableProcessedOutput = 1 + enableWrapAtEolOutput = 2 +) + +var kernel32 = syscall.NewLazyDLL("kernel32.dll") + +var ( + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") +) + +type ( + short int16 + word uint16 + + coord struct { + x short + y short + } + smallRect struct { + left short + top short + right short + bottom short + } + consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord + } +) + +type State struct { + mode uint32 +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + st &^= (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) + _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) + if e != 0 { + return nil, error(e) + } + return &State{st}, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + return &State{st}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) + return err +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + var info consoleScreenBufferInfo + _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) + if e != 0 { + return 0, 0, error(e) + } + return int(info.size.x), int(info.size.y), nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + old := st + + st &^= (enableEchoInput) + st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) + _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) + if e != 0 { + return nil, error(e) + } + + defer func() { + syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) + }() + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(syscall.Handle(fd), buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/go.net/websocket/client.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/client.go new file mode 100644 index 00000000000..a861bb92c6c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/client.go @@ -0,0 +1,98 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "crypto/tls" + "io" + "net" + "net/http" + "net/url" +) + +// DialError is an error that occurs while dialling a websocket server. +type DialError struct { + *Config + Err error +} + +func (e *DialError) Error() string { + return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() +} + +// NewConfig creates a new WebSocket config for client connection. +func NewConfig(server, origin string) (config *Config, err error) { + config = new(Config) + config.Version = ProtocolVersionHybi13 + config.Location, err = url.ParseRequestURI(server) + if err != nil { + return + } + config.Origin, err = url.ParseRequestURI(origin) + if err != nil { + return + } + config.Header = http.Header(make(map[string][]string)) + return +} + +// NewClient creates a new WebSocket client connection over rwc. +func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + err = hybiClientHandshake(config, br, bw) + if err != nil { + return + } + buf := bufio.NewReadWriter(br, bw) + ws = newHybiClientConn(config, buf, rwc) + return +} + +// Dial opens a new client connection to a WebSocket. +func Dial(url_, protocol, origin string) (ws *Conn, err error) { + config, err := NewConfig(url_, origin) + if err != nil { + return nil, err + } + if protocol != "" { + config.Protocol = []string{protocol} + } + return DialConfig(config) +} + +// DialConfig opens a new client connection to a WebSocket with a config. +func DialConfig(config *Config) (ws *Conn, err error) { + var client net.Conn + if config.Location == nil { + return nil, &DialError{config, ErrBadWebSocketLocation} + } + if config.Origin == nil { + return nil, &DialError{config, ErrBadWebSocketOrigin} + } + switch config.Location.Scheme { + case "ws": + client, err = net.Dial("tcp", config.Location.Host) + + case "wss": + client, err = tls.Dial("tcp", config.Location.Host, config.TlsConfig) + + default: + err = ErrBadScheme + } + if err != nil { + goto Error + } + + ws, err = NewClient(config, client) + if err != nil { + goto Error + } + return + +Error: + return nil, &DialError{config, err} +} diff --git a/src/code.google.com/p/go.net/websocket/exampledial_test.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/exampledial_test.go similarity index 100% rename from src/code.google.com/p/go.net/websocket/exampledial_test.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/exampledial_test.go diff --git a/src/code.google.com/p/go.net/websocket/examplehandler_test.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/examplehandler_test.go similarity index 100% rename from src/code.google.com/p/go.net/websocket/examplehandler_test.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/examplehandler_test.go diff --git a/src/code.google.com/p/go.net/websocket/hybi.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi.go similarity index 96% rename from src/code.google.com/p/go.net/websocket/hybi.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi.go index 90f5d9ca01b..f8c0b2e2994 100644 --- a/src/code.google.com/p/go.net/websocket/hybi.go +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi.go @@ -385,21 +385,8 @@ func getNonceAccept(nonce []byte) (expected []byte, err error) { return } -func isHybiVersion(version int) bool { - switch version { - case ProtocolVersionHybi08, ProtocolVersionHybi13: - return true - default: - } - return false -} - // Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { - if !isHybiVersion(config.Version) { - panic("wrong protocol version.") - } - bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") bw.WriteString("Host: " + config.Location.Host + "\r\n") @@ -410,11 +397,12 @@ func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (er nonce = []byte(config.handshakeData["key"]) } bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") - if config.Version == ProtocolVersionHybi13 { - bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") - } else if config.Version == ProtocolVersionHybi08 { - bw.WriteString("Sec-WebSocket-Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") + bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") + + if config.Version != ProtocolVersionHybi13 { + return ErrBadProtocolVersion } + bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") if len(config.Protocol) > 0 { bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") @@ -500,8 +488,6 @@ func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Reques switch version { case "13": c.Version = ProtocolVersionHybi13 - case "8": - c.Version = ProtocolVersionHybi08 default: return http.StatusBadRequest, ErrBadWebSocketVersion } @@ -536,8 +522,6 @@ func Origin(config *Config, req *http.Request) (*url.URL, error) { switch config.Version { case ProtocolVersionHybi13: origin = req.Header.Get("Origin") - case ProtocolVersionHybi08: - origin = req.Header.Get("Sec-Websocket-Origin") } if origin == "null" { return nil, nil diff --git a/src/code.google.com/p/go.net/websocket/hybi_test.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi_test.go similarity index 84% rename from src/code.google.com/p/go.net/websocket/hybi_test.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi_test.go index 9f68e2830b3..d6a19108a6d 100644 --- a/src/code.google.com/p/go.net/websocket/hybi_test.go +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/hybi_test.go @@ -116,7 +116,7 @@ Sec-WebSocket-Protocol: chat config.Protocol = append(config.Protocol, "superchat") config.Version = ProtocolVersionHybi13 config.Header = http.Header(make(map[string][]string)) - config.Header.Add("UserFields-Agent", "test") + config.Header.Add("User-Agent", "test") config.handshakeData = map[string]string{ "key": "dGhlIHNhbXBsZSBub25jZQ==", @@ -148,69 +148,7 @@ Sec-WebSocket-Protocol: chat "Origin": config.Origin.String(), "Sec-Websocket-Protocol": "chat, superchat", "Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi13), - "UserFields-Agent": "test", - } - for k, v := range expectedHeader { - if req.Header.Get(k) != v { - t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k))) - } - } -} - -func TestHybiClientHandshakeHybi08(t *testing.T) { - b := bytes.NewBuffer([]byte{}) - bw := bufio.NewWriter(b) - br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols -Upgrade: websocket -Connection: Upgrade -Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= -Sec-WebSocket-Protocol: chat - -`)) - var err error - config := new(Config) - config.Location, err = url.ParseRequestURI("ws://server.example.com/chat") - if err != nil { - t.Fatal("location url", err) - } - config.Origin, err = url.ParseRequestURI("http://example.com") - if err != nil { - t.Fatal("origin url", err) - } - config.Protocol = append(config.Protocol, "chat") - config.Protocol = append(config.Protocol, "superchat") - config.Version = ProtocolVersionHybi08 - - config.handshakeData = map[string]string{ - "key": "dGhlIHNhbXBsZSBub25jZQ==", - } - err = hybiClientHandshake(config, br, bw) - if err != nil { - t.Errorf("handshake failed: %v", err) - } - req, err := http.ReadRequest(bufio.NewReader(b)) - if err != nil { - t.Fatalf("read request: %v", err) - } - if req.Method != "GET" { - t.Errorf("request method expected GET, but got %q", req.Method) - } - if req.URL.Path != "/chat" { - t.Errorf("request path expected /demo, but got %q", req.URL.Path) - } - if req.Proto != "HTTP/1.1" { - t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto) - } - if req.Host != "server.example.com" { - t.Errorf("request Host expected example.com, but got %v", req.Host) - } - var expectedHeader = map[string]string{ - "Connection": "Upgrade", - "Upgrade": "websocket", - "Sec-Websocket-Key": config.handshakeData["key"], - "Sec-Websocket-Origin": config.Origin.String(), - "Sec-Websocket-Protocol": "chat, superchat", - "Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi08), + "User-Agent": "test", } for k, v := range expectedHeader { if req.Header.Get(k) != v { @@ -314,52 +252,6 @@ Sec-WebSocket-Version: 13 } } -func TestHybiServerHandshakeHybi08(t *testing.T) { - config := new(Config) - handshaker := &hybiServerHandshaker{Config: config} - br := bufio.NewReader(strings.NewReader(`GET /chat HTTP/1.1 -Host: server.example.com -Upgrade: websocket -Connection: Upgrade -Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== -Sec-WebSocket-Origin: http://example.com -Sec-WebSocket-Protocol: chat, superchat -Sec-WebSocket-Version: 8 - -`)) - req, err := http.ReadRequest(br) - if err != nil { - t.Fatal("request", err) - } - code, err := handshaker.ReadHandshake(br, req) - if err != nil { - t.Errorf("handshake failed: %v", err) - } - if code != http.StatusSwitchingProtocols { - t.Errorf("status expected %q but got %q", http.StatusSwitchingProtocols, code) - } - b := bytes.NewBuffer([]byte{}) - bw := bufio.NewWriter(b) - - config.Protocol = []string{"chat"} - - err = handshaker.AcceptHandshake(bw) - if err != nil { - t.Errorf("handshake response failed: %v", err) - } - expectedResponse := strings.Join([]string{ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", - "Sec-WebSocket-Protocol: chat", - "", ""}, "\r\n") - - if b.String() != expectedResponse { - t.Errorf("handshake expected %q but got %q", expectedResponse, b.String()) - } -} - func TestHybiServerHandshakeHybiBadVersion(t *testing.T) { config := new(Config) handshaker := &hybiServerHandshaker{Config: config} diff --git a/Godeps/_workspace/src/code.google.com/p/go.net/websocket/server.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/server.go new file mode 100644 index 00000000000..70322133c49 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/server.go @@ -0,0 +1,114 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "fmt" + "io" + "net/http" +) + +func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { + var hs serverHandshaker = &hybiServerHandshaker{Config: config} + code, err := hs.ReadHandshake(buf.Reader, req) + if err == ErrBadWebSocketVersion { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if err != nil { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if handshake != nil { + err = handshake(config, req) + if err != nil { + code = http.StatusForbidden + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + } + err = hs.AcceptHandshake(buf.Writer) + if err != nil { + code = http.StatusBadRequest + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + conn = hs.NewServerConn(buf, rwc, req) + return +} + +// Server represents a server of a WebSocket. +type Server struct { + // Config is a WebSocket configuration for new WebSocket connection. + Config + + // Handshake is an optional function in WebSocket handshake. + // For example, you can check, or don't check Origin header. + // Another example, you can select config.Protocol. + Handshake func(*Config, *http.Request) error + + // Handler handles a WebSocket connection. + Handler +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.serveWebSocket(w, req) +} + +func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { + rwc, buf, err := w.(http.Hijacker).Hijack() + if err != nil { + panic("Hijack failed: " + err.Error()) + return + } + // The server should abort the WebSocket connection if it finds + // the client did not send a handshake that matches with protocol + // specification. + defer rwc.Close() + conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) + if err != nil { + return + } + if conn == nil { + panic("unexpected nil conn") + } + s.Handler(conn) +} + +// Handler is a simple interface to a WebSocket browser client. +// It checks if Origin header is valid URL by default. +// You might want to verify websocket.Conn.Config().Origin in the func. +// If you use Server instead of Handler, you could call websocket.Origin and +// check the origin in your Handshake func. So, if you want to accept +// non-browser client, which doesn't send Origin header, you could use Server +//. that doesn't check origin in its Handshake. +type Handler func(*Conn) + +func checkOrigin(config *Config, req *http.Request) (err error) { + config.Origin, err = Origin(config, req) + if err == nil && config.Origin == nil { + return fmt.Errorf("null origin") + } + return err +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s := Server{Handler: h, Handshake: checkOrigin} + s.serveWebSocket(w, req) +} diff --git a/src/code.google.com/p/go.net/websocket/websocket.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket.go similarity index 98% rename from src/code.google.com/p/go.net/websocket/websocket.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket.go index 861b3c68f2a..0f4917bf7e6 100644 --- a/src/code.google.com/p/go.net/websocket/websocket.go +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket.go @@ -21,13 +21,9 @@ import ( ) const ( - ProtocolVersionHixie75 = -75 - ProtocolVersionHixie76 = -76 - ProtocolVersionHybi00 = 0 - ProtocolVersionHybi08 = 8 ProtocolVersionHybi13 = 13 ProtocolVersionHybi = ProtocolVersionHybi13 - SupportedProtocolVersion = "13, 8" + SupportedProtocolVersion = "13" ContinuationFrame = 0 TextFrame = 1 @@ -133,7 +129,7 @@ type frameReaderFactory interface { // frameWriter is an interface to write a WebSocket frame. type frameWriter interface { - // Writer is to write playload of the frame. + // Writer is to write payload of the frame. io.WriteCloser } diff --git a/src/code.google.com/p/go.net/websocket/websocket_test.go b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket_test.go similarity index 94% rename from src/code.google.com/p/go.net/websocket/websocket_test.go rename to Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket_test.go index 53e445be380..48f14b696fa 100644 --- a/src/code.google.com/p/go.net/websocket/websocket_test.go +++ b/Godeps/_workspace/src/code.google.com/p/go.net/websocket/websocket_test.go @@ -245,7 +245,7 @@ func TestWithTwoProtocol(t *testing.T) { func TestWithBadProtocol(t *testing.T) { _, err := testWithProtocol(t, []string{"test"}) if err != ErrBadStatus { - t.Errorf("SubProto: expected %q, got %q", ErrBadStatus) + t.Errorf("SubProto: expected %v, got %v", ErrBadStatus, err) } } @@ -286,6 +286,20 @@ func TestTrailingSpaces(t *testing.T) { } } +func TestDialConfigBadVersion(t *testing.T) { + once.Do(startServer) + config := newConfig(t, "/echo") + config.Version = 1234 + + _, err := DialConfig(config) + + if dialerr, ok := err.(*DialError); ok { + if dialerr.Err != ErrBadProtocolVersion { + t.Errorf("dial expected err %q but got %q", ErrBadProtocolVersion, dialerr.Err) + } + } +} + func TestSmallBuffer(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=1145 // Read should be able to handle reading a fragment of a frame. diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/Makefile b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/Makefile new file mode 100644 index 00000000000..e99b839a7de --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/Makefile @@ -0,0 +1,40 @@ +# Go support for Protocol Buffers - Google's data interchange format +# +# Copyright 2010 The Go Authors. All rights reserved. +# http://code.google.com/p/goprotobuf/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +install: + go install + +test: install generate-test-pbs + go test + + +generate-test-pbs: + make install && cd testdata && make diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/all_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/all_test.go new file mode 100644 index 00000000000..bfcc9299938 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/all_test.go @@ -0,0 +1,1948 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "math/rand" + "reflect" + "runtime/debug" + "strings" + "testing" + "time" + + . "./testdata" + . "code.google.com/p/gogoprotobuf/proto" +) + +var globalO *Buffer + +func old() *Buffer { + if globalO == nil { + globalO = NewBuffer(nil) + } + globalO.Reset() + return globalO +} + +func equalbytes(b1, b2 []byte, t *testing.T) { + if len(b1) != len(b2) { + t.Errorf("wrong lengths: 2*%d != %d", len(b1), len(b2)) + return + } + for i := 0; i < len(b1); i++ { + if b1[i] != b2[i] { + t.Errorf("bad byte[%d]:%x %x: %s %s", i, b1[i], b2[i], b1, b2) + } + } +} + +func initGoTestField() *GoTestField { + f := new(GoTestField) + f.Label = String("label") + f.Type = String("type") + return f +} + +// These are all structurally equivalent but the tag numbers differ. +// (It's remarkable that required, optional, and repeated all have +// 8 letters.) +func initGoTest_RequiredGroup() *GoTest_RequiredGroup { + return &GoTest_RequiredGroup{ + RequiredField: String("required"), + } +} + +func initGoTest_OptionalGroup() *GoTest_OptionalGroup { + return &GoTest_OptionalGroup{ + RequiredField: String("optional"), + } +} + +func initGoTest_RepeatedGroup() *GoTest_RepeatedGroup { + return &GoTest_RepeatedGroup{ + RequiredField: String("repeated"), + } +} + +func initGoTest(setdefaults bool) *GoTest { + pb := new(GoTest) + if setdefaults { + pb.F_BoolDefaulted = Bool(Default_GoTest_F_BoolDefaulted) + pb.F_Int32Defaulted = Int32(Default_GoTest_F_Int32Defaulted) + pb.F_Int64Defaulted = Int64(Default_GoTest_F_Int64Defaulted) + pb.F_Fixed32Defaulted = Uint32(Default_GoTest_F_Fixed32Defaulted) + pb.F_Fixed64Defaulted = Uint64(Default_GoTest_F_Fixed64Defaulted) + pb.F_Uint32Defaulted = Uint32(Default_GoTest_F_Uint32Defaulted) + pb.F_Uint64Defaulted = Uint64(Default_GoTest_F_Uint64Defaulted) + pb.F_FloatDefaulted = Float32(Default_GoTest_F_FloatDefaulted) + pb.F_DoubleDefaulted = Float64(Default_GoTest_F_DoubleDefaulted) + pb.F_StringDefaulted = String(Default_GoTest_F_StringDefaulted) + pb.F_BytesDefaulted = Default_GoTest_F_BytesDefaulted + pb.F_Sint32Defaulted = Int32(Default_GoTest_F_Sint32Defaulted) + pb.F_Sint64Defaulted = Int64(Default_GoTest_F_Sint64Defaulted) + } + + pb.Kind = GoTest_TIME.Enum() + pb.RequiredField = initGoTestField() + pb.F_BoolRequired = Bool(true) + pb.F_Int32Required = Int32(3) + pb.F_Int64Required = Int64(6) + pb.F_Fixed32Required = Uint32(32) + pb.F_Fixed64Required = Uint64(64) + pb.F_Uint32Required = Uint32(3232) + pb.F_Uint64Required = Uint64(6464) + pb.F_FloatRequired = Float32(3232) + pb.F_DoubleRequired = Float64(6464) + pb.F_StringRequired = String("string") + pb.F_BytesRequired = []byte("bytes") + pb.F_Sint32Required = Int32(-32) + pb.F_Sint64Required = Int64(-64) + pb.Requiredgroup = initGoTest_RequiredGroup() + + return pb +} + +func fail(msg string, b *bytes.Buffer, s string, t *testing.T) { + data := b.Bytes() + ld := len(data) + ls := len(s) / 2 + + fmt.Printf("fail %s ld=%d ls=%d\n", msg, ld, ls) + + // find the interesting spot - n + n := ls + if ld < ls { + n = ld + } + j := 0 + for i := 0; i < n; i++ { + bs := hex(s[j])*16 + hex(s[j+1]) + j += 2 + if data[i] == bs { + continue + } + n = i + break + } + l := n - 10 + if l < 0 { + l = 0 + } + h := n + 10 + + // find the interesting spot - n + fmt.Printf("is[%d]:", l) + for i := l; i < h; i++ { + if i >= ld { + fmt.Printf(" --") + continue + } + fmt.Printf(" %.2x", data[i]) + } + fmt.Printf("\n") + + fmt.Printf("sb[%d]:", l) + for i := l; i < h; i++ { + if i >= ls { + fmt.Printf(" --") + continue + } + bs := hex(s[j])*16 + hex(s[j+1]) + j += 2 + fmt.Printf(" %.2x", bs) + } + fmt.Printf("\n") + + t.Fail() + + // t.Errorf("%s: \ngood: %s\nbad: %x", msg, s, b.Bytes()) + // Print the output in a partially-decoded format; can + // be helpful when updating the test. It produces the output + // that is pasted, with minor edits, into the argument to verify(). + // data := b.Bytes() + // nesting := 0 + // for b.Len() > 0 { + // start := len(data) - b.Len() + // var u uint64 + // u, err := DecodeVarint(b) + // if err != nil { + // fmt.Printf("decode error on varint:", err) + // return + // } + // wire := u & 0x7 + // tag := u >> 3 + // switch wire { + // case WireVarint: + // v, err := DecodeVarint(b) + // if err != nil { + // fmt.Printf("decode error on varint:", err) + // return + // } + // fmt.Printf("\t\t\"%x\" // field %d, encoding %d, value %d\n", + // data[start:len(data)-b.Len()], tag, wire, v) + // case WireFixed32: + // v, err := DecodeFixed32(b) + // if err != nil { + // fmt.Printf("decode error on fixed32:", err) + // return + // } + // fmt.Printf("\t\t\"%x\" // field %d, encoding %d, value %d\n", + // data[start:len(data)-b.Len()], tag, wire, v) + // case WireFixed64: + // v, err := DecodeFixed64(b) + // if err != nil { + // fmt.Printf("decode error on fixed64:", err) + // return + // } + // fmt.Printf("\t\t\"%x\" // field %d, encoding %d, value %d\n", + // data[start:len(data)-b.Len()], tag, wire, v) + // case WireBytes: + // nb, err := DecodeVarint(b) + // if err != nil { + // fmt.Printf("decode error on bytes:", err) + // return + // } + // after_tag := len(data) - b.Len() + // str := make([]byte, nb) + // _, err = b.Read(str) + // if err != nil { + // fmt.Printf("decode error on bytes:", err) + // return + // } + // fmt.Printf("\t\t\"%x\" \"%x\" // field %d, encoding %d (FIELD)\n", + // data[start:after_tag], str, tag, wire) + // case WireStartGroup: + // nesting++ + // fmt.Printf("\t\t\"%x\"\t\t// start group field %d level %d\n", + // data[start:len(data)-b.Len()], tag, nesting) + // case WireEndGroup: + // fmt.Printf("\t\t\"%x\"\t\t// end group field %d level %d\n", + // data[start:len(data)-b.Len()], tag, nesting) + // nesting-- + // default: + // fmt.Printf("unrecognized wire type %d\n", wire) + // return + // } + // } +} + +func hex(c uint8) uint8 { + if '0' <= c && c <= '9' { + return c - '0' + } + if 'a' <= c && c <= 'f' { + return 10 + c - 'a' + } + if 'A' <= c && c <= 'F' { + return 10 + c - 'A' + } + return 0 +} + +func equal(b []byte, s string, t *testing.T) bool { + if 2*len(b) != len(s) { + // fail(fmt.Sprintf("wrong lengths: 2*%d != %d", len(b), len(s)), b, s, t) + fmt.Printf("wrong lengths: 2*%d != %d\n", len(b), len(s)) + return false + } + for i, j := 0, 0; i < len(b); i, j = i+1, j+2 { + x := hex(s[j])*16 + hex(s[j+1]) + if b[i] != x { + // fail(fmt.Sprintf("bad byte[%d]:%x %x", i, b[i], x), b, s, t) + fmt.Printf("bad byte[%d]:%x %x", i, b[i], x) + return false + } + } + return true +} + +func overify(t *testing.T, pb *GoTest, expected string) { + o := old() + err := o.Marshal(pb) + if err != nil { + fmt.Printf("overify marshal-1 err = %v", err) + o.DebugPrint("", o.Bytes()) + t.Fatalf("expected = %s", expected) + } + if !equal(o.Bytes(), expected, t) { + o.DebugPrint("overify neq 1", o.Bytes()) + t.Fatalf("expected = %s", expected) + } + + // Now test Unmarshal by recreating the original buffer. + pbd := new(GoTest) + err = o.Unmarshal(pbd) + if err != nil { + t.Fatalf("overify unmarshal err = %v", err) + o.DebugPrint("", o.Bytes()) + t.Fatalf("string = %s", expected) + } + o.Reset() + err = o.Marshal(pbd) + if err != nil { + t.Errorf("overify marshal-2 err = %v", err) + o.DebugPrint("", o.Bytes()) + t.Fatalf("string = %s", expected) + } + if !equal(o.Bytes(), expected, t) { + o.DebugPrint("overify neq 2", o.Bytes()) + t.Fatalf("string = %s", expected) + } +} + +// Simple tests for numeric encode/decode primitives (varint, etc.) +func TestNumericPrimitives(t *testing.T) { + for i := uint64(0); i < 1e6; i += 111 { + o := old() + if o.EncodeVarint(i) != nil { + t.Error("EncodeVarint") + break + } + x, e := o.DecodeVarint() + if e != nil { + t.Fatal("DecodeVarint") + } + if x != i { + t.Fatal("varint decode fail:", i, x) + } + + o = old() + if o.EncodeFixed32(i) != nil { + t.Fatal("encFixed32") + } + x, e = o.DecodeFixed32() + if e != nil { + t.Fatal("decFixed32") + } + if x != i { + t.Fatal("fixed32 decode fail:", i, x) + } + + o = old() + if o.EncodeFixed64(i*1234567) != nil { + t.Error("encFixed64") + break + } + x, e = o.DecodeFixed64() + if e != nil { + t.Error("decFixed64") + break + } + if x != i*1234567 { + t.Error("fixed64 decode fail:", i*1234567, x) + break + } + + o = old() + i32 := int32(i - 12345) + if o.EncodeZigzag32(uint64(i32)) != nil { + t.Fatal("EncodeZigzag32") + } + x, e = o.DecodeZigzag32() + if e != nil { + t.Fatal("DecodeZigzag32") + } + if x != uint64(uint32(i32)) { + t.Fatal("zigzag32 decode fail:", i32, x) + } + + o = old() + i64 := int64(i - 12345) + if o.EncodeZigzag64(uint64(i64)) != nil { + t.Fatal("EncodeZigzag64") + } + x, e = o.DecodeZigzag64() + if e != nil { + t.Fatal("DecodeZigzag64") + } + if x != uint64(i64) { + t.Fatal("zigzag64 decode fail:", i64, x) + } + } +} + +// fakeMarshaler is a simple struct implementing Marshaler and Message interfaces. +type fakeMarshaler struct { + b []byte + err error +} + +func (f fakeMarshaler) Marshal() ([]byte, error) { + return f.b, f.err +} + +func (f fakeMarshaler) String() string { + return fmt.Sprintf("Bytes: %v Error: %v", f.b, f.err) +} + +func (f fakeMarshaler) ProtoMessage() {} + +func (f fakeMarshaler) Reset() {} + +// Simple tests for proto messages that implement the Marshaler interface. +func TestMarshalerEncoding(t *testing.T) { + tests := []struct { + name string + m Message + want []byte + wantErr error + }{ + { + name: "Marshaler that fails", + m: fakeMarshaler{ + err: errors.New("some marshal err"), + b: []byte{5, 6, 7}, + }, + // Since there's an error, nothing should be written to buffer. + want: nil, + wantErr: errors.New("some marshal err"), + }, + { + name: "Marshaler that succeeds", + m: fakeMarshaler{ + b: []byte{0, 1, 2, 3, 4, 127, 255}, + }, + want: []byte{0, 1, 2, 3, 4, 127, 255}, + wantErr: nil, + }, + } + for _, test := range tests { + b := NewBuffer(nil) + err := b.Marshal(test.m) + if !reflect.DeepEqual(test.wantErr, err) { + t.Errorf("%s: got err %v wanted %v", test.name, err, test.wantErr) + } + if !reflect.DeepEqual(test.want, b.Bytes()) { + t.Errorf("%s: got bytes %v wanted %v", test.name, b.Bytes(), test.want) + } + } +} + +// Simple tests for bytes +func TestBytesPrimitives(t *testing.T) { + o := old() + bytes := []byte{'n', 'o', 'w', ' ', 'i', 's', ' ', 't', 'h', 'e', ' ', 't', 'i', 'm', 'e'} + if o.EncodeRawBytes(bytes) != nil { + t.Error("EncodeRawBytes") + } + decb, e := o.DecodeRawBytes(false) + if e != nil { + t.Error("DecodeRawBytes") + } + equalbytes(bytes, decb, t) +} + +// Simple tests for strings +func TestStringPrimitives(t *testing.T) { + o := old() + s := "now is the time" + if o.EncodeStringBytes(s) != nil { + t.Error("enc_string") + } + decs, e := o.DecodeStringBytes() + if e != nil { + t.Error("dec_string") + } + if s != decs { + t.Error("string encode/decode fail:", s, decs) + } +} + +// Do we catch the "required bit not set" case? +func TestRequiredBit(t *testing.T) { + o := old() + pb := new(GoTest) + err := o.Marshal(pb) + if err == nil { + t.Error("did not catch missing required fields") + } else if strings.Index(err.Error(), "Kind") < 0 { + t.Error("wrong error type:", err) + } +} + +// Check that all fields are nil. +// Clearly silly, and a residue from a more interesting test with an earlier, +// different initialization property, but it once caught a compiler bug so +// it lives. +func checkInitialized(pb *GoTest, t *testing.T) { + if pb.F_BoolDefaulted != nil { + t.Error("New or Reset did not set boolean:", *pb.F_BoolDefaulted) + } + if pb.F_Int32Defaulted != nil { + t.Error("New or Reset did not set int32:", *pb.F_Int32Defaulted) + } + if pb.F_Int64Defaulted != nil { + t.Error("New or Reset did not set int64:", *pb.F_Int64Defaulted) + } + if pb.F_Fixed32Defaulted != nil { + t.Error("New or Reset did not set fixed32:", *pb.F_Fixed32Defaulted) + } + if pb.F_Fixed64Defaulted != nil { + t.Error("New or Reset did not set fixed64:", *pb.F_Fixed64Defaulted) + } + if pb.F_Uint32Defaulted != nil { + t.Error("New or Reset did not set uint32:", *pb.F_Uint32Defaulted) + } + if pb.F_Uint64Defaulted != nil { + t.Error("New or Reset did not set uint64:", *pb.F_Uint64Defaulted) + } + if pb.F_FloatDefaulted != nil { + t.Error("New or Reset did not set float:", *pb.F_FloatDefaulted) + } + if pb.F_DoubleDefaulted != nil { + t.Error("New or Reset did not set double:", *pb.F_DoubleDefaulted) + } + if pb.F_StringDefaulted != nil { + t.Error("New or Reset did not set string:", *pb.F_StringDefaulted) + } + if pb.F_BytesDefaulted != nil { + t.Error("New or Reset did not set bytes:", string(pb.F_BytesDefaulted)) + } + if pb.F_Sint32Defaulted != nil { + t.Error("New or Reset did not set int32:", *pb.F_Sint32Defaulted) + } + if pb.F_Sint64Defaulted != nil { + t.Error("New or Reset did not set int64:", *pb.F_Sint64Defaulted) + } +} + +// Does Reset() reset? +func TestReset(t *testing.T) { + pb := initGoTest(true) + // muck with some values + pb.F_BoolDefaulted = Bool(false) + pb.F_Int32Defaulted = Int32(237) + pb.F_Int64Defaulted = Int64(12346) + pb.F_Fixed32Defaulted = Uint32(32000) + pb.F_Fixed64Defaulted = Uint64(666) + pb.F_Uint32Defaulted = Uint32(323232) + pb.F_Uint64Defaulted = nil + pb.F_FloatDefaulted = nil + pb.F_DoubleDefaulted = Float64(0) + pb.F_StringDefaulted = String("gotcha") + pb.F_BytesDefaulted = []byte("asdfasdf") + pb.F_Sint32Defaulted = Int32(123) + pb.F_Sint64Defaulted = Int64(789) + pb.Reset() + checkInitialized(pb, t) +} + +// All required fields set, no defaults provided. +func TestEncodeDecode1(t *testing.T) { + pb := initGoTest(false) + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 0x20 + "714000000000000000"+ // field 14, encoding 1, value 0x40 + "78a019"+ // field 15, encoding 0, value 0xca0 = 3232 + "8001c032"+ // field 16, encoding 0, value 0x1940 = 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2, string "string" + "b304"+ // field 70, encoding 3, start group + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // field 70, encoding 4, end group + "aa0605"+"6279746573"+ // field 101, encoding 2, string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f") // field 103, encoding 0, 0x7f zigzag64 +} + +// All required fields set, defaults provided. +func TestEncodeDecode2(t *testing.T) { + pb := initGoTest(true) + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 32 + "714000000000000000"+ // field 14, encoding 1, value 64 + "78a019"+ // field 15, encoding 0, value 3232 + "8001c032"+ // field 16, encoding 0, value 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2 string "string" + "c00201"+ // field 40, encoding 0, value 1 + "c80220"+ // field 41, encoding 0, value 32 + "d00240"+ // field 42, encoding 0, value 64 + "dd0240010000"+ // field 43, encoding 5, value 320 + "e1028002000000000000"+ // field 44, encoding 1, value 640 + "e8028019"+ // field 45, encoding 0, value 3200 + "f0028032"+ // field 46, encoding 0, value 6400 + "fd02e0659948"+ // field 47, encoding 5, value 314159.0 + "81030000000050971041"+ // field 48, encoding 1, value 271828.0 + "8a0310"+"68656c6c6f2c2022776f726c6421220a"+ // field 49, encoding 2 string "hello, \"world!\"\n" + "b304"+ // start group field 70 level 1 + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // end group field 70 level 1 + "aa0605"+"6279746573"+ // field 101, encoding 2 string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f"+ // field 103, encoding 0, 0x7f zigzag64 + "8a1907"+"4269676e6f7365"+ // field 401, encoding 2, string "Bignose" + "90193f"+ // field 402, encoding 0, value 63 + "98197f") // field 403, encoding 0, value 127 + +} + +// All default fields set to their default value by hand +func TestEncodeDecode3(t *testing.T) { + pb := initGoTest(false) + pb.F_BoolDefaulted = Bool(true) + pb.F_Int32Defaulted = Int32(32) + pb.F_Int64Defaulted = Int64(64) + pb.F_Fixed32Defaulted = Uint32(320) + pb.F_Fixed64Defaulted = Uint64(640) + pb.F_Uint32Defaulted = Uint32(3200) + pb.F_Uint64Defaulted = Uint64(6400) + pb.F_FloatDefaulted = Float32(314159) + pb.F_DoubleDefaulted = Float64(271828) + pb.F_StringDefaulted = String("hello, \"world!\"\n") + pb.F_BytesDefaulted = []byte("Bignose") + pb.F_Sint32Defaulted = Int32(-32) + pb.F_Sint64Defaulted = Int64(-64) + + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 32 + "714000000000000000"+ // field 14, encoding 1, value 64 + "78a019"+ // field 15, encoding 0, value 3232 + "8001c032"+ // field 16, encoding 0, value 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2 string "string" + "c00201"+ // field 40, encoding 0, value 1 + "c80220"+ // field 41, encoding 0, value 32 + "d00240"+ // field 42, encoding 0, value 64 + "dd0240010000"+ // field 43, encoding 5, value 320 + "e1028002000000000000"+ // field 44, encoding 1, value 640 + "e8028019"+ // field 45, encoding 0, value 3200 + "f0028032"+ // field 46, encoding 0, value 6400 + "fd02e0659948"+ // field 47, encoding 5, value 314159.0 + "81030000000050971041"+ // field 48, encoding 1, value 271828.0 + "8a0310"+"68656c6c6f2c2022776f726c6421220a"+ // field 49, encoding 2 string "hello, \"world!\"\n" + "b304"+ // start group field 70 level 1 + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // end group field 70 level 1 + "aa0605"+"6279746573"+ // field 101, encoding 2 string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f"+ // field 103, encoding 0, 0x7f zigzag64 + "8a1907"+"4269676e6f7365"+ // field 401, encoding 2, string "Bignose" + "90193f"+ // field 402, encoding 0, value 63 + "98197f") // field 403, encoding 0, value 127 + +} + +// All required fields set, defaults provided, all non-defaulted optional fields have values. +func TestEncodeDecode4(t *testing.T) { + pb := initGoTest(true) + pb.Table = String("hello") + pb.Param = Int32(7) + pb.OptionalField = initGoTestField() + pb.F_BoolOptional = Bool(true) + pb.F_Int32Optional = Int32(32) + pb.F_Int64Optional = Int64(64) + pb.F_Fixed32Optional = Uint32(3232) + pb.F_Fixed64Optional = Uint64(6464) + pb.F_Uint32Optional = Uint32(323232) + pb.F_Uint64Optional = Uint64(646464) + pb.F_FloatOptional = Float32(32.) + pb.F_DoubleOptional = Float64(64.) + pb.F_StringOptional = String("hello") + pb.F_BytesOptional = []byte("Bignose") + pb.F_Sint32Optional = Int32(-32) + pb.F_Sint64Optional = Int64(-64) + pb.Optionalgroup = initGoTest_OptionalGroup() + + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "1205"+"68656c6c6f"+ // field 2, encoding 2, string "hello" + "1807"+ // field 3, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "320d"+"0a056c6162656c120474797065"+ // field 6, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 32 + "714000000000000000"+ // field 14, encoding 1, value 64 + "78a019"+ // field 15, encoding 0, value 3232 + "8001c032"+ // field 16, encoding 0, value 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2 string "string" + "f00101"+ // field 30, encoding 0, value 1 + "f80120"+ // field 31, encoding 0, value 32 + "800240"+ // field 32, encoding 0, value 64 + "8d02a00c0000"+ // field 33, encoding 5, value 3232 + "91024019000000000000"+ // field 34, encoding 1, value 6464 + "9802a0dd13"+ // field 35, encoding 0, value 323232 + "a002c0ba27"+ // field 36, encoding 0, value 646464 + "ad0200000042"+ // field 37, encoding 5, value 32.0 + "b1020000000000005040"+ // field 38, encoding 1, value 64.0 + "ba0205"+"68656c6c6f"+ // field 39, encoding 2, string "hello" + "c00201"+ // field 40, encoding 0, value 1 + "c80220"+ // field 41, encoding 0, value 32 + "d00240"+ // field 42, encoding 0, value 64 + "dd0240010000"+ // field 43, encoding 5, value 320 + "e1028002000000000000"+ // field 44, encoding 1, value 640 + "e8028019"+ // field 45, encoding 0, value 3200 + "f0028032"+ // field 46, encoding 0, value 6400 + "fd02e0659948"+ // field 47, encoding 5, value 314159.0 + "81030000000050971041"+ // field 48, encoding 1, value 271828.0 + "8a0310"+"68656c6c6f2c2022776f726c6421220a"+ // field 49, encoding 2 string "hello, \"world!\"\n" + "b304"+ // start group field 70 level 1 + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // end group field 70 level 1 + "d305"+ // start group field 90 level 1 + "da0508"+"6f7074696f6e616c"+ // field 91, encoding 2, string "optional" + "d405"+ // end group field 90 level 1 + "aa0605"+"6279746573"+ // field 101, encoding 2 string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f"+ // field 103, encoding 0, 0x7f zigzag64 + "ea1207"+"4269676e6f7365"+ // field 301, encoding 2, string "Bignose" + "f0123f"+ // field 302, encoding 0, value 63 + "f8127f"+ // field 303, encoding 0, value 127 + "8a1907"+"4269676e6f7365"+ // field 401, encoding 2, string "Bignose" + "90193f"+ // field 402, encoding 0, value 63 + "98197f") // field 403, encoding 0, value 127 + +} + +// All required fields set, defaults provided, all repeated fields given two values. +func TestEncodeDecode5(t *testing.T) { + pb := initGoTest(true) + pb.RepeatedField = []*GoTestField{initGoTestField(), initGoTestField()} + pb.F_BoolRepeated = []bool{false, true} + pb.F_Int32Repeated = []int32{32, 33} + pb.F_Int64Repeated = []int64{64, 65} + pb.F_Fixed32Repeated = []uint32{3232, 3333} + pb.F_Fixed64Repeated = []uint64{6464, 6565} + pb.F_Uint32Repeated = []uint32{323232, 333333} + pb.F_Uint64Repeated = []uint64{646464, 656565} + pb.F_FloatRepeated = []float32{32., 33.} + pb.F_DoubleRepeated = []float64{64., 65.} + pb.F_StringRepeated = []string{"hello", "sailor"} + pb.F_BytesRepeated = [][]byte{[]byte("big"), []byte("nose")} + pb.F_Sint32Repeated = []int32{32, -32} + pb.F_Sint64Repeated = []int64{64, -64} + pb.Repeatedgroup = []*GoTest_RepeatedGroup{initGoTest_RepeatedGroup(), initGoTest_RepeatedGroup()} + + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "2a0d"+"0a056c6162656c120474797065"+ // field 5, encoding 2 (GoTestField) + "2a0d"+"0a056c6162656c120474797065"+ // field 5, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 32 + "714000000000000000"+ // field 14, encoding 1, value 64 + "78a019"+ // field 15, encoding 0, value 3232 + "8001c032"+ // field 16, encoding 0, value 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2 string "string" + "a00100"+ // field 20, encoding 0, value 0 + "a00101"+ // field 20, encoding 0, value 1 + "a80120"+ // field 21, encoding 0, value 32 + "a80121"+ // field 21, encoding 0, value 33 + "b00140"+ // field 22, encoding 0, value 64 + "b00141"+ // field 22, encoding 0, value 65 + "bd01a00c0000"+ // field 23, encoding 5, value 3232 + "bd01050d0000"+ // field 23, encoding 5, value 3333 + "c1014019000000000000"+ // field 24, encoding 1, value 6464 + "c101a519000000000000"+ // field 24, encoding 1, value 6565 + "c801a0dd13"+ // field 25, encoding 0, value 323232 + "c80195ac14"+ // field 25, encoding 0, value 333333 + "d001c0ba27"+ // field 26, encoding 0, value 646464 + "d001b58928"+ // field 26, encoding 0, value 656565 + "dd0100000042"+ // field 27, encoding 5, value 32.0 + "dd0100000442"+ // field 27, encoding 5, value 33.0 + "e1010000000000005040"+ // field 28, encoding 1, value 64.0 + "e1010000000000405040"+ // field 28, encoding 1, value 65.0 + "ea0105"+"68656c6c6f"+ // field 29, encoding 2, string "hello" + "ea0106"+"7361696c6f72"+ // field 29, encoding 2, string "sailor" + "c00201"+ // field 40, encoding 0, value 1 + "c80220"+ // field 41, encoding 0, value 32 + "d00240"+ // field 42, encoding 0, value 64 + "dd0240010000"+ // field 43, encoding 5, value 320 + "e1028002000000000000"+ // field 44, encoding 1, value 640 + "e8028019"+ // field 45, encoding 0, value 3200 + "f0028032"+ // field 46, encoding 0, value 6400 + "fd02e0659948"+ // field 47, encoding 5, value 314159.0 + "81030000000050971041"+ // field 48, encoding 1, value 271828.0 + "8a0310"+"68656c6c6f2c2022776f726c6421220a"+ // field 49, encoding 2 string "hello, \"world!\"\n" + "b304"+ // start group field 70 level 1 + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // end group field 70 level 1 + "8305"+ // start group field 80 level 1 + "8a0508"+"7265706561746564"+ // field 81, encoding 2, string "repeated" + "8405"+ // end group field 80 level 1 + "8305"+ // start group field 80 level 1 + "8a0508"+"7265706561746564"+ // field 81, encoding 2, string "repeated" + "8405"+ // end group field 80 level 1 + "aa0605"+"6279746573"+ // field 101, encoding 2 string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f"+ // field 103, encoding 0, 0x7f zigzag64 + "ca0c03"+"626967"+ // field 201, encoding 2, string "big" + "ca0c04"+"6e6f7365"+ // field 201, encoding 2, string "nose" + "d00c40"+ // field 202, encoding 0, value 32 + "d00c3f"+ // field 202, encoding 0, value -32 + "d80c8001"+ // field 203, encoding 0, value 64 + "d80c7f"+ // field 203, encoding 0, value -64 + "8a1907"+"4269676e6f7365"+ // field 401, encoding 2, string "Bignose" + "90193f"+ // field 402, encoding 0, value 63 + "98197f") // field 403, encoding 0, value 127 + +} + +// All required fields set, all packed repeated fields given two values. +func TestEncodeDecode6(t *testing.T) { + pb := initGoTest(false) + pb.F_BoolRepeatedPacked = []bool{false, true} + pb.F_Int32RepeatedPacked = []int32{32, 33} + pb.F_Int64RepeatedPacked = []int64{64, 65} + pb.F_Fixed32RepeatedPacked = []uint32{3232, 3333} + pb.F_Fixed64RepeatedPacked = []uint64{6464, 6565} + pb.F_Uint32RepeatedPacked = []uint32{323232, 333333} + pb.F_Uint64RepeatedPacked = []uint64{646464, 656565} + pb.F_FloatRepeatedPacked = []float32{32., 33.} + pb.F_DoubleRepeatedPacked = []float64{64., 65.} + pb.F_Sint32RepeatedPacked = []int32{32, -32} + pb.F_Sint64RepeatedPacked = []int64{64, -64} + + overify(t, pb, + "0807"+ // field 1, encoding 0, value 7 + "220d"+"0a056c6162656c120474797065"+ // field 4, encoding 2 (GoTestField) + "5001"+ // field 10, encoding 0, value 1 + "5803"+ // field 11, encoding 0, value 3 + "6006"+ // field 12, encoding 0, value 6 + "6d20000000"+ // field 13, encoding 5, value 32 + "714000000000000000"+ // field 14, encoding 1, value 64 + "78a019"+ // field 15, encoding 0, value 3232 + "8001c032"+ // field 16, encoding 0, value 6464 + "8d0100004a45"+ // field 17, encoding 5, value 3232.0 + "9101000000000040b940"+ // field 18, encoding 1, value 6464.0 + "9a0106"+"737472696e67"+ // field 19, encoding 2 string "string" + "9203020001"+ // field 50, encoding 2, 2 bytes, value 0, value 1 + "9a03022021"+ // field 51, encoding 2, 2 bytes, value 32, value 33 + "a203024041"+ // field 52, encoding 2, 2 bytes, value 64, value 65 + "aa0308"+ // field 53, encoding 2, 8 bytes + "a00c0000050d0000"+ // value 3232, value 3333 + "b20310"+ // field 54, encoding 2, 16 bytes + "4019000000000000a519000000000000"+ // value 6464, value 6565 + "ba0306"+ // field 55, encoding 2, 6 bytes + "a0dd1395ac14"+ // value 323232, value 333333 + "c20306"+ // field 56, encoding 2, 6 bytes + "c0ba27b58928"+ // value 646464, value 656565 + "ca0308"+ // field 57, encoding 2, 8 bytes + "0000004200000442"+ // value 32.0, value 33.0 + "d20310"+ // field 58, encoding 2, 16 bytes + "00000000000050400000000000405040"+ // value 64.0, value 65.0 + "b304"+ // start group field 70 level 1 + "ba0408"+"7265717569726564"+ // field 71, encoding 2, string "required" + "b404"+ // end group field 70 level 1 + "aa0605"+"6279746573"+ // field 101, encoding 2 string "bytes" + "b0063f"+ // field 102, encoding 0, 0x3f zigzag32 + "b8067f"+ // field 103, encoding 0, 0x7f zigzag64 + "b21f02"+ // field 502, encoding 2, 2 bytes + "403f"+ // value 32, value -32 + "ba1f03"+ // field 503, encoding 2, 3 bytes + "80017f") // value 64, value -64 +} + +// Test that we can encode empty bytes fields. +func TestEncodeDecodeBytes1(t *testing.T) { + pb := initGoTest(false) + + // Create our bytes + pb.F_BytesRequired = []byte{} + pb.F_BytesRepeated = [][]byte{{}} + pb.F_BytesOptional = []byte{} + + d, err := Marshal(pb) + if err != nil { + t.Error(err) + } + + pbd := new(GoTest) + if err := Unmarshal(d, pbd); err != nil { + t.Error(err) + } + + if pbd.F_BytesRequired == nil || len(pbd.F_BytesRequired) != 0 { + t.Error("required empty bytes field is incorrect") + } + if pbd.F_BytesRepeated == nil || len(pbd.F_BytesRepeated) == 1 && pbd.F_BytesRepeated[0] == nil { + t.Error("repeated empty bytes field is incorrect") + } + if pbd.F_BytesOptional == nil || len(pbd.F_BytesOptional) != 0 { + t.Error("optional empty bytes field is incorrect") + } +} + +// Test that we encode nil-valued fields of a repeated bytes field correctly. +// Since entries in a repeated field cannot be nil, nil must mean empty value. +func TestEncodeDecodeBytes2(t *testing.T) { + pb := initGoTest(false) + + // Create our bytes + pb.F_BytesRepeated = [][]byte{nil} + + d, err := Marshal(pb) + if err != nil { + t.Error(err) + } + + pbd := new(GoTest) + if err := Unmarshal(d, pbd); err != nil { + t.Error(err) + } + + if len(pbd.F_BytesRepeated) != 1 || pbd.F_BytesRepeated[0] == nil { + t.Error("Unexpected value for repeated bytes field") + } +} + +// All required fields set, defaults provided, all repeated fields given two values. +func TestSkippingUnrecognizedFields(t *testing.T) { + o := old() + pb := initGoTestField() + + // Marshal it normally. + o.Marshal(pb) + + // Now new a GoSkipTest record. + skip := &GoSkipTest{ + SkipInt32: Int32(32), + SkipFixed32: Uint32(3232), + SkipFixed64: Uint64(6464), + SkipString: String("skipper"), + Skipgroup: &GoSkipTest_SkipGroup{ + GroupInt32: Int32(75), + GroupString: String("wxyz"), + }, + } + + // Marshal it into same buffer. + o.Marshal(skip) + + pbd := new(GoTestField) + o.Unmarshal(pbd) + + // The __unrecognized field should be a marshaling of GoSkipTest + skipd := new(GoSkipTest) + + o.SetBuf(pbd.XXX_unrecognized) + o.Unmarshal(skipd) + + if *skipd.SkipInt32 != *skip.SkipInt32 { + t.Error("skip int32", skipd.SkipInt32) + } + if *skipd.SkipFixed32 != *skip.SkipFixed32 { + t.Error("skip fixed32", skipd.SkipFixed32) + } + if *skipd.SkipFixed64 != *skip.SkipFixed64 { + t.Error("skip fixed64", skipd.SkipFixed64) + } + if *skipd.SkipString != *skip.SkipString { + t.Error("skip string", *skipd.SkipString) + } + if *skipd.Skipgroup.GroupInt32 != *skip.Skipgroup.GroupInt32 { + t.Error("skip group int32", skipd.Skipgroup.GroupInt32) + } + if *skipd.Skipgroup.GroupString != *skip.Skipgroup.GroupString { + t.Error("skip group string", *skipd.Skipgroup.GroupString) + } +} + +// Check that unrecognized fields of a submessage are preserved. +func TestSubmessageUnrecognizedFields(t *testing.T) { + nm := &NewMessage{ + Nested: &NewMessage_Nested{ + Name: String("Nigel"), + FoodGroup: String("carbs"), + }, + } + b, err := Marshal(nm) + if err != nil { + t.Fatalf("Marshal of NewMessage: %v", err) + } + + // Unmarshal into an OldMessage. + om := new(OldMessage) + if err := Unmarshal(b, om); err != nil { + t.Fatalf("Unmarshal to OldMessage: %v", err) + } + exp := &OldMessage{ + Nested: &OldMessage_Nested{ + Name: String("Nigel"), + // normal protocol buffer users should not do this + XXX_unrecognized: []byte("\x12\x05carbs"), + }, + } + if !Equal(om, exp) { + t.Errorf("om = %v, want %v", om, exp) + } + + // Clone the OldMessage. + om = Clone(om).(*OldMessage) + if !Equal(om, exp) { + t.Errorf("Clone(om) = %v, want %v", om, exp) + } + + // Marshal the OldMessage, then unmarshal it into an empty NewMessage. + if b, err = Marshal(om); err != nil { + t.Fatalf("Marshal of OldMessage: %v", err) + } + t.Logf("Marshal(%v) -> %q", om, b) + nm2 := new(NewMessage) + if err := Unmarshal(b, nm2); err != nil { + t.Fatalf("Unmarshal to NewMessage: %v", err) + } + if !Equal(nm, nm2) { + t.Errorf("NewMessage round-trip: %v => %v", nm, nm2) + } +} + +// Check that we can grow an array (repeated field) to have many elements. +// This test doesn't depend only on our encoding; for variety, it makes sure +// we create, encode, and decode the correct contents explicitly. It's therefore +// a bit messier. +// This test also uses (and hence tests) the Marshal/Unmarshal functions +// instead of the methods. +func TestBigRepeated(t *testing.T) { + pb := initGoTest(true) + + // Create the arrays + const N = 50 // Internally the library starts much smaller. + pb.Repeatedgroup = make([]*GoTest_RepeatedGroup, N) + pb.F_Sint64Repeated = make([]int64, N) + pb.F_Sint32Repeated = make([]int32, N) + pb.F_BytesRepeated = make([][]byte, N) + pb.F_StringRepeated = make([]string, N) + pb.F_DoubleRepeated = make([]float64, N) + pb.F_FloatRepeated = make([]float32, N) + pb.F_Uint64Repeated = make([]uint64, N) + pb.F_Uint32Repeated = make([]uint32, N) + pb.F_Fixed64Repeated = make([]uint64, N) + pb.F_Fixed32Repeated = make([]uint32, N) + pb.F_Int64Repeated = make([]int64, N) + pb.F_Int32Repeated = make([]int32, N) + pb.F_BoolRepeated = make([]bool, N) + pb.RepeatedField = make([]*GoTestField, N) + + // Fill in the arrays with checkable values. + igtf := initGoTestField() + igtrg := initGoTest_RepeatedGroup() + for i := 0; i < N; i++ { + pb.Repeatedgroup[i] = igtrg + pb.F_Sint64Repeated[i] = int64(i) + pb.F_Sint32Repeated[i] = int32(i) + s := fmt.Sprint(i) + pb.F_BytesRepeated[i] = []byte(s) + pb.F_StringRepeated[i] = s + pb.F_DoubleRepeated[i] = float64(i) + pb.F_FloatRepeated[i] = float32(i) + pb.F_Uint64Repeated[i] = uint64(i) + pb.F_Uint32Repeated[i] = uint32(i) + pb.F_Fixed64Repeated[i] = uint64(i) + pb.F_Fixed32Repeated[i] = uint32(i) + pb.F_Int64Repeated[i] = int64(i) + pb.F_Int32Repeated[i] = int32(i) + pb.F_BoolRepeated[i] = i%2 == 0 + pb.RepeatedField[i] = igtf + } + + // Marshal. + buf, _ := Marshal(pb) + + // Now test Unmarshal by recreating the original buffer. + pbd := new(GoTest) + Unmarshal(buf, pbd) + + // Check the checkable values + for i := uint64(0); i < N; i++ { + if pbd.Repeatedgroup[i] == nil { // TODO: more checking? + t.Error("pbd.Repeatedgroup bad") + } + var x uint64 + x = uint64(pbd.F_Sint64Repeated[i]) + if x != i { + t.Error("pbd.F_Sint64Repeated bad", x, i) + } + x = uint64(pbd.F_Sint32Repeated[i]) + if x != i { + t.Error("pbd.F_Sint32Repeated bad", x, i) + } + s := fmt.Sprint(i) + equalbytes(pbd.F_BytesRepeated[i], []byte(s), t) + if pbd.F_StringRepeated[i] != s { + t.Error("pbd.F_Sint32Repeated bad", pbd.F_StringRepeated[i], i) + } + x = uint64(pbd.F_DoubleRepeated[i]) + if x != i { + t.Error("pbd.F_DoubleRepeated bad", x, i) + } + x = uint64(pbd.F_FloatRepeated[i]) + if x != i { + t.Error("pbd.F_FloatRepeated bad", x, i) + } + x = pbd.F_Uint64Repeated[i] + if x != i { + t.Error("pbd.F_Uint64Repeated bad", x, i) + } + x = uint64(pbd.F_Uint32Repeated[i]) + if x != i { + t.Error("pbd.F_Uint32Repeated bad", x, i) + } + x = pbd.F_Fixed64Repeated[i] + if x != i { + t.Error("pbd.F_Fixed64Repeated bad", x, i) + } + x = uint64(pbd.F_Fixed32Repeated[i]) + if x != i { + t.Error("pbd.F_Fixed32Repeated bad", x, i) + } + x = uint64(pbd.F_Int64Repeated[i]) + if x != i { + t.Error("pbd.F_Int64Repeated bad", x, i) + } + x = uint64(pbd.F_Int32Repeated[i]) + if x != i { + t.Error("pbd.F_Int32Repeated bad", x, i) + } + if pbd.F_BoolRepeated[i] != (i%2 == 0) { + t.Error("pbd.F_BoolRepeated bad", x, i) + } + if pbd.RepeatedField[i] == nil { // TODO: more checking? + t.Error("pbd.RepeatedField bad") + } + } +} + +// Verify we give a useful message when decoding to the wrong structure type. +func TestTypeMismatch(t *testing.T) { + pb1 := initGoTest(true) + + // Marshal + o := old() + o.Marshal(pb1) + + // Now Unmarshal it to the wrong type. + pb2 := initGoTestField() + err := o.Unmarshal(pb2) + if err == nil { + t.Error("expected error, got no error") + } else if !strings.Contains(err.Error(), "bad wiretype") { + t.Error("expected bad wiretype error, got", err) + } +} + +func encodeDecode(t *testing.T, in, out Message, msg string) { + buf, err := Marshal(in) + if err != nil { + t.Fatalf("failed marshaling %v: %v", msg, err) + } + if err := Unmarshal(buf, out); err != nil { + t.Fatalf("failed unmarshaling %v: %v", msg, err) + } +} + +func TestPackedNonPackedDecoderSwitching(t *testing.T) { + np, p := new(NonPackedTest), new(PackedTest) + + // non-packed -> packed + np.A = []int32{0, 1, 1, 2, 3, 5} + encodeDecode(t, np, p, "non-packed -> packed") + if !reflect.DeepEqual(np.A, p.B) { + t.Errorf("failed non-packed -> packed; np.A=%+v, p.B=%+v", np.A, p.B) + } + + // packed -> non-packed + np.Reset() + p.B = []int32{3, 1, 4, 1, 5, 9} + encodeDecode(t, p, np, "packed -> non-packed") + if !reflect.DeepEqual(p.B, np.A) { + t.Errorf("failed packed -> non-packed; p.B=%+v, np.A=%+v", p.B, np.A) + } +} + +func TestProto1RepeatedGroup(t *testing.T) { + pb := &MessageList{ + Message: []*MessageList_Message{ + { + Name: String("blah"), + Count: Int32(7), + }, + // NOTE: pb.Message[1] is a nil + nil, + }, + } + + o := old() + if err := o.Marshal(pb); err != ErrRepeatedHasNil { + t.Fatalf("unexpected or no error when marshaling: %v", err) + } +} + +// Test that enums work. Checks for a bug introduced by making enums +// named types instead of int32: newInt32FromUint64 would crash with +// a type mismatch in reflect.PointTo. +func TestEnum(t *testing.T) { + pb := new(GoEnum) + pb.Foo = FOO_FOO1.Enum() + o := old() + if err := o.Marshal(pb); err != nil { + t.Fatal("error encoding enum:", err) + } + pb1 := new(GoEnum) + if err := o.Unmarshal(pb1); err != nil { + t.Fatal("error decoding enum:", err) + } + if *pb1.Foo != FOO_FOO1 { + t.Error("expected 7 but got ", *pb1.Foo) + } +} + +// Enum types have String methods. Check that enum fields can be printed. +// We don't care what the value actually is, just as long as it doesn't crash. +func TestPrintingNilEnumFields(t *testing.T) { + pb := new(GoEnum) + fmt.Sprintf("%+v", pb) +} + +// Verify that absent required fields cause Marshal/Unmarshal to return errors. +func TestRequiredFieldEnforcement(t *testing.T) { + pb := new(GoTestField) + _, err := Marshal(pb) + if err == nil { + t.Error("marshal: expected error, got nil") + } else if strings.Index(err.Error(), "Label") < 0 { + t.Errorf("marshal: bad error type: %v", err) + } + + // A slightly sneaky, yet valid, proto. It encodes the same required field twice, + // so simply counting the required fields is insufficient. + // field 1, encoding 2, value "hi" + buf := []byte("\x0A\x02hi\x0A\x02hi") + err = Unmarshal(buf, pb) + if err == nil { + t.Error("unmarshal: expected error, got nil") + } else if strings.Index(err.Error(), "{Unknown}") < 0 { + t.Errorf("unmarshal: bad error type: %v", err) + } +} + +func TestTypedNilMarshal(t *testing.T) { + // A typed nil should return ErrNil and not crash. + _, err := Marshal((*GoEnum)(nil)) + if err != ErrNil { + t.Errorf("Marshal: got err %v, want ErrNil", err) + } +} + +// A type that implements the Marshaler interface, but is not nillable. +type nonNillableInt uint64 + +func (nni nonNillableInt) Marshal() ([]byte, error) { + return EncodeVarint(uint64(nni)), nil +} + +type NNIMessage struct { + nni nonNillableInt +} + +func (*NNIMessage) Reset() {} +func (*NNIMessage) String() string { return "" } +func (*NNIMessage) ProtoMessage() {} + +// A type that implements the Marshaler interface and is nillable. +type nillableMessage struct { + x uint64 +} + +func (nm *nillableMessage) Marshal() ([]byte, error) { + return EncodeVarint(nm.x), nil +} + +type NMMessage struct { + nm *nillableMessage +} + +func (*NMMessage) Reset() {} +func (*NMMessage) String() string { return "" } +func (*NMMessage) ProtoMessage() {} + +// Verify a type that uses the Marshaler interface, but has a nil pointer. +func TestNilMarshaler(t *testing.T) { + // Try a struct with a Marshaler field that is nil. + // It should be directly marshable. + nmm := new(NMMessage) + if _, err := Marshal(nmm); err != nil { + t.Error("unexpected error marshaling nmm: ", err) + } + + // Try a struct with a Marshaler field that is not nillable. + nnim := new(NNIMessage) + nnim.nni = 7 + var _ Marshaler = nnim.nni // verify it is truly a Marshaler + if _, err := Marshal(nnim); err != nil { + t.Error("unexpected error marshaling nnim: ", err) + } +} + +func TestAllSetDefaults(t *testing.T) { + // Exercise SetDefaults with all scalar field types. + m := &Defaults{ + // NaN != NaN, so override that here. + F_Nan: Float32(1.7), + } + expected := &Defaults{ + F_Bool: Bool(true), + F_Int32: Int32(32), + F_Int64: Int64(64), + F_Fixed32: Uint32(320), + F_Fixed64: Uint64(640), + F_Uint32: Uint32(3200), + F_Uint64: Uint64(6400), + F_Float: Float32(314159), + F_Double: Float64(271828), + F_String: String(`hello, "world!"` + "\n"), + F_Bytes: []byte("Bignose"), + F_Sint32: Int32(-32), + F_Sint64: Int64(-64), + F_Enum: Defaults_GREEN.Enum(), + F_Pinf: Float32(float32(math.Inf(1))), + F_Ninf: Float32(float32(math.Inf(-1))), + F_Nan: Float32(1.7), + } + SetDefaults(m) + if !Equal(m, expected) { + t.Errorf(" got %v\nwant %v", m, expected) + } +} + +func TestSetDefaultsWithSetField(t *testing.T) { + // Check that a set value is not overridden. + m := &Defaults{ + F_Int32: Int32(12), + } + SetDefaults(m) + if v := m.GetF_Int32(); v != 12 { + t.Errorf("m.FInt32 = %v, want 12", v) + } +} + +func TestSetDefaultsWithSubMessage(t *testing.T) { + m := &OtherMessage{ + Key: Int64(123), + Inner: &InnerMessage{ + Host: String("gopher"), + }, + } + expected := &OtherMessage{ + Key: Int64(123), + Inner: &InnerMessage{ + Host: String("gopher"), + Port: Int32(4000), + }, + } + SetDefaults(m) + if !Equal(m, expected) { + t.Errorf("\n got %v\nwant %v", m, expected) + } +} + +func TestSetDefaultsWithRepeatedSubMessage(t *testing.T) { + m := &MyMessage{ + RepInner: []*InnerMessage{{}}, + } + expected := &MyMessage{ + RepInner: []*InnerMessage{{ + Port: Int32(4000), + }}, + } + SetDefaults(m) + if !Equal(m, expected) { + t.Errorf("\n got %v\nwant %v", m, expected) + } +} + +func TestMaximumTagNumber(t *testing.T) { + m := &MaxTag{ + LastField: String("natural goat essence"), + } + buf, err := Marshal(m) + if err != nil { + t.Fatalf("proto.Marshal failed: %v", err) + } + m2 := new(MaxTag) + if err := Unmarshal(buf, m2); err != nil { + t.Fatalf("proto.Unmarshal failed: %v", err) + } + if got, want := m2.GetLastField(), *m.LastField; got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestJSON(t *testing.T) { + m := &MyMessage{ + Count: Int32(4), + Pet: []string{"bunny", "kitty"}, + Inner: &InnerMessage{ + Host: String("cauchy"), + }, + Bikeshed: MyMessage_GREEN.Enum(), + } + const expected = `{"count":4,"pet":["bunny","kitty"],"inner":{"host":"cauchy"},"bikeshed":1}` + + b, err := json.Marshal(m) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + s := string(b) + if s != expected { + t.Errorf("got %s\nwant %s", s, expected) + } + + received := new(MyMessage) + if err := json.Unmarshal(b, received); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + if !Equal(received, m) { + t.Fatalf("got %s, want %s", received, m) + } + + // Test unmarshalling of JSON with symbolic enum name. + const old = `{"count":4,"pet":["bunny","kitty"],"inner":{"host":"cauchy"},"bikeshed":"GREEN"}` + received.Reset() + if err := json.Unmarshal([]byte(old), received); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + if !Equal(received, m) { + t.Fatalf("got %s, want %s", received, m) + } +} + +func TestBadWireType(t *testing.T) { + b := []byte{7<<3 | 6} // field 7, wire type 6 + pb := new(OtherMessage) + if err := Unmarshal(b, pb); err == nil { + t.Errorf("Unmarshal did not fail") + } else if !strings.Contains(err.Error(), "unknown wire type") { + t.Errorf("wrong error: %v", err) + } +} + +func TestBytesWithInvalidLength(t *testing.T) { + // If a byte sequence has an invalid (negative) length, Unmarshal should not panic. + b := []byte{2<<3 | WireBytes, 0xff, 0xff, 0xff, 0xff, 0xff, 0} + Unmarshal(b, new(MyMessage)) +} + +func TestLengthOverflow(t *testing.T) { + // Overflowing a length should not panic. + b := []byte{2<<3 | WireBytes, 1, 1, 3<<3 | WireBytes, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x01} + Unmarshal(b, new(MyMessage)) +} + +func TestVarintOverflow(t *testing.T) { + // Overflowing a 64-bit length should not be allowed. + b := []byte{1<<3 | WireVarint, 0x01, 3<<3 | WireBytes, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01} + if err := Unmarshal(b, new(MyMessage)); err == nil { + t.Fatalf("Overflowed uint64 length without error") + } +} + +func TestUnmarshalFuzz(t *testing.T) { + const N = 1000 + seed := time.Now().UnixNano() + t.Logf("RNG seed is %d", seed) + rng := rand.New(rand.NewSource(seed)) + buf := make([]byte, 20) + for i := 0; i < N; i++ { + for j := range buf { + buf[j] = byte(rng.Intn(256)) + } + fuzzUnmarshal(t, buf) + } +} + +func TestMergeMessages(t *testing.T) { + pb := &MessageList{Message: []*MessageList_Message{{Name: String("x"), Count: Int32(1)}}} + data, err := Marshal(pb) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + + pb1 := new(MessageList) + if err := Unmarshal(data, pb1); err != nil { + t.Fatalf("first Unmarshal: %v", err) + } + if err := Unmarshal(data, pb1); err != nil { + t.Fatalf("second Unmarshal: %v", err) + } + if len(pb1.Message) != 1 { + t.Errorf("two Unmarshals produced %d Messages, want 1", len(pb1.Message)) + } + + pb2 := new(MessageList) + if err := UnmarshalMerge(data, pb2); err != nil { + t.Fatalf("first UnmarshalMerge: %v", err) + } + if err := UnmarshalMerge(data, pb2); err != nil { + t.Fatalf("second UnmarshalMerge: %v", err) + } + if len(pb2.Message) != 2 { + t.Errorf("two UnmarshalMerges produced %d Messages, want 2", len(pb2.Message)) + } +} + +func TestExtensionMarshalOrder(t *testing.T) { + m := &MyMessage{Count: Int(123)} + if err := SetExtension(m, E_Ext_More, &Ext{Data: String("alpha")}); err != nil { + t.Fatalf("SetExtension: %v", err) + } + if err := SetExtension(m, E_Ext_Text, String("aleph")); err != nil { + t.Fatalf("SetExtension: %v", err) + } + if err := SetExtension(m, E_Ext_Number, Int32(1)); err != nil { + t.Fatalf("SetExtension: %v", err) + } + + // Serialize m several times, and check we get the same bytes each time. + var orig []byte + for i := 0; i < 100; i++ { + b, err := Marshal(m) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if i == 0 { + orig = b + continue + } + if !bytes.Equal(b, orig) { + t.Errorf("Bytes differ on attempt #%d", i) + } + } +} + +// Many extensions, because small maps might not iterate differently on each iteration. +var exts = []*ExtensionDesc{ + E_X201, + E_X202, + E_X203, + E_X204, + E_X205, + E_X206, + E_X207, + E_X208, + E_X209, + E_X210, + E_X211, + E_X212, + E_X213, + E_X214, + E_X215, + E_X216, + E_X217, + E_X218, + E_X219, + E_X220, + E_X221, + E_X222, + E_X223, + E_X224, + E_X225, + E_X226, + E_X227, + E_X228, + E_X229, + E_X230, + E_X231, + E_X232, + E_X233, + E_X234, + E_X235, + E_X236, + E_X237, + E_X238, + E_X239, + E_X240, + E_X241, + E_X242, + E_X243, + E_X244, + E_X245, + E_X246, + E_X247, + E_X248, + E_X249, + E_X250, +} + +func TestMessageSetMarshalOrder(t *testing.T) { + m := &MyMessageSet{} + for _, x := range exts { + if err := SetExtension(m, x, &Empty{}); err != nil { + t.Fatalf("SetExtension: %v", err) + } + } + + buf, err := Marshal(m) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + + // Serialize m several times, and check we get the same bytes each time. + for i := 0; i < 10; i++ { + b1, err := Marshal(m) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if !bytes.Equal(b1, buf) { + t.Errorf("Bytes differ on re-Marshal #%d", i) + } + + m2 := &MyMessageSet{} + if err := Unmarshal(buf, m2); err != nil { + t.Errorf("Unmarshal: %v", err) + } + b2, err := Marshal(m2) + if err != nil { + t.Errorf("re-Marshal: %v", err) + } + if !bytes.Equal(b2, buf) { + t.Errorf("Bytes differ on round-trip #%d", i) + } + } +} + +func TestUnmarshalMergesMessages(t *testing.T) { + // If a nested message occurs twice in the input, + // the fields should be merged when decoding. + a := &OtherMessage{ + Key: Int64(123), + Inner: &InnerMessage{ + Host: String("polhode"), + Port: Int32(1234), + }, + } + aData, err := Marshal(a) + if err != nil { + t.Fatalf("Marshal(a): %v", err) + } + b := &OtherMessage{ + Weight: Float32(1.2), + Inner: &InnerMessage{ + Host: String("herpolhode"), + Connected: Bool(true), + }, + } + bData, err := Marshal(b) + if err != nil { + t.Fatalf("Marshal(b): %v", err) + } + want := &OtherMessage{ + Key: Int64(123), + Weight: Float32(1.2), + Inner: &InnerMessage{ + Host: String("herpolhode"), + Port: Int32(1234), + Connected: Bool(true), + }, + } + got := new(OtherMessage) + if err := Unmarshal(append(aData, bData...), got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if !Equal(got, want) { + t.Errorf("\n got %v\nwant %v", got, want) + } +} + +func TestEncodingSizes(t *testing.T) { + tests := []struct { + m Message + n int + }{ + {&Defaults{F_Int32: Int32(math.MaxInt32)}, 6}, + {&Defaults{F_Int32: Int32(math.MinInt32)}, 6}, + {&Defaults{F_Uint32: Uint32(math.MaxUint32)}, 6}, + } + for _, test := range tests { + b, err := Marshal(test.m) + if err != nil { + t.Errorf("Marshal(%v): %v", test.m, err) + continue + } + if len(b) != test.n { + t.Errorf("Marshal(%v) yielded %d bytes, want %d bytes", test.m, len(b), test.n) + } + } +} + +func TestRequiredNotSetError(t *testing.T) { + pb := initGoTest(false) + pb.RequiredField.Label = nil + pb.F_Int32Required = nil + pb.F_Int64Required = nil + + expected := "0807" + // field 1, encoding 0, value 7 + "2206" + "120474797065" + // field 4, encoding 2 (GoTestField) + "5001" + // field 10, encoding 0, value 1 + "6d20000000" + // field 13, encoding 5, value 0x20 + "714000000000000000" + // field 14, encoding 1, value 0x40 + "78a019" + // field 15, encoding 0, value 0xca0 = 3232 + "8001c032" + // field 16, encoding 0, value 0x1940 = 6464 + "8d0100004a45" + // field 17, encoding 5, value 3232.0 + "9101000000000040b940" + // field 18, encoding 1, value 6464.0 + "9a0106" + "737472696e67" + // field 19, encoding 2, string "string" + "b304" + // field 70, encoding 3, start group + "ba0408" + "7265717569726564" + // field 71, encoding 2, string "required" + "b404" + // field 70, encoding 4, end group + "aa0605" + "6279746573" + // field 101, encoding 2, string "bytes" + "b0063f" + // field 102, encoding 0, 0x3f zigzag32 + "b8067f" // field 103, encoding 0, 0x7f zigzag64 + + o := old() + bytes, err := Marshal(pb) + if _, ok := err.(*RequiredNotSetError); !ok { + fmt.Printf("marshal-1 err = %v, want *RequiredNotSetError", err) + o.DebugPrint("", bytes) + t.Fatalf("expected = %s", expected) + } + if strings.Index(err.Error(), "RequiredField.Label") < 0 { + t.Errorf("marshal-1 wrong err msg: %v", err) + } + if !equal(bytes, expected, t) { + o.DebugPrint("neq 1", bytes) + t.Fatalf("expected = %s", expected) + } + + // Now test Unmarshal by recreating the original buffer. + pbd := new(GoTest) + err = Unmarshal(bytes, pbd) + if _, ok := err.(*RequiredNotSetError); !ok { + t.Fatalf("unmarshal err = %v, want *RequiredNotSetError", err) + o.DebugPrint("", bytes) + t.Fatalf("string = %s", expected) + } + if strings.Index(err.Error(), "RequiredField.{Unknown}") < 0 { + t.Errorf("unmarshal wrong err msg: %v", err) + } + bytes, err = Marshal(pbd) + if _, ok := err.(*RequiredNotSetError); !ok { + t.Errorf("marshal-2 err = %v, want *RequiredNotSetError", err) + o.DebugPrint("", bytes) + t.Fatalf("string = %s", expected) + } + if strings.Index(err.Error(), "RequiredField.Label") < 0 { + t.Errorf("marshal-2 wrong err msg: %v", err) + } + if !equal(bytes, expected, t) { + o.DebugPrint("neq 2", bytes) + t.Fatalf("string = %s", expected) + } +} + +func fuzzUnmarshal(t *testing.T, data []byte) { + defer func() { + if e := recover(); e != nil { + t.Errorf("These bytes caused a panic: %+v", data) + t.Logf("Stack:\n%s", debug.Stack()) + t.FailNow() + } + }() + + pb := new(MyMessage) + Unmarshal(data, pb) +} + +// Benchmarks + +func testMsg() *GoTest { + pb := initGoTest(true) + const N = 1000 // Internally the library starts much smaller. + pb.F_Int32Repeated = make([]int32, N) + pb.F_DoubleRepeated = make([]float64, N) + for i := 0; i < N; i++ { + pb.F_Int32Repeated[i] = int32(i) + pb.F_DoubleRepeated[i] = float64(i) + } + return pb +} + +func bytesMsg() *GoTest { + pb := initGoTest(true) + buf := make([]byte, 4000) + for i := range buf { + buf[i] = byte(i) + } + pb.F_BytesDefaulted = buf + return pb +} + +func benchmarkMarshal(b *testing.B, pb Message, marshal func(Message) ([]byte, error)) { + d, _ := marshal(pb) + b.SetBytes(int64(len(d))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + marshal(pb) + } +} + +func benchmarkBufferMarshal(b *testing.B, pb Message) { + p := NewBuffer(nil) + benchmarkMarshal(b, pb, func(pb0 Message) ([]byte, error) { + p.Reset() + err := p.Marshal(pb0) + return p.Bytes(), err + }) +} + +func benchmarkSize(b *testing.B, pb Message) { + benchmarkMarshal(b, pb, func(pb0 Message) ([]byte, error) { + Size(pb) + return nil, nil + }) +} + +func newOf(pb Message) Message { + in := reflect.ValueOf(pb) + if in.IsNil() { + return pb + } + return reflect.New(in.Type().Elem()).Interface().(Message) +} + +func benchmarkUnmarshal(b *testing.B, pb Message, unmarshal func([]byte, Message) error) { + d, _ := Marshal(pb) + b.SetBytes(int64(len(d))) + pbd := newOf(pb) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + unmarshal(d, pbd) + } +} + +func benchmarkBufferUnmarshal(b *testing.B, pb Message) { + p := NewBuffer(nil) + benchmarkUnmarshal(b, pb, func(d []byte, pb0 Message) error { + p.SetBuf(d) + return p.Unmarshal(pb0) + }) +} + +// Benchmark{Marshal,BufferMarshal,Size,Unmarshal,BufferUnmarshal}{,Bytes} + +func BenchmarkMarshal(b *testing.B) { + benchmarkMarshal(b, testMsg(), Marshal) +} + +func BenchmarkBufferMarshal(b *testing.B) { + benchmarkBufferMarshal(b, testMsg()) +} + +func BenchmarkSize(b *testing.B) { + benchmarkSize(b, testMsg()) +} + +func BenchmarkUnmarshal(b *testing.B) { + benchmarkUnmarshal(b, testMsg(), Unmarshal) +} + +func BenchmarkBufferUnmarshal(b *testing.B) { + benchmarkBufferUnmarshal(b, testMsg()) +} + +func BenchmarkMarshalBytes(b *testing.B) { + benchmarkMarshal(b, bytesMsg(), Marshal) +} + +func BenchmarkBufferMarshalBytes(b *testing.B) { + benchmarkBufferMarshal(b, bytesMsg()) +} + +func BenchmarkSizeBytes(b *testing.B) { + benchmarkSize(b, bytesMsg()) +} + +func BenchmarkUnmarshalBytes(b *testing.B) { + benchmarkUnmarshal(b, bytesMsg(), Unmarshal) +} + +func BenchmarkBufferUnmarshalBytes(b *testing.B) { + benchmarkBufferUnmarshal(b, bytesMsg()) +} + +func BenchmarkUnmarshalUnrecognizedFields(b *testing.B) { + b.StopTimer() + pb := initGoTestField() + skip := &GoSkipTest{ + SkipInt32: Int32(32), + SkipFixed32: Uint32(3232), + SkipFixed64: Uint64(6464), + SkipString: String("skipper"), + Skipgroup: &GoSkipTest_SkipGroup{ + GroupInt32: Int32(75), + GroupString: String("wxyz"), + }, + } + + pbd := new(GoTestField) + p := NewBuffer(nil) + p.Marshal(pb) + p.Marshal(skip) + p2 := NewBuffer(nil) + + b.StartTimer() + for i := 0; i < b.N; i++ { + p2.SetBuf(p.Bytes()) + p2.Unmarshal(pbd) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone.go new file mode 100644 index 00000000000..b4b5c058874 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone.go @@ -0,0 +1,174 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Protocol buffer deep copy. +// TODO: MessageSet and RawMessage. + +package proto + +import ( + "log" + "reflect" + "strings" +) + +// Clone returns a deep copy of a protocol buffer. +func Clone(pb Message) Message { + in := reflect.ValueOf(pb) + if in.IsNil() { + return pb + } + + out := reflect.New(in.Type().Elem()) + // out is empty so a merge is a deep copy. + mergeStruct(out.Elem(), in.Elem()) + return out.Interface().(Message) +} + +// Merge merges src into dst. +// Required and optional fields that are set in src will be set to that value in dst. +// Elements of repeated fields will be appended. +// Merge panics if src and dst are not the same type, or if dst is nil. +func Merge(dst, src Message) { + in := reflect.ValueOf(src) + out := reflect.ValueOf(dst) + if out.IsNil() { + panic("proto: nil destination") + } + if in.Type() != out.Type() { + // Explicit test prior to mergeStruct so that mistyped nils will fail + panic("proto: type mismatch") + } + if in.IsNil() { + // Merging nil into non-nil is a quiet no-op + return + } + mergeStruct(out.Elem(), in.Elem()) +} + +func mergeStruct(out, in reflect.Value) { + for i := 0; i < in.NumField(); i++ { + f := in.Type().Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + mergeAny(out.Field(i), in.Field(i)) + } + + if emIn, ok := in.Addr().Interface().(extensionsMap); ok { + emOut := out.Addr().Interface().(extensionsMap) + mergeExtension(emOut.ExtensionMap(), emIn.ExtensionMap()) + } else if emIn, ok := in.Addr().Interface().(extensionsBytes); ok { + emOut := out.Addr().Interface().(extensionsBytes) + bIn := emIn.GetExtensions() + bOut := emOut.GetExtensions() + *bOut = append(*bOut, *bIn...) + } + + uf := in.FieldByName("XXX_unrecognized") + if !uf.IsValid() { + return + } + uin := uf.Bytes() + if len(uin) > 0 { + out.FieldByName("XXX_unrecognized").SetBytes(append([]byte(nil), uin...)) + } +} + +func mergeAny(out, in reflect.Value) { + if in.Type() == protoMessageType { + if !in.IsNil() { + if out.IsNil() { + out.Set(reflect.ValueOf(Clone(in.Interface().(Message)))) + } else { + Merge(out.Interface().(Message), in.Interface().(Message)) + } + } + return + } + switch in.Kind() { + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, + reflect.String, reflect.Uint32, reflect.Uint64: + out.Set(in) + case reflect.Ptr: + if in.IsNil() { + return + } + if out.IsNil() { + out.Set(reflect.New(in.Elem().Type())) + } + mergeAny(out.Elem(), in.Elem()) + case reflect.Slice: + if in.IsNil() { + return + } + n := in.Len() + if out.IsNil() { + out.Set(reflect.MakeSlice(in.Type(), 0, n)) + } + switch in.Type().Elem().Kind() { + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, + reflect.String, reflect.Uint32, reflect.Uint64: + out.Set(reflect.AppendSlice(out, in)) + case reflect.Uint8: + // []byte is a scalar bytes field. + out.Set(in) + default: + for i := 0; i < n; i++ { + x := reflect.Indirect(reflect.New(in.Type().Elem())) + mergeAny(x, in.Index(i)) + out.Set(reflect.Append(out, x)) + } + } + case reflect.Struct: + mergeStruct(out, in) + default: + // unknown type, so not a protocol buffer + log.Printf("proto: don't know how to copy %v", in) + } +} + +func mergeExtension(out, in map[int32]Extension) { + for extNum, eIn := range in { + eOut := Extension{desc: eIn.desc} + if eIn.value != nil { + v := reflect.New(reflect.TypeOf(eIn.value)).Elem() + mergeAny(v, reflect.ValueOf(eIn.value)) + eOut.value = v.Interface() + } + if eIn.enc != nil { + eOut.enc = make([]byte, len(eIn.enc)) + copy(eOut.enc, eIn.enc) + } + + out[extNum] = eOut + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone_test.go new file mode 100644 index 00000000000..652fbb78fd5 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/clone_test.go @@ -0,0 +1,186 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "testing" + + "code.google.com/p/gogoprotobuf/proto" + + pb "./testdata" +) + +var cloneTestMessage = &pb.MyMessage{ + Count: proto.Int32(42), + Name: proto.String("Dave"), + Pet: []string{"bunny", "kitty", "horsey"}, + Inner: &pb.InnerMessage{ + Host: proto.String("niles"), + Port: proto.Int32(9099), + Connected: proto.Bool(true), + }, + Others: []*pb.OtherMessage{ + { + Value: []byte("some bytes"), + }, + }, + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: proto.Int32(6), + }, + RepBytes: [][]byte{[]byte("sham"), []byte("wow")}, +} + +func init() { + ext := &pb.Ext{ + Data: proto.String("extension"), + } + if err := proto.SetExtension(cloneTestMessage, pb.E_Ext_More, ext); err != nil { + panic("SetExtension: " + err.Error()) + } +} + +func TestClone(t *testing.T) { + m := proto.Clone(cloneTestMessage).(*pb.MyMessage) + if !proto.Equal(m, cloneTestMessage) { + t.Errorf("Clone(%v) = %v", cloneTestMessage, m) + } + + // Verify it was a deep copy. + *m.Inner.Port++ + if proto.Equal(m, cloneTestMessage) { + t.Error("Mutating clone changed the original") + } +} + +func TestCloneNil(t *testing.T) { + var m *pb.MyMessage + if c := proto.Clone(m); !proto.Equal(m, c) { + t.Errorf("Clone(%v) = %v", m, c) + } +} + +var mergeTests = []struct { + src, dst, want proto.Message +}{ + { + src: &pb.MyMessage{ + Count: proto.Int32(42), + }, + dst: &pb.MyMessage{ + Name: proto.String("Dave"), + }, + want: &pb.MyMessage{ + Count: proto.Int32(42), + Name: proto.String("Dave"), + }, + }, + { + src: &pb.MyMessage{ + Inner: &pb.InnerMessage{ + Host: proto.String("hey"), + Connected: proto.Bool(true), + }, + Pet: []string{"horsey"}, + Others: []*pb.OtherMessage{ + { + Value: []byte("some bytes"), + }, + }, + }, + dst: &pb.MyMessage{ + Inner: &pb.InnerMessage{ + Host: proto.String("niles"), + Port: proto.Int32(9099), + }, + Pet: []string{"bunny", "kitty"}, + Others: []*pb.OtherMessage{ + { + Key: proto.Int64(31415926535), + }, + { + // Explicitly test a src=nil field + Inner: nil, + }, + }, + }, + want: &pb.MyMessage{ + Inner: &pb.InnerMessage{ + Host: proto.String("hey"), + Connected: proto.Bool(true), + Port: proto.Int32(9099), + }, + Pet: []string{"bunny", "kitty", "horsey"}, + Others: []*pb.OtherMessage{ + { + Key: proto.Int64(31415926535), + }, + {}, + { + Value: []byte("some bytes"), + }, + }, + }, + }, + { + src: &pb.MyMessage{ + RepBytes: [][]byte{[]byte("wow")}, + }, + dst: &pb.MyMessage{ + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: proto.Int32(6), + }, + RepBytes: [][]byte{[]byte("sham")}, + }, + want: &pb.MyMessage{ + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: proto.Int32(6), + }, + RepBytes: [][]byte{[]byte("sham"), []byte("wow")}, + }, + }, + // Check that a scalar bytes field replaces rather than appends. + { + src: &pb.OtherMessage{Value: []byte("foo")}, + dst: &pb.OtherMessage{Value: []byte("bar")}, + want: &pb.OtherMessage{Value: []byte("foo")}, + }, +} + +func TestMerge(t *testing.T) { + for _, m := range mergeTests { + got := proto.Clone(m.dst) + proto.Merge(got, m.src) + if !proto.Equal(got, m.want) { + t.Errorf("Merge(%v, %v)\n got %v\nwant %v\n", m.dst, m.src, got, m.want) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode.go new file mode 100644 index 00000000000..9714104a105 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode.go @@ -0,0 +1,726 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for decoding protocol buffer data to construct in-memory representations. + */ + +import ( + "errors" + "fmt" + "io" + "os" + "reflect" +) + +// errOverflow is returned when an integer is too large to be represented. +var errOverflow = errors.New("proto: integer overflow") + +// The fundamental decoders that interpret bytes on the wire. +// Those that take integer types all return uint64 and are +// therefore of type valueDecoder. + +// DecodeVarint reads a varint-encoded integer from the slice. +// It returns the integer and the number of bytes consumed, or +// zero if there is not enough. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func DecodeVarint(buf []byte) (x uint64, n int) { + // x, n already 0 + for shift := uint(0); shift < 64; shift += 7 { + if n >= len(buf) { + return 0, 0 + } + b := uint64(buf[n]) + n++ + x |= (b & 0x7F) << shift + if (b & 0x80) == 0 { + return x, n + } + } + + // The number is too large to represent in a 64-bit value. + return 0, 0 +} + +// DecodeVarint reads a varint-encoded integer from the Buffer. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func (p *Buffer) DecodeVarint() (x uint64, err error) { + // x, err already 0 + + i := p.index + l := len(p.buf) + + for shift := uint(0); shift < 64; shift += 7 { + if i >= l { + err = io.ErrUnexpectedEOF + return + } + b := p.buf[i] + i++ + x |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + p.index = i + return + } + } + + // The number is too large to represent in a 64-bit value. + err = errOverflow + return +} + +// DecodeFixed64 reads a 64-bit integer from the Buffer. +// This is the format for the +// fixed64, sfixed64, and double protocol buffer types. +func (p *Buffer) DecodeFixed64() (x uint64, err error) { + // x, err already 0 + i := p.index + 8 + if i < 0 || i > len(p.buf) { + err = io.ErrUnexpectedEOF + return + } + p.index = i + + x = uint64(p.buf[i-8]) + x |= uint64(p.buf[i-7]) << 8 + x |= uint64(p.buf[i-6]) << 16 + x |= uint64(p.buf[i-5]) << 24 + x |= uint64(p.buf[i-4]) << 32 + x |= uint64(p.buf[i-3]) << 40 + x |= uint64(p.buf[i-2]) << 48 + x |= uint64(p.buf[i-1]) << 56 + return +} + +// DecodeFixed32 reads a 32-bit integer from the Buffer. +// This is the format for the +// fixed32, sfixed32, and float protocol buffer types. +func (p *Buffer) DecodeFixed32() (x uint64, err error) { + // x, err already 0 + i := p.index + 4 + if i < 0 || i > len(p.buf) { + err = io.ErrUnexpectedEOF + return + } + p.index = i + + x = uint64(p.buf[i-4]) + x |= uint64(p.buf[i-3]) << 8 + x |= uint64(p.buf[i-2]) << 16 + x |= uint64(p.buf[i-1]) << 24 + return +} + +// DecodeZigzag64 reads a zigzag-encoded 64-bit integer +// from the Buffer. +// This is the format used for the sint64 protocol buffer type. +func (p *Buffer) DecodeZigzag64() (x uint64, err error) { + x, err = p.DecodeVarint() + if err != nil { + return + } + x = (x >> 1) ^ uint64((int64(x&1)<<63)>>63) + return +} + +// DecodeZigzag32 reads a zigzag-encoded 32-bit integer +// from the Buffer. +// This is the format used for the sint32 protocol buffer type. +func (p *Buffer) DecodeZigzag32() (x uint64, err error) { + x, err = p.DecodeVarint() + if err != nil { + return + } + x = uint64((uint32(x) >> 1) ^ uint32((int32(x&1)<<31)>>31)) + return +} + +// These are not ValueDecoders: they produce an array of bytes or a string. +// bytes, embedded messages + +// DecodeRawBytes reads a count-delimited byte buffer from the Buffer. +// This is the format used for the bytes protocol buffer +// type and for embedded messages. +func (p *Buffer) DecodeRawBytes(alloc bool) (buf []byte, err error) { + n, err := p.DecodeVarint() + if err != nil { + return + } + + nb := int(n) + if nb < 0 { + return nil, fmt.Errorf("proto: bad byte length %d", nb) + } + end := p.index + nb + if end < p.index || end > len(p.buf) { + return nil, io.ErrUnexpectedEOF + } + + if !alloc { + // todo: check if can get more uses of alloc=false + buf = p.buf[p.index:end] + p.index += nb + return + } + + buf = make([]byte, nb) + copy(buf, p.buf[p.index:]) + p.index += nb + return +} + +// DecodeStringBytes reads an encoded string from the Buffer. +// This is the format used for the proto2 string type. +func (p *Buffer) DecodeStringBytes() (s string, err error) { + buf, err := p.DecodeRawBytes(false) + if err != nil { + return + } + return string(buf), nil +} + +// Skip the next item in the buffer. Its wire type is decoded and presented as an argument. +// If the protocol buffer has extensions, and the field matches, add it as an extension. +// Otherwise, if the XXX_unrecognized field exists, append the skipped data there. +func (o *Buffer) skipAndSave(t reflect.Type, tag, wire int, base structPointer, unrecField field) error { + oi := o.index + + err := o.skip(t, tag, wire) + if err != nil { + return err + } + + if !unrecField.IsValid() { + return nil + } + + ptr := structPointer_Bytes(base, unrecField) + + // Add the skipped field to struct field + obuf := o.buf + + o.buf = *ptr + o.EncodeVarint(uint64(tag<<3 | wire)) + *ptr = append(o.buf, obuf[oi:o.index]...) + + o.buf = obuf + + return nil +} + +// Skip the next item in the buffer. Its wire type is decoded and presented as an argument. +func (o *Buffer) skip(t reflect.Type, tag, wire int) error { + + var u uint64 + var err error + + switch wire { + case WireVarint: + _, err = o.DecodeVarint() + case WireFixed64: + _, err = o.DecodeFixed64() + case WireBytes: + _, err = o.DecodeRawBytes(false) + case WireFixed32: + _, err = o.DecodeFixed32() + case WireStartGroup: + for { + u, err = o.DecodeVarint() + if err != nil { + break + } + fwire := int(u & 0x7) + if fwire == WireEndGroup { + break + } + ftag := int(u >> 3) + err = o.skip(t, ftag, fwire) + if err != nil { + break + } + } + default: + err = fmt.Errorf("proto: can't skip unknown wire type %d for %s", wire, t) + } + return err +} + +// Unmarshaler is the interface representing objects that can +// unmarshal themselves. The method should reset the receiver before +// decoding starts. The argument points to data that may be +// overwritten, so implementations should not keep references to the +// buffer. +type Unmarshaler interface { + Unmarshal([]byte) error +} + +// Unmarshal parses the protocol buffer representation in buf and places the +// decoded result in pb. If the struct underlying pb does not match +// the data in buf, the results can be unpredictable. +// +// Unmarshal resets pb before starting to unmarshal, so any +// existing data in pb is always removed. Use UnmarshalMerge +// to preserve and append to existing data. +func Unmarshal(buf []byte, pb Message) error { + pb.Reset() + return UnmarshalMerge(buf, pb) +} + +// UnmarshalMerge parses the protocol buffer representation in buf and +// writes the decoded result to pb. If the struct underlying pb does not match +// the data in buf, the results can be unpredictable. +// +// UnmarshalMerge merges into existing data in pb. +// Most code should use Unmarshal instead. +func UnmarshalMerge(buf []byte, pb Message) error { + // If the object can unmarshal itself, let it. + if u, ok := pb.(Unmarshaler); ok { + return u.Unmarshal(buf) + } + return NewBuffer(buf).Unmarshal(pb) +} + +// Unmarshal parses the protocol buffer representation in the +// Buffer and places the decoded result in pb. If the struct +// underlying pb does not match the data in the buffer, the results can be +// unpredictable. +func (p *Buffer) Unmarshal(pb Message) error { + // If the object can unmarshal itself, let it. + if u, ok := pb.(Unmarshaler); ok { + err := u.Unmarshal(p.buf[p.index:]) + p.index = len(p.buf) + return err + } + + typ, base, err := getbase(pb) + if err != nil { + return err + } + + err = p.unmarshalType(typ.Elem(), GetProperties(typ.Elem()), false, base) + + if collectStats { + stats.Decode++ + } + + return err +} + +// unmarshalType does the work of unmarshaling a structure. +func (o *Buffer) unmarshalType(st reflect.Type, prop *StructProperties, is_group bool, base structPointer) error { + var state errorState + required, reqFields := prop.reqCount, uint64(0) + + var err error + for err == nil && o.index < len(o.buf) { + oi := o.index + var u uint64 + u, err = o.DecodeVarint() + if err != nil { + break + } + wire := int(u & 0x7) + if wire == WireEndGroup { + if is_group { + return nil // input is satisfied + } + return fmt.Errorf("proto: %s: wiretype end group for non-group", st) + } + tag := int(u >> 3) + if tag <= 0 { + return fmt.Errorf("proto: %s: illegal tag %d", st, tag) + } + fieldnum, ok := prop.decoderTags.get(tag) + if !ok { + // Maybe it's an extension? + if prop.extendable { + if e := structPointer_Interface(base, st).(extendableProto); isExtensionField(e, int32(tag)) { + if err = o.skip(st, tag, wire); err == nil { + if ee, ok := e.(extensionsMap); ok { + ext := ee.ExtensionMap()[int32(tag)] // may be missing + ext.enc = append(ext.enc, o.buf[oi:o.index]...) + ee.ExtensionMap()[int32(tag)] = ext + } else if ee, ok := e.(extensionsBytes); ok { + ext := ee.GetExtensions() + *ext = append(*ext, o.buf[oi:o.index]...) + } + } + continue + } + } + err = o.skipAndSave(st, tag, wire, base, prop.unrecField) + continue + } + p := prop.Prop[fieldnum] + + if p.dec == nil { + fmt.Fprintf(os.Stderr, "proto: no protobuf decoder for %s.%s\n", st, st.Field(fieldnum).Name) + continue + } + dec := p.dec + if wire != WireStartGroup && wire != p.WireType { + if wire == WireBytes && p.packedDec != nil { + // a packable field + dec = p.packedDec + } else { + err = fmt.Errorf("proto: bad wiretype for field %s.%s: got wiretype %d, want %d", st, st.Field(fieldnum).Name, wire, p.WireType) + continue + } + } + decErr := dec(o, p, base) + if decErr != nil && !state.shouldContinue(decErr, p) { + err = decErr + } + if err == nil && p.Required { + // Successfully decoded a required field. + if tag <= 64 { + // use bitmap for fields 1-64 to catch field reuse. + var mask uint64 = 1 << uint64(tag-1) + if reqFields&mask == 0 { + // new required field + reqFields |= mask + required-- + } + } else { + // This is imprecise. It can be fooled by a required field + // with a tag > 64 that is encoded twice; that's very rare. + // A fully correct implementation would require allocating + // a data structure, which we would like to avoid. + required-- + } + } + } + if err == nil { + if is_group { + return io.ErrUnexpectedEOF + } + if state.err != nil { + return state.err + } + if required > 0 { + // Not enough information to determine the exact field. If we use extra + // CPU, we could determine the field only if the missing required field + // has a tag <= 64 and we check reqFields. + return &RequiredNotSetError{"{Unknown}"} + } + } + return err +} + +// Individual type decoders +// For each, +// u is the decoded value, +// v is a pointer to the field (pointer) in the struct + +// Sizes of the pools to allocate inside the Buffer. +// The goal is modest amortization and allocation +// on at least 16-byte boundaries. +const ( + boolPoolSize = 16 + uint32PoolSize = 8 + uint64PoolSize = 4 +) + +// Decode a bool. +func (o *Buffer) dec_bool(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + if len(o.bools) == 0 { + o.bools = make([]bool, boolPoolSize) + } + o.bools[0] = u != 0 + *structPointer_Bool(base, p.field) = &o.bools[0] + o.bools = o.bools[1:] + return nil +} + +// Decode an int32. +func (o *Buffer) dec_int32(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + word32_Set(structPointer_Word32(base, p.field), o, uint32(u)) + return nil +} + +// Decode an int64. +func (o *Buffer) dec_int64(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + word64_Set(structPointer_Word64(base, p.field), o, u) + return nil +} + +// Decode a string. +func (o *Buffer) dec_string(p *Properties, base structPointer) error { + s, err := o.DecodeStringBytes() + if err != nil { + return err + } + sp := new(string) + *sp = s + *structPointer_String(base, p.field) = sp + return nil +} + +// Decode a slice of bytes ([]byte). +func (o *Buffer) dec_slice_byte(p *Properties, base structPointer) error { + b, err := o.DecodeRawBytes(true) + if err != nil { + return err + } + *structPointer_Bytes(base, p.field) = b + return nil +} + +// Decode a slice of bools ([]bool). +func (o *Buffer) dec_slice_bool(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + v := structPointer_BoolSlice(base, p.field) + *v = append(*v, u != 0) + return nil +} + +// Decode a slice of bools ([]bool) in packed format. +func (o *Buffer) dec_slice_packed_bool(p *Properties, base structPointer) error { + v := structPointer_BoolSlice(base, p.field) + + nn, err := o.DecodeVarint() + if err != nil { + return err + } + nb := int(nn) // number of bytes of encoded bools + + y := *v + for i := 0; i < nb; i++ { + u, err := p.valDec(o) + if err != nil { + return err + } + y = append(y, u != 0) + } + + *v = y + return nil +} + +// Decode a slice of int32s ([]int32). +func (o *Buffer) dec_slice_int32(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + structPointer_Word32Slice(base, p.field).Append(uint32(u)) + return nil +} + +// Decode a slice of int32s ([]int32) in packed format. +func (o *Buffer) dec_slice_packed_int32(p *Properties, base structPointer) error { + v := structPointer_Word32Slice(base, p.field) + + nn, err := o.DecodeVarint() + if err != nil { + return err + } + nb := int(nn) // number of bytes of encoded int32s + + fin := o.index + nb + if fin < o.index { + return errOverflow + } + for o.index < fin { + u, err := p.valDec(o) + if err != nil { + return err + } + v.Append(uint32(u)) + } + return nil +} + +// Decode a slice of int64s ([]int64). +func (o *Buffer) dec_slice_int64(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + + structPointer_Word64Slice(base, p.field).Append(u) + return nil +} + +// Decode a slice of int64s ([]int64) in packed format. +func (o *Buffer) dec_slice_packed_int64(p *Properties, base structPointer) error { + v := structPointer_Word64Slice(base, p.field) + + nn, err := o.DecodeVarint() + if err != nil { + return err + } + nb := int(nn) // number of bytes of encoded int64s + + fin := o.index + nb + if fin < o.index { + return errOverflow + } + for o.index < fin { + u, err := p.valDec(o) + if err != nil { + return err + } + v.Append(u) + } + return nil +} + +// Decode a slice of strings ([]string). +func (o *Buffer) dec_slice_string(p *Properties, base structPointer) error { + s, err := o.DecodeStringBytes() + if err != nil { + return err + } + v := structPointer_StringSlice(base, p.field) + *v = append(*v, s) + return nil +} + +// Decode a slice of slice of bytes ([][]byte). +func (o *Buffer) dec_slice_slice_byte(p *Properties, base structPointer) error { + b, err := o.DecodeRawBytes(true) + if err != nil { + return err + } + v := structPointer_BytesSlice(base, p.field) + *v = append(*v, b) + return nil +} + +// Decode a group. +func (o *Buffer) dec_struct_group(p *Properties, base structPointer) error { + bas := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(bas) { + // allocate new nested message + bas = toStructPointer(reflect.New(p.stype)) + structPointer_SetStructPointer(base, p.field, bas) + } + return o.unmarshalType(p.stype, p.sprop, true, bas) +} + +// Decode an embedded message. +func (o *Buffer) dec_struct_message(p *Properties, base structPointer) (err error) { + raw, e := o.DecodeRawBytes(false) + if e != nil { + return e + } + + bas := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(bas) { + // allocate new nested message + bas = toStructPointer(reflect.New(p.stype)) + structPointer_SetStructPointer(base, p.field, bas) + } + + // If the object can unmarshal itself, let it. + if p.isUnmarshaler { + iv := structPointer_Interface(bas, p.stype) + return iv.(Unmarshaler).Unmarshal(raw) + } + + obuf := o.buf + oi := o.index + o.buf = raw + o.index = 0 + + err = o.unmarshalType(p.stype, p.sprop, false, bas) + o.buf = obuf + o.index = oi + + return err +} + +// Decode a slice of embedded messages. +func (o *Buffer) dec_slice_struct_message(p *Properties, base structPointer) error { + return o.dec_slice_struct(p, false, base) +} + +// Decode a slice of embedded groups. +func (o *Buffer) dec_slice_struct_group(p *Properties, base structPointer) error { + return o.dec_slice_struct(p, true, base) +} + +// Decode a slice of structs ([]*struct). +func (o *Buffer) dec_slice_struct(p *Properties, is_group bool, base structPointer) error { + v := reflect.New(p.stype) + bas := toStructPointer(v) + structPointer_StructPointerSlice(base, p.field).Append(bas) + + if is_group { + err := o.unmarshalType(p.stype, p.sprop, is_group, bas) + return err + } + + raw, err := o.DecodeRawBytes(false) + if err != nil { + return err + } + + // If the object can unmarshal itself, let it. + if p.isUnmarshaler { + iv := v.Interface() + return iv.(Unmarshaler).Unmarshal(raw) + } + + obuf := o.buf + oi := o.index + o.buf = raw + o.index = 0 + + err = o.unmarshalType(p.stype, p.sprop, is_group, bas) + + o.buf = obuf + o.index = oi + + return err +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode_gogo.go new file mode 100644 index 00000000000..1161dbd5493 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/decode_gogo.go @@ -0,0 +1,220 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "reflect" +) + +// Decode a reference to a bool pointer. +func (o *Buffer) dec_ref_bool(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + if len(o.bools) == 0 { + o.bools = make([]bool, boolPoolSize) + } + o.bools[0] = u != 0 + *structPointer_RefBool(base, p.field) = o.bools[0] + o.bools = o.bools[1:] + return nil +} + +// Decode a reference to an int32 pointer. +func (o *Buffer) dec_ref_int32(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + refWord32_Set(structPointer_RefWord32(base, p.field), o, uint32(u)) + return nil +} + +// Decode a reference to an int64 pointer. +func (o *Buffer) dec_ref_int64(p *Properties, base structPointer) error { + u, err := p.valDec(o) + if err != nil { + return err + } + refWord64_Set(structPointer_RefWord64(base, p.field), o, u) + return nil +} + +// Decode a reference to a string pointer. +func (o *Buffer) dec_ref_string(p *Properties, base structPointer) error { + s, err := o.DecodeStringBytes() + if err != nil { + return err + } + *structPointer_RefString(base, p.field) = s + return nil +} + +// Decode a reference to a struct pointer. +func (o *Buffer) dec_ref_struct_message(p *Properties, base structPointer) (err error) { + raw, e := o.DecodeRawBytes(false) + if e != nil { + return e + } + + // If the object can unmarshal itself, let it. + if p.isUnmarshaler { + panic("not supported, since this is a pointer receiver") + } + + obuf := o.buf + oi := o.index + o.buf = raw + o.index = 0 + + bas := structPointer_FieldPointer(base, p.field) + + err = o.unmarshalType(p.stype, p.sprop, false, bas) + o.buf = obuf + o.index = oi + + return err +} + +// Decode a slice of references to struct pointers ([]struct). +func (o *Buffer) dec_slice_ref_struct(p *Properties, is_group bool, base structPointer) error { + newBas := appendStructPointer(base, p.field, p.sstype) + + if is_group { + panic("not supported, maybe in future, if requested.") + } + + raw, err := o.DecodeRawBytes(false) + if err != nil { + return err + } + + // If the object can unmarshal itself, let it. + if p.isUnmarshaler { + panic("not supported, since this is not a pointer receiver.") + } + + obuf := o.buf + oi := o.index + o.buf = raw + o.index = 0 + + err = o.unmarshalType(p.stype, p.sprop, is_group, newBas) + + o.buf = obuf + o.index = oi + + return err +} + +// Decode a slice of references to struct pointers. +func (o *Buffer) dec_slice_ref_struct_message(p *Properties, base structPointer) error { + return o.dec_slice_ref_struct(p, false, base) +} + +func setPtrCustomType(base structPointer, f field, v interface{}) { + if v == nil { + return + } + structPointer_SetStructPointer(base, f, structPointer(reflect.ValueOf(v).Pointer())) +} + +func setCustomType(base structPointer, f field, value interface{}) { + if value == nil { + return + } + v := reflect.ValueOf(value).Elem() + t := reflect.TypeOf(value).Elem() + kind := t.Kind() + switch kind { + case reflect.Slice: + slice := reflect.MakeSlice(t, v.Len(), v.Cap()) + reflect.Copy(slice, v) + oldHeader := structPointer_GetSliceHeader(base, f) + oldHeader.Data = slice.Pointer() + oldHeader.Len = v.Len() + oldHeader.Cap = v.Cap() + default: + l := 1 + size := reflect.TypeOf(value).Elem().Size() + if kind == reflect.Array { + l = reflect.TypeOf(value).Elem().Len() + size = reflect.TypeOf(value).Size() + } + total := int(size) * l + structPointer_Copy(toStructPointer(reflect.ValueOf(value)), structPointer_Add(base, f), total) + } +} + +func (o *Buffer) dec_custom_bytes(p *Properties, base structPointer) error { + b, err := o.DecodeRawBytes(true) + if err != nil { + return err + } + i := reflect.New(p.ctype.Elem()).Interface() + custom := (i).(Unmarshaler) + if err := custom.Unmarshal(b); err != nil { + return err + } + setPtrCustomType(base, p.field, custom) + return nil +} + +func (o *Buffer) dec_custom_ref_bytes(p *Properties, base structPointer) error { + b, err := o.DecodeRawBytes(true) + if err != nil { + return err + } + i := reflect.New(p.ctype).Interface() + custom := (i).(Unmarshaler) + if err := custom.Unmarshal(b); err != nil { + return err + } + if custom != nil { + setCustomType(base, p.field, custom) + } + return nil +} + +// Decode a slice of bytes ([]byte) into a slice of custom types. +func (o *Buffer) dec_custom_slice_bytes(p *Properties, base structPointer) error { + b, err := o.DecodeRawBytes(true) + if err != nil { + return err + } + i := reflect.New(p.ctype.Elem()).Interface() + custom := (i).(Unmarshaler) + if err := custom.Unmarshal(b); err != nil { + return err + } + newBas := appendStructPointer(base, p.field, p.ctype) + + setCustomType(newBas, 0, custom) + + return nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode.go new file mode 100644 index 00000000000..2d3e03f6920 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode.go @@ -0,0 +1,961 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for encoding data into the wire format for protocol buffers. + */ + +import ( + "errors" + "fmt" + "reflect" + "sort" +) + +// RequiredNotSetError is the error returned if Marshal is called with +// a protocol buffer struct whose required fields have not +// all been initialized. It is also the error returned if Unmarshal is +// called with an encoded protocol buffer that does not include all the +// required fields. +// +// When printed, RequiredNotSetError reports the first unset required field in a +// message. If the field cannot be precisely determined, it is reported as +// "{Unknown}". +type RequiredNotSetError struct { + field string +} + +func (e *RequiredNotSetError) Error() string { + return fmt.Sprintf("proto: required field %q not set", e.field) +} + +var ( + // ErrRepeatedHasNil is the error returned if Marshal is called with + // a struct with a repeated field containing a nil element. + ErrRepeatedHasNil = errors.New("proto: repeated field has nil element") + + // ErrNil is the error returned if Marshal is called with nil. + ErrNil = errors.New("proto: Marshal called with nil") +) + +// The fundamental encoders that put bytes on the wire. +// Those that take integer types all accept uint64 and are +// therefore of type valueEncoder. + +const maxVarintBytes = 10 // maximum length of a varint + +// EncodeVarint returns the varint encoding of x. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +// Not used by the package itself, but helpful to clients +// wishing to use the same encoding. +func EncodeVarint(x uint64) []byte { + var buf [maxVarintBytes]byte + var n int + for n = 0; x > 127; n++ { + buf[n] = 0x80 | uint8(x&0x7F) + x >>= 7 + } + buf[n] = uint8(x) + n++ + return buf[0:n] +} + +// EncodeVarint writes a varint-encoded integer to the Buffer. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func (p *Buffer) EncodeVarint(x uint64) error { + for x >= 1<<7 { + p.buf = append(p.buf, uint8(x&0x7f|0x80)) + x >>= 7 + } + p.buf = append(p.buf, uint8(x)) + return nil +} + +func sizeVarint(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} + +// EncodeFixed64 writes a 64-bit integer to the Buffer. +// This is the format for the +// fixed64, sfixed64, and double protocol buffer types. +func (p *Buffer) EncodeFixed64(x uint64) error { + p.buf = append(p.buf, + uint8(x), + uint8(x>>8), + uint8(x>>16), + uint8(x>>24), + uint8(x>>32), + uint8(x>>40), + uint8(x>>48), + uint8(x>>56)) + return nil +} + +func sizeFixed64(x uint64) int { + return 8 +} + +// EncodeFixed32 writes a 32-bit integer to the Buffer. +// This is the format for the +// fixed32, sfixed32, and float protocol buffer types. +func (p *Buffer) EncodeFixed32(x uint64) error { + p.buf = append(p.buf, + uint8(x), + uint8(x>>8), + uint8(x>>16), + uint8(x>>24)) + return nil +} + +func sizeFixed32(x uint64) int { + return 4 +} + +// EncodeZigzag64 writes a zigzag-encoded 64-bit integer +// to the Buffer. +// This is the format used for the sint64 protocol buffer type. +func (p *Buffer) EncodeZigzag64(x uint64) error { + // use signed number to get arithmetic right shift. + return p.EncodeVarint(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} + +func sizeZigzag64(x uint64) int { + return sizeVarint(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} + +// EncodeZigzag32 writes a zigzag-encoded 32-bit integer +// to the Buffer. +// This is the format used for the sint32 protocol buffer type. +func (p *Buffer) EncodeZigzag32(x uint64) error { + // use signed number to get arithmetic right shift. + return p.EncodeVarint(uint64((uint32(x) << 1) ^ uint32((int32(x) >> 31)))) +} + +func sizeZigzag32(x uint64) int { + return sizeVarint(uint64((uint32(x) << 1) ^ uint32((int32(x) >> 31)))) +} + +// EncodeRawBytes writes a count-delimited byte buffer to the Buffer. +// This is the format used for the bytes protocol buffer +// type and for embedded messages. +func (p *Buffer) EncodeRawBytes(b []byte) error { + p.EncodeVarint(uint64(len(b))) + p.buf = append(p.buf, b...) + return nil +} + +func sizeRawBytes(b []byte) int { + return sizeVarint(uint64(len(b))) + + len(b) +} + +// EncodeStringBytes writes an encoded string to the Buffer. +// This is the format used for the proto2 string type. +func (p *Buffer) EncodeStringBytes(s string) error { + p.EncodeVarint(uint64(len(s))) + p.buf = append(p.buf, s...) + return nil +} + +func sizeStringBytes(s string) int { + return sizeVarint(uint64(len(s))) + + len(s) +} + +// Marshaler is the interface representing objects that can marshal themselves. +type Marshaler interface { + Marshal() ([]byte, error) +} + +// Marshal takes the protocol buffer +// and encodes it into the wire format, returning the data. +func Marshal(pb Message) ([]byte, error) { + // Can the object marshal itself? + if m, ok := pb.(Marshaler); ok { + return m.Marshal() + } + p := NewBuffer(nil) + err := p.Marshal(pb) + var state errorState + if err != nil && !state.shouldContinue(err, nil) { + return nil, err + } + if p.buf == nil && err == nil { + // Return a non-nil slice on success. + return []byte{}, nil + } + return p.buf, err +} + +// Marshal takes the protocol buffer +// and encodes it into the wire format, writing the result to the +// Buffer. +func (p *Buffer) Marshal(pb Message) error { + // Can the object marshal itself? + if m, ok := pb.(Marshaler); ok { + data, err := m.Marshal() + if err != nil { + return err + } + p.buf = append(p.buf, data...) + return nil + } + + t, base, err := getbase(pb) + if structPointer_IsNil(base) { + return ErrNil + } + if err == nil { + err = p.enc_struct(t.Elem(), GetProperties(t.Elem()), base) + } + + if collectStats { + stats.Encode++ + } + + return err +} + +// Size returns the encoded size of a protocol buffer. +func Size(pb Message) (n int) { + // Can the object marshal itself? If so, Size is slow. + // TODO: add Size to Marshaler, or add a Sizer interface. + if m, ok := pb.(Marshaler); ok { + b, _ := m.Marshal() + return len(b) + } + + t, base, err := getbase(pb) + if structPointer_IsNil(base) { + return 0 + } + if err == nil { + n = size_struct(t.Elem(), GetProperties(t.Elem()), base) + } + + if collectStats { + stats.Size++ + } + + return +} + +// Individual type encoders. + +// Encode a bool. +func (o *Buffer) enc_bool(p *Properties, base structPointer) error { + v := *structPointer_Bool(base, p.field) + if v == nil { + return ErrNil + } + x := 0 + if *v { + x = 1 + } + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, uint64(x)) + return nil +} + +func size_bool(p *Properties, base structPointer) int { + v := *structPointer_Bool(base, p.field) + if v == nil { + return 0 + } + return len(p.tagcode) + 1 // each bool takes exactly one byte +} + +// Encode an int32. +func (o *Buffer) enc_int32(p *Properties, base structPointer) error { + v := structPointer_Word32(base, p.field) + if word32_IsNil(v) { + return ErrNil + } + x := word32_Get(v) + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, uint64(x)) + return nil +} + +func size_int32(p *Properties, base structPointer) (n int) { + v := structPointer_Word32(base, p.field) + if word32_IsNil(v) { + return 0 + } + x := word32_Get(v) + n += len(p.tagcode) + n += p.valSize(uint64(x)) + return +} + +// Encode an int64. +func (o *Buffer) enc_int64(p *Properties, base structPointer) error { + v := structPointer_Word64(base, p.field) + if word64_IsNil(v) { + return ErrNil + } + x := word64_Get(v) + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, x) + return nil +} + +func size_int64(p *Properties, base structPointer) (n int) { + v := structPointer_Word64(base, p.field) + if word64_IsNil(v) { + return 0 + } + x := word64_Get(v) + n += len(p.tagcode) + n += p.valSize(x) + return +} + +// Encode a string. +func (o *Buffer) enc_string(p *Properties, base structPointer) error { + v := *structPointer_String(base, p.field) + if v == nil { + return ErrNil + } + x := *v + o.buf = append(o.buf, p.tagcode...) + o.EncodeStringBytes(x) + return nil +} + +func size_string(p *Properties, base structPointer) (n int) { + v := *structPointer_String(base, p.field) + if v == nil { + return 0 + } + x := *v + n += len(p.tagcode) + n += sizeStringBytes(x) + return +} + +// All protocol buffer fields are nillable, but be careful. +func isNil(v reflect.Value) bool { + switch v.Kind() { + case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return v.IsNil() + } + return false +} + +// Encode a message struct. +func (o *Buffer) enc_struct_message(p *Properties, base structPointer) error { + var state errorState + structp := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(structp) { + return ErrNil + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, err := m.Marshal() + if err != nil && !state.shouldContinue(err, nil) { + return err + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + return nil + } + + o.buf = append(o.buf, p.tagcode...) + return o.enc_len_struct(p.stype, p.sprop, structp, &state) +} + +func size_struct_message(p *Properties, base structPointer) int { + structp := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(structp) { + return 0 + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, _ := m.Marshal() + n0 := len(p.tagcode) + n1 := sizeRawBytes(data) + return n0 + n1 + } + + n0 := len(p.tagcode) + n1 := size_struct(p.stype, p.sprop, structp) + n2 := sizeVarint(uint64(n1)) // size of encoded length + return n0 + n1 + n2 +} + +// Encode a group struct. +func (o *Buffer) enc_struct_group(p *Properties, base structPointer) error { + var state errorState + b := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(b) { + return ErrNil + } + + o.EncodeVarint(uint64((p.Tag << 3) | WireStartGroup)) + err := o.enc_struct(p.stype, p.sprop, b) + if err != nil && !state.shouldContinue(err, nil) { + return err + } + o.EncodeVarint(uint64((p.Tag << 3) | WireEndGroup)) + return state.err +} + +func size_struct_group(p *Properties, base structPointer) (n int) { + b := structPointer_GetStructPointer(base, p.field) + if structPointer_IsNil(b) { + return 0 + } + + n += sizeVarint(uint64((p.Tag << 3) | WireStartGroup)) + n += size_struct(p.stype, p.sprop, b) + n += sizeVarint(uint64((p.Tag << 3) | WireEndGroup)) + return +} + +// Encode a slice of bools ([]bool). +func (o *Buffer) enc_slice_bool(p *Properties, base structPointer) error { + s := *structPointer_BoolSlice(base, p.field) + l := len(s) + if l == 0 { + return ErrNil + } + for _, x := range s { + o.buf = append(o.buf, p.tagcode...) + v := uint64(0) + if x { + v = 1 + } + p.valEnc(o, v) + } + return nil +} + +func size_slice_bool(p *Properties, base structPointer) int { + s := *structPointer_BoolSlice(base, p.field) + l := len(s) + if l == 0 { + return 0 + } + return l * (len(p.tagcode) + 1) // each bool takes exactly one byte +} + +// Encode a slice of bools ([]bool) in packed format. +func (o *Buffer) enc_slice_packed_bool(p *Properties, base structPointer) error { + s := *structPointer_BoolSlice(base, p.field) + l := len(s) + if l == 0 { + return ErrNil + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeVarint(uint64(l)) // each bool takes exactly one byte + for _, x := range s { + v := uint64(0) + if x { + v = 1 + } + p.valEnc(o, v) + } + return nil +} + +func size_slice_packed_bool(p *Properties, base structPointer) (n int) { + s := *structPointer_BoolSlice(base, p.field) + l := len(s) + if l == 0 { + return 0 + } + n += len(p.tagcode) + n += sizeVarint(uint64(l)) + n += l // each bool takes exactly one byte + return +} + +// Encode a slice of bytes ([]byte). +func (o *Buffer) enc_slice_byte(p *Properties, base structPointer) error { + s := *structPointer_Bytes(base, p.field) + if s == nil { + return ErrNil + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(s) + return nil +} + +func size_slice_byte(p *Properties, base structPointer) (n int) { + s := *structPointer_Bytes(base, p.field) + if s == nil { + return 0 + } + n += len(p.tagcode) + n += sizeRawBytes(s) + return +} + +// Encode a slice of int32s ([]int32). +func (o *Buffer) enc_slice_int32(p *Properties, base structPointer) error { + s := structPointer_Word32Slice(base, p.field) + l := s.Len() + if l == 0 { + return ErrNil + } + for i := 0; i < l; i++ { + o.buf = append(o.buf, p.tagcode...) + x := s.Index(i) + p.valEnc(o, uint64(x)) + } + return nil +} + +func size_slice_int32(p *Properties, base structPointer) (n int) { + s := structPointer_Word32Slice(base, p.field) + l := s.Len() + if l == 0 { + return 0 + } + for i := 0; i < l; i++ { + n += len(p.tagcode) + x := s.Index(i) + n += p.valSize(uint64(x)) + } + return +} + +// Encode a slice of int32s ([]int32) in packed format. +func (o *Buffer) enc_slice_packed_int32(p *Properties, base structPointer) error { + s := structPointer_Word32Slice(base, p.field) + l := s.Len() + if l == 0 { + return ErrNil + } + // TODO: Reuse a Buffer. + buf := NewBuffer(nil) + for i := 0; i < l; i++ { + p.valEnc(buf, uint64(s.Index(i))) + } + + o.buf = append(o.buf, p.tagcode...) + o.EncodeVarint(uint64(len(buf.buf))) + o.buf = append(o.buf, buf.buf...) + return nil +} + +func size_slice_packed_int32(p *Properties, base structPointer) (n int) { + s := structPointer_Word32Slice(base, p.field) + l := s.Len() + if l == 0 { + return 0 + } + var bufSize int + for i := 0; i < l; i++ { + bufSize += p.valSize(uint64(s.Index(i))) + } + + n += len(p.tagcode) + n += sizeVarint(uint64(bufSize)) + n += bufSize + return +} + +// Encode a slice of int64s ([]int64). +func (o *Buffer) enc_slice_int64(p *Properties, base structPointer) error { + s := structPointer_Word64Slice(base, p.field) + l := s.Len() + if l == 0 { + return ErrNil + } + for i := 0; i < l; i++ { + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, s.Index(i)) + } + return nil +} + +func size_slice_int64(p *Properties, base structPointer) (n int) { + s := structPointer_Word64Slice(base, p.field) + l := s.Len() + if l == 0 { + return 0 + } + for i := 0; i < l; i++ { + n += len(p.tagcode) + n += p.valSize(s.Index(i)) + } + return +} + +// Encode a slice of int64s ([]int64) in packed format. +func (o *Buffer) enc_slice_packed_int64(p *Properties, base structPointer) error { + s := structPointer_Word64Slice(base, p.field) + l := s.Len() + if l == 0 { + return ErrNil + } + // TODO: Reuse a Buffer. + buf := NewBuffer(nil) + for i := 0; i < l; i++ { + p.valEnc(buf, s.Index(i)) + } + + o.buf = append(o.buf, p.tagcode...) + o.EncodeVarint(uint64(len(buf.buf))) + o.buf = append(o.buf, buf.buf...) + return nil +} + +func size_slice_packed_int64(p *Properties, base structPointer) (n int) { + s := structPointer_Word64Slice(base, p.field) + l := s.Len() + if l == 0 { + return 0 + } + var bufSize int + for i := 0; i < l; i++ { + bufSize += p.valSize(s.Index(i)) + } + + n += len(p.tagcode) + n += sizeVarint(uint64(bufSize)) + n += bufSize + return +} + +// Encode a slice of slice of bytes ([][]byte). +func (o *Buffer) enc_slice_slice_byte(p *Properties, base structPointer) error { + ss := *structPointer_BytesSlice(base, p.field) + l := len(ss) + if l == 0 { + return ErrNil + } + for i := 0; i < l; i++ { + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(ss[i]) + } + return nil +} + +func size_slice_slice_byte(p *Properties, base structPointer) (n int) { + ss := *structPointer_BytesSlice(base, p.field) + l := len(ss) + if l == 0 { + return 0 + } + n += l * len(p.tagcode) + for i := 0; i < l; i++ { + n += sizeRawBytes(ss[i]) + } + return +} + +// Encode a slice of strings ([]string). +func (o *Buffer) enc_slice_string(p *Properties, base structPointer) error { + ss := *structPointer_StringSlice(base, p.field) + l := len(ss) + for i := 0; i < l; i++ { + o.buf = append(o.buf, p.tagcode...) + o.EncodeStringBytes(ss[i]) + } + return nil +} + +func size_slice_string(p *Properties, base structPointer) (n int) { + ss := *structPointer_StringSlice(base, p.field) + l := len(ss) + n += l * len(p.tagcode) + for i := 0; i < l; i++ { + n += sizeStringBytes(ss[i]) + } + return +} + +// Encode a slice of message structs ([]*struct). +func (o *Buffer) enc_slice_struct_message(p *Properties, base structPointer) error { + var state errorState + s := structPointer_StructPointerSlice(base, p.field) + l := s.Len() + + for i := 0; i < l; i++ { + structp := s.Index(i) + if structPointer_IsNil(structp) { + return ErrRepeatedHasNil + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, err := m.Marshal() + if err != nil && !state.shouldContinue(err, nil) { + return err + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + continue + } + + o.buf = append(o.buf, p.tagcode...) + err := o.enc_len_struct(p.stype, p.sprop, structp, &state) + if err != nil && !state.shouldContinue(err, nil) { + if err == ErrNil { + return ErrRepeatedHasNil + } + return err + } + } + return state.err +} + +func size_slice_struct_message(p *Properties, base structPointer) (n int) { + s := structPointer_StructPointerSlice(base, p.field) + l := s.Len() + n += l * len(p.tagcode) + for i := 0; i < l; i++ { + structp := s.Index(i) + if structPointer_IsNil(structp) { + return // return the size up to this point + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, _ := m.Marshal() + n += len(p.tagcode) + n += sizeRawBytes(data) + continue + } + + n0 := size_struct(p.stype, p.sprop, structp) + n1 := sizeVarint(uint64(n0)) // size of encoded length + n += n0 + n1 + } + return +} + +// Encode a slice of group structs ([]*struct). +func (o *Buffer) enc_slice_struct_group(p *Properties, base structPointer) error { + var state errorState + s := structPointer_StructPointerSlice(base, p.field) + l := s.Len() + + for i := 0; i < l; i++ { + b := s.Index(i) + if structPointer_IsNil(b) { + return ErrRepeatedHasNil + } + + o.EncodeVarint(uint64((p.Tag << 3) | WireStartGroup)) + + err := o.enc_struct(p.stype, p.sprop, b) + + if err != nil && !state.shouldContinue(err, nil) { + if err == ErrNil { + return ErrRepeatedHasNil + } + return err + } + + o.EncodeVarint(uint64((p.Tag << 3) | WireEndGroup)) + } + return state.err +} + +func size_slice_struct_group(p *Properties, base structPointer) (n int) { + s := structPointer_StructPointerSlice(base, p.field) + l := s.Len() + + n += l * sizeVarint(uint64((p.Tag<<3)|WireStartGroup)) + n += l * sizeVarint(uint64((p.Tag<<3)|WireEndGroup)) + for i := 0; i < l; i++ { + b := s.Index(i) + if structPointer_IsNil(b) { + return // return size up to this point + } + + n += size_struct(p.stype, p.sprop, b) + } + return +} + +// Encode an extension map. +func (o *Buffer) enc_map(p *Properties, base structPointer) error { + v := *structPointer_ExtMap(base, p.field) + if err := encodeExtensionMap(v); err != nil { + return err + } + // Fast-path for common cases: zero or one extensions. + if len(v) <= 1 { + for _, e := range v { + o.buf = append(o.buf, e.enc...) + } + return nil + } + + // Sort keys to provide a deterministic encoding. + keys := make([]int, 0, len(v)) + for k := range v { + keys = append(keys, int(k)) + } + sort.Ints(keys) + + for _, k := range keys { + o.buf = append(o.buf, v[int32(k)].enc...) + } + return nil +} + +func size_map(p *Properties, base structPointer) int { + v := *structPointer_ExtMap(base, p.field) + return sizeExtensionMap(v) +} + +// Encode a struct. +func (o *Buffer) enc_struct(t reflect.Type, prop *StructProperties, base structPointer) error { + var state errorState + // Encode fields in tag order so that decoders may use optimizations + // that depend on the ordering. + // http://code.google.com/apis/protocolbuffers/docs/encoding.html#order + for _, i := range prop.order { + p := prop.Prop[i] + if p.enc != nil { + err := p.enc(o, p, base) + if err != nil { + if err == ErrNil { + if p.Required && state.err == nil { + state.err = &RequiredNotSetError{p.Name} + } + } else if !state.shouldContinue(err, p) { + return err + } + } + } + } + + // Add unrecognized fields at the end. + if prop.unrecField.IsValid() { + v := *structPointer_Bytes(base, prop.unrecField) + if len(v) > 0 { + o.buf = append(o.buf, v...) + } + } + + return state.err +} + +func size_struct(t reflect.Type, prop *StructProperties, base structPointer) (n int) { + for _, i := range prop.order { + p := prop.Prop[i] + if p.size != nil { + n += p.size(p, base) + } + } + + // Add unrecognized fields at the end. + if prop.unrecField.IsValid() { + v := *structPointer_Bytes(base, prop.unrecField) + n += len(v) + } + + return +} + +var zeroes [20]byte // longer than any conceivable sizeVarint + +// Encode a struct, preceded by its encoded length (as a varint). +func (o *Buffer) enc_len_struct(t reflect.Type, prop *StructProperties, base structPointer, state *errorState) error { + iLen := len(o.buf) + o.buf = append(o.buf, 0, 0, 0, 0) // reserve four bytes for length + iMsg := len(o.buf) + err := o.enc_struct(t, prop, base) + if err != nil && !state.shouldContinue(err, nil) { + return err + } + lMsg := len(o.buf) - iMsg + lLen := sizeVarint(uint64(lMsg)) + switch x := lLen - (iMsg - iLen); { + case x > 0: // actual length is x bytes larger than the space we reserved + // Move msg x bytes right. + o.buf = append(o.buf, zeroes[:x]...) + copy(o.buf[iMsg+x:], o.buf[iMsg:iMsg+lMsg]) + case x < 0: // actual length is x bytes smaller than the space we reserved + // Move msg x bytes left. + copy(o.buf[iMsg+x:], o.buf[iMsg:iMsg+lMsg]) + o.buf = o.buf[:len(o.buf)+x] // x is negative + } + // Encode the length in the reserved space. + o.buf = o.buf[:iLen] + o.EncodeVarint(uint64(lMsg)) + o.buf = o.buf[:len(o.buf)+lMsg] + return state.err +} + +// errorState maintains the first error that occurs and updates that error +// with additional context. +type errorState struct { + err error +} + +// shouldContinue reports whether encoding should continue upon encountering the +// given error. If the error is RequiredNotSetError, shouldContinue returns true +// and, if this is the first appearance of that error, remembers it for future +// reporting. +// +// If prop is not nil, it may update any error with additional context about the +// field with the error. +func (s *errorState) shouldContinue(err error, prop *Properties) bool { + // Ignore unset required fields. + reqNotSet, ok := err.(*RequiredNotSetError) + if !ok { + return false + } + if s.err == nil { + if prop != nil { + err = &RequiredNotSetError{prop.Name + "." + reqNotSet.field} + } + s.err = err + } + return true +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode_gogo.go new file mode 100644 index 00000000000..d5d7017aa9e --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/encode_gogo.go @@ -0,0 +1,361 @@ +// Extensions for Protocol Buffers to create more go like structures. +// +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "reflect" +) + +type Sizer interface { + Size() int +} + +func (o *Buffer) enc_ext_slice_byte(p *Properties, base structPointer) error { + s := *structPointer_Bytes(base, p.field) + if s == nil { + return ErrNil + } + o.buf = append(o.buf, s...) + return nil +} + +func size_ext_slice_byte(p *Properties, base structPointer) (n int) { + s := *structPointer_Bytes(base, p.field) + if s == nil { + return 0 + } + n += len(s) + return +} + +// Encode a reference to bool pointer. +func (o *Buffer) enc_ref_bool(p *Properties, base structPointer) error { + v := structPointer_RefBool(base, p.field) + if v == nil { + return ErrNil + } + x := 0 + if *v { + x = 1 + } + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, uint64(x)) + return nil +} + +func size_ref_bool(p *Properties, base structPointer) int { + v := structPointer_RefBool(base, p.field) + if v == nil { + return 0 + } + return len(p.tagcode) + 1 // each bool takes exactly one byte +} + +// Encode a reference to int32 pointer. +func (o *Buffer) enc_ref_int32(p *Properties, base structPointer) error { + v := structPointer_RefWord32(base, p.field) + if refWord32_IsNil(v) { + return ErrNil + } + x := refWord32_Get(v) + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, uint64(x)) + return nil +} + +func size_ref_int32(p *Properties, base structPointer) (n int) { + v := structPointer_RefWord32(base, p.field) + if refWord32_IsNil(v) { + return 0 + } + x := refWord32_Get(v) + n += len(p.tagcode) + n += p.valSize(uint64(x)) + return +} + +// Encode a reference to an int64 pointer. +func (o *Buffer) enc_ref_int64(p *Properties, base structPointer) error { + v := structPointer_RefWord64(base, p.field) + if refWord64_IsNil(v) { + return ErrNil + } + x := refWord64_Get(v) + o.buf = append(o.buf, p.tagcode...) + p.valEnc(o, x) + return nil +} + +func size_ref_int64(p *Properties, base structPointer) (n int) { + v := structPointer_RefWord64(base, p.field) + if refWord64_IsNil(v) { + return 0 + } + x := refWord64_Get(v) + n += len(p.tagcode) + n += p.valSize(x) + return +} + +// Encode a reference to a string pointer. +func (o *Buffer) enc_ref_string(p *Properties, base structPointer) error { + v := structPointer_RefString(base, p.field) + if v == nil { + return ErrNil + } + x := *v + o.buf = append(o.buf, p.tagcode...) + o.EncodeStringBytes(x) + return nil +} + +func size_ref_string(p *Properties, base structPointer) (n int) { + v := structPointer_RefString(base, p.field) + if v == nil { + return 0 + } + x := *v + n += len(p.tagcode) + n += sizeStringBytes(x) + return +} + +// Encode a reference to a message struct. +func (o *Buffer) enc_ref_struct_message(p *Properties, base structPointer) error { + var state errorState + structp := structPointer_GetRefStructPointer(base, p.field) + if structPointer_IsNil(structp) { + return ErrNil + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, err := m.Marshal() + if err != nil && !state.shouldContinue(err, nil) { + return err + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + return nil + } + + o.buf = append(o.buf, p.tagcode...) + return o.enc_len_struct(p.stype, p.sprop, structp, &state) +} + +//TODO this is only copied, please fix this +func size_ref_struct_message(p *Properties, base structPointer) int { + structp := structPointer_GetRefStructPointer(base, p.field) + if structPointer_IsNil(structp) { + return 0 + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, _ := m.Marshal() + n0 := len(p.tagcode) + n1 := sizeRawBytes(data) + return n0 + n1 + } + + n0 := len(p.tagcode) + n1 := size_struct(p.stype, p.sprop, structp) + n2 := sizeVarint(uint64(n1)) // size of encoded length + return n0 + n1 + n2 +} + +// Encode a slice of references to message struct pointers ([]struct). +func (o *Buffer) enc_slice_ref_struct_message(p *Properties, base structPointer) error { + var state errorState + ss := structPointer_GetStructPointer(base, p.field) + ss1 := structPointer_GetRefStructPointer(ss, field(0)) + size := p.stype.Size() + l := structPointer_Len(base, p.field) + for i := 0; i < l; i++ { + structp := structPointer_Add(ss1, field(uintptr(i)*size)) + if structPointer_IsNil(structp) { + return ErrRepeatedHasNil + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, err := m.Marshal() + if err != nil && !state.shouldContinue(err, nil) { + return err + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + continue + } + + o.buf = append(o.buf, p.tagcode...) + err := o.enc_len_struct(p.stype, p.sprop, structp, &state) + if err != nil && !state.shouldContinue(err, nil) { + if err == ErrNil { + return ErrRepeatedHasNil + } + return err + } + + } + return state.err +} + +//TODO this is only copied, please fix this +func size_slice_ref_struct_message(p *Properties, base structPointer) (n int) { + ss := structPointer_GetStructPointer(base, p.field) + ss1 := structPointer_GetRefStructPointer(ss, field(0)) + size := p.stype.Size() + l := structPointer_Len(base, p.field) + n += l * len(p.tagcode) + for i := 0; i < l; i++ { + structp := structPointer_Add(ss1, field(uintptr(i)*size)) + if structPointer_IsNil(structp) { + return // return the size up to this point + } + + // Can the object marshal itself? + if p.isMarshaler { + m := structPointer_Interface(structp, p.stype).(Marshaler) + data, _ := m.Marshal() + n += len(p.tagcode) + n += sizeRawBytes(data) + continue + } + + n0 := size_struct(p.stype, p.sprop, structp) + n1 := sizeVarint(uint64(n0)) // size of encoded length + n += n0 + n1 + } + return +} + +func (o *Buffer) enc_custom_bytes(p *Properties, base structPointer) error { + i := structPointer_InterfaceRef(base, p.field, p.ctype) + if i == nil { + return ErrNil + } + custom := i.(Marshaler) + data, err := custom.Marshal() + if err != nil { + return err + } + if data == nil { + return ErrNil + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + return nil +} + +func size_custom_bytes(p *Properties, base structPointer) (n int) { + n += len(p.tagcode) + i := structPointer_InterfaceRef(base, p.field, p.ctype) + if i == nil { + return 0 + } + custom := i.(Marshaler) + data, _ := custom.Marshal() + n += sizeRawBytes(data) + return +} + +func (o *Buffer) enc_custom_ref_bytes(p *Properties, base structPointer) error { + custom := structPointer_InterfaceAt(base, p.field, p.ctype).(Marshaler) + data, err := custom.Marshal() + if err != nil { + return err + } + if data == nil { + return ErrNil + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + return nil +} + +func size_custom_ref_bytes(p *Properties, base structPointer) (n int) { + n += len(p.tagcode) + i := structPointer_InterfaceAt(base, p.field, p.ctype) + if i == nil { + return 0 + } + custom := i.(Marshaler) + data, _ := custom.Marshal() + n += sizeRawBytes(data) + return +} + +func (o *Buffer) enc_custom_slice_bytes(p *Properties, base structPointer) error { + inter := structPointer_InterfaceRef(base, p.field, p.ctype) + if inter == nil { + return ErrNil + } + slice := reflect.ValueOf(inter) + l := slice.Len() + for i := 0; i < l; i++ { + v := slice.Index(i) + custom := v.Interface().(Marshaler) + data, err := custom.Marshal() + if err != nil { + return err + } + o.buf = append(o.buf, p.tagcode...) + o.EncodeRawBytes(data) + } + return nil +} + +func size_custom_slice_bytes(p *Properties, base structPointer) (n int) { + inter := structPointer_InterfaceRef(base, p.field, p.ctype) + if inter == nil { + return 0 + } + slice := reflect.ValueOf(inter) + l := slice.Len() + n += l * len(p.tagcode) + for i := 0; i < l; i++ { + v := slice.Index(i) + custom := v.Interface().(Marshaler) + data, _ := custom.Marshal() + n += sizeRawBytes(data) + } + return +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal.go new file mode 100644 index 00000000000..42542e69282 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal.go @@ -0,0 +1,241 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Protocol buffer comparison. +// TODO: MessageSet. + +package proto + +import ( + "bytes" + "log" + "reflect" + "strings" +) + +/* +Equal returns true iff protocol buffers a and b are equal. +The arguments must both be pointers to protocol buffer structs. + +Equality is defined in this way: + - Two messages are equal iff they are the same type, + corresponding fields are equal, unknown field sets + are equal, and extensions sets are equal. + - Two set scalar fields are equal iff their values are equal. + If the fields are of a floating-point type, remember that + NaN != x for all x, including NaN. + - Two repeated fields are equal iff their lengths are the same, + and their corresponding elements are equal (a "bytes" field, + although represented by []byte, is not a repeated field) + - Two unset fields are equal. + - Two unknown field sets are equal if their current + encoded state is equal. (TODO) + - Two extension sets are equal iff they have corresponding + elements that are pairwise equal. + - Every other combination of things are not equal. + +The return value is undefined if a and b are not protocol buffers. +*/ +func Equal(a, b Message) bool { + if a == nil || b == nil { + return a == b + } + v1, v2 := reflect.ValueOf(a), reflect.ValueOf(b) + if v1.Type() != v2.Type() { + return false + } + if v1.Kind() == reflect.Ptr { + if v1.IsNil() { + return v2.IsNil() + } + if v2.IsNil() { + return false + } + v1, v2 = v1.Elem(), v2.Elem() + } + if v1.Kind() != reflect.Struct { + return false + } + return equalStruct(v1, v2) +} + +// v1 and v2 are known to have the same type. +func equalStruct(v1, v2 reflect.Value) bool { + for i := 0; i < v1.NumField(); i++ { + f := v1.Type().Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + f1, f2 := v1.Field(i), v2.Field(i) + if f.Type.Kind() == reflect.Ptr { + if n1, n2 := f1.IsNil(), f2.IsNil(); n1 && n2 { + // both unset + continue + } else if n1 != n2 { + // set/unset mismatch + return false + } + b1, ok := f1.Interface().(raw) + if ok { + b2 := f2.Interface().(raw) + // RawMessage + if !bytes.Equal(b1.Bytes(), b2.Bytes()) { + return false + } + continue + } + f1, f2 = f1.Elem(), f2.Elem() + } + if !equalAny(f1, f2) { + return false + } + } + + if em1 := v1.FieldByName("XXX_extensions"); em1.IsValid() { + em2 := v2.FieldByName("XXX_extensions") + if !equalExtensions(v1.Type(), em1.Interface().(map[int32]Extension), em2.Interface().(map[int32]Extension)) { + return false + } + } + + uf := v1.FieldByName("XXX_unrecognized") + if !uf.IsValid() { + return true + } + + u1 := uf.Bytes() + u2 := v2.FieldByName("XXX_unrecognized").Bytes() + if !bytes.Equal(u1, u2) { + return false + } + + return true +} + +// v1 and v2 are known to have the same type. +func equalAny(v1, v2 reflect.Value) bool { + if v1.Type() == protoMessageType { + m1, _ := v1.Interface().(Message) + m2, _ := v2.Interface().(Message) + return Equal(m1, m2) + } + switch v1.Kind() { + case reflect.Bool: + return v1.Bool() == v2.Bool() + case reflect.Float32, reflect.Float64: + return v1.Float() == v2.Float() + case reflect.Int32, reflect.Int64: + return v1.Int() == v2.Int() + case reflect.Ptr: + return equalAny(v1.Elem(), v2.Elem()) + case reflect.Slice: + if v1.Type().Elem().Kind() == reflect.Uint8 { + // short circuit: []byte + if v1.IsNil() != v2.IsNil() { + return false + } + return bytes.Equal(v1.Interface().([]byte), v2.Interface().([]byte)) + } + + if v1.Len() != v2.Len() { + return false + } + for i := 0; i < v1.Len(); i++ { + if !equalAny(v1.Index(i), v2.Index(i)) { + return false + } + } + return true + case reflect.String: + return v1.Interface().(string) == v2.Interface().(string) + case reflect.Struct: + return equalStruct(v1, v2) + case reflect.Uint32, reflect.Uint64: + return v1.Uint() == v2.Uint() + } + + // unknown type, so not a protocol buffer + log.Printf("proto: don't know how to compare %v", v1) + return false +} + +// base is the struct type that the extensions are based on. +// em1 and em2 are extension maps. +func equalExtensions(base reflect.Type, em1, em2 map[int32]Extension) bool { + if len(em1) != len(em2) { + return false + } + + for extNum, e1 := range em1 { + e2, ok := em2[extNum] + if !ok { + return false + } + + m1, m2 := e1.value, e2.value + + if m1 != nil && m2 != nil { + // Both are unencoded. + if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2)) { + return false + } + continue + } + + // At least one is encoded. To do a semantically correct comparison + // we need to unmarshal them first. + var desc *ExtensionDesc + if m := extensionMaps[base]; m != nil { + desc = m[extNum] + } + if desc == nil { + log.Printf("proto: don't know how to compare extension %d of %v", extNum, base) + continue + } + var err error + if m1 == nil { + m1, err = decodeExtension(e1.enc, desc) + } + if m2 == nil && err == nil { + m2, err = decodeExtension(e2.enc, desc) + } + if err != nil { + // The encoded form is invalid. + log.Printf("proto: badly encoded extension %d of %v: %v", extNum, base, err) + return false + } + if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2)) { + return false + } + } + + return true +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal_test.go new file mode 100644 index 00000000000..d3535de60d6 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/equal_test.go @@ -0,0 +1,166 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "testing" + + pb "./testdata" + . "code.google.com/p/gogoprotobuf/proto" +) + +// Four identical base messages. +// The init function adds extensions to some of them. +var messageWithoutExtension = &pb.MyMessage{Count: Int32(7)} +var messageWithExtension1a = &pb.MyMessage{Count: Int32(7)} +var messageWithExtension1b = &pb.MyMessage{Count: Int32(7)} +var messageWithExtension2 = &pb.MyMessage{Count: Int32(7)} + +// Two messages with non-message extensions. +var messageWithInt32Extension1 = &pb.MyMessage{Count: Int32(8)} +var messageWithInt32Extension2 = &pb.MyMessage{Count: Int32(8)} + +func init() { + ext1 := &pb.Ext{Data: String("Kirk")} + ext2 := &pb.Ext{Data: String("Picard")} + + // messageWithExtension1a has ext1, but never marshals it. + if err := SetExtension(messageWithExtension1a, pb.E_Ext_More, ext1); err != nil { + panic("SetExtension on 1a failed: " + err.Error()) + } + + // messageWithExtension1b is the unmarshaled form of messageWithExtension1a. + if err := SetExtension(messageWithExtension1b, pb.E_Ext_More, ext1); err != nil { + panic("SetExtension on 1b failed: " + err.Error()) + } + buf, err := Marshal(messageWithExtension1b) + if err != nil { + panic("Marshal of 1b failed: " + err.Error()) + } + messageWithExtension1b.Reset() + if err := Unmarshal(buf, messageWithExtension1b); err != nil { + panic("Unmarshal of 1b failed: " + err.Error()) + } + + // messageWithExtension2 has ext2. + if err := SetExtension(messageWithExtension2, pb.E_Ext_More, ext2); err != nil { + panic("SetExtension on 2 failed: " + err.Error()) + } + + if err := SetExtension(messageWithInt32Extension1, pb.E_Ext_Number, Int32(23)); err != nil { + panic("SetExtension on Int32-1 failed: " + err.Error()) + } + if err := SetExtension(messageWithInt32Extension1, pb.E_Ext_Number, Int32(24)); err != nil { + panic("SetExtension on Int32-2 failed: " + err.Error()) + } +} + +var EqualTests = []struct { + desc string + a, b Message + exp bool +}{ + {"different types", &pb.GoEnum{}, &pb.GoTestField{}, false}, + {"equal empty", &pb.GoEnum{}, &pb.GoEnum{}, true}, + {"nil vs nil", nil, nil, true}, + {"typed nil vs typed nil", (*pb.GoEnum)(nil), (*pb.GoEnum)(nil), true}, + {"typed nil vs empty", (*pb.GoEnum)(nil), &pb.GoEnum{}, false}, + {"different typed nil", (*pb.GoEnum)(nil), (*pb.GoTestField)(nil), false}, + + {"one set field, one unset field", &pb.GoTestField{Label: String("foo")}, &pb.GoTestField{}, false}, + {"one set field zero, one unset field", &pb.GoTest{Param: Int32(0)}, &pb.GoTest{}, false}, + {"different set fields", &pb.GoTestField{Label: String("foo")}, &pb.GoTestField{Label: String("bar")}, false}, + {"equal set", &pb.GoTestField{Label: String("foo")}, &pb.GoTestField{Label: String("foo")}, true}, + + {"repeated, one set", &pb.GoTest{F_Int32Repeated: []int32{2, 3}}, &pb.GoTest{}, false}, + {"repeated, different length", &pb.GoTest{F_Int32Repeated: []int32{2, 3}}, &pb.GoTest{F_Int32Repeated: []int32{2}}, false}, + {"repeated, different value", &pb.GoTest{F_Int32Repeated: []int32{2}}, &pb.GoTest{F_Int32Repeated: []int32{3}}, false}, + {"repeated, equal", &pb.GoTest{F_Int32Repeated: []int32{2, 4}}, &pb.GoTest{F_Int32Repeated: []int32{2, 4}}, true}, + {"repeated, nil equal nil", &pb.GoTest{F_Int32Repeated: nil}, &pb.GoTest{F_Int32Repeated: nil}, true}, + {"repeated, nil equal empty", &pb.GoTest{F_Int32Repeated: nil}, &pb.GoTest{F_Int32Repeated: []int32{}}, true}, + {"repeated, empty equal nil", &pb.GoTest{F_Int32Repeated: []int32{}}, &pb.GoTest{F_Int32Repeated: nil}, true}, + + { + "nested, different", + &pb.GoTest{RequiredField: &pb.GoTestField{Label: String("foo")}}, + &pb.GoTest{RequiredField: &pb.GoTestField{Label: String("bar")}}, + false, + }, + { + "nested, equal", + &pb.GoTest{RequiredField: &pb.GoTestField{Label: String("wow")}}, + &pb.GoTest{RequiredField: &pb.GoTestField{Label: String("wow")}}, + true, + }, + + {"bytes", &pb.OtherMessage{Value: []byte("foo")}, &pb.OtherMessage{Value: []byte("foo")}, true}, + {"bytes, empty", &pb.OtherMessage{Value: []byte{}}, &pb.OtherMessage{Value: []byte{}}, true}, + {"bytes, empty vs nil", &pb.OtherMessage{Value: []byte{}}, &pb.OtherMessage{Value: nil}, false}, + { + "repeated bytes", + &pb.MyMessage{RepBytes: [][]byte{[]byte("sham"), []byte("wow")}}, + &pb.MyMessage{RepBytes: [][]byte{[]byte("sham"), []byte("wow")}}, + true, + }, + + {"extension vs. no extension", messageWithoutExtension, messageWithExtension1a, false}, + {"extension vs. same extension", messageWithExtension1a, messageWithExtension1b, true}, + {"extension vs. different extension", messageWithExtension1a, messageWithExtension2, false}, + + {"int32 extension vs. itself", messageWithInt32Extension1, messageWithInt32Extension1, true}, + {"int32 extension vs. a different int32", messageWithInt32Extension1, messageWithInt32Extension2, false}, + + { + "message with group", + &pb.MyMessage{ + Count: Int32(1), + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: Int32(5), + }, + }, + &pb.MyMessage{ + Count: Int32(1), + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: Int32(5), + }, + }, + true, + }, +} + +func TestEqual(t *testing.T) { + for _, tc := range EqualTests { + if res := Equal(tc.a, tc.b); res != tc.exp { + t.Errorf("%v: Equal(%v, %v) = %v, want %v", tc.desc, tc.a, tc.b, res, tc.exp) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions.go new file mode 100644 index 00000000000..3749958ba8d --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions.go @@ -0,0 +1,460 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Types and routines for supporting protocol buffer extensions. + */ + +import ( + "errors" + "reflect" + "strconv" + "sync" +) + +// ErrMissingExtension is the error returned by GetExtension if the named extension is not in the message. +var ErrMissingExtension = errors.New("proto: missing extension") + +// ExtensionRange represents a range of message extensions for a protocol buffer. +// Used in code generated by the protocol compiler. +type ExtensionRange struct { + Start, End int32 // both inclusive +} + +// extendableProto is an interface implemented by any protocol buffer that may be extended. +type extendableProto interface { + Message + ExtensionRangeArray() []ExtensionRange +} + +type extensionsMap interface { + extendableProto + ExtensionMap() map[int32]Extension +} + +type extensionsBytes interface { + extendableProto + GetExtensions() *[]byte +} + +var extendableProtoType = reflect.TypeOf((*extendableProto)(nil)).Elem() + +// ExtensionDesc represents an extension specification. +// Used in generated code from the protocol compiler. +type ExtensionDesc struct { + ExtendedType Message // nil pointer to the type that is being extended + ExtensionType interface{} // nil pointer to the extension type + Field int32 // field number + Name string // fully-qualified name of extension, for text formatting + Tag string // protobuf tag style +} + +func (ed *ExtensionDesc) repeated() bool { + t := reflect.TypeOf(ed.ExtensionType) + return t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8 +} + +// Extension represents an extension in a message. +type Extension struct { + // When an extension is stored in a message using SetExtension + // only desc and value are set. When the message is marshaled + // enc will be set to the encoded form of the message. + // + // When a message is unmarshaled and contains extensions, each + // extension will have only enc set. When such an extension is + // accessed using GetExtension (or GetExtensions) desc and value + // will be set. + desc *ExtensionDesc + value interface{} + enc []byte +} + +// SetRawExtension is for testing only. +func SetRawExtension(base extendableProto, id int32, b []byte) { + if ebase, ok := base.(extensionsMap); ok { + ebase.ExtensionMap()[id] = Extension{enc: b} + } else if ebase, ok := base.(extensionsBytes); ok { + clearExtension(base, id) + ext := ebase.GetExtensions() + *ext = append(*ext, b...) + } else { + panic("unreachable") + } +} + +// isExtensionField returns true iff the given field number is in an extension range. +func isExtensionField(pb extendableProto, field int32) bool { + for _, er := range pb.ExtensionRangeArray() { + if er.Start <= field && field <= er.End { + return true + } + } + return false +} + +// checkExtensionTypes checks that the given extension is valid for pb. +func checkExtensionTypes(pb extendableProto, extension *ExtensionDesc) error { + // Check the extended type. + if a, b := reflect.TypeOf(pb), reflect.TypeOf(extension.ExtendedType); a != b { + return errors.New("proto: bad extended type; " + b.String() + " does not extend " + a.String()) + } + // Check the range. + if !isExtensionField(pb, extension.Field) { + return errors.New("proto: bad extension number; not in declared ranges") + } + return nil +} + +// extPropKey is sufficient to uniquely identify an extension. +type extPropKey struct { + base reflect.Type + field int32 +} + +var extProp = struct { + sync.RWMutex + m map[extPropKey]*Properties +}{ + m: make(map[extPropKey]*Properties), +} + +func extensionProperties(ed *ExtensionDesc) *Properties { + key := extPropKey{base: reflect.TypeOf(ed.ExtendedType), field: ed.Field} + + extProp.RLock() + if prop, ok := extProp.m[key]; ok { + extProp.RUnlock() + return prop + } + extProp.RUnlock() + + extProp.Lock() + defer extProp.Unlock() + // Check again. + if prop, ok := extProp.m[key]; ok { + return prop + } + + prop := new(Properties) + prop.Init(reflect.TypeOf(ed.ExtensionType), "unknown_name", ed.Tag, nil) + extProp.m[key] = prop + return prop +} + +// encodeExtensionMap encodes any unmarshaled (unencoded) extensions in m. +func encodeExtensionMap(m map[int32]Extension) error { + for k, e := range m { + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + et := reflect.TypeOf(e.desc.ExtensionType) + props := extensionProperties(e.desc) + + p := NewBuffer(nil) + // If e.value has type T, the encoder expects a *struct{ X T }. + // Pass a *T with a zero field and hope it all works out. + x := reflect.New(et) + x.Elem().Set(reflect.ValueOf(e.value)) + if err := props.enc(p, props, toStructPointer(x)); err != nil { + return err + } + e.enc = p.buf + m[k] = e + } + return nil +} + +func sizeExtensionMap(m map[int32]Extension) (n int) { + for _, e := range m { + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + n += len(e.enc) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + et := reflect.TypeOf(e.desc.ExtensionType) + props := extensionProperties(e.desc) + + // If e.value has type T, the encoder expects a *struct{ X T }. + // Pass a *T with a zero field and hope it all works out. + x := reflect.New(et) + x.Elem().Set(reflect.ValueOf(e.value)) + n += props.size(props, toStructPointer(x)) + } + return +} + +// HasExtension returns whether the given extension is present in pb. +func HasExtension(pb extendableProto, extension *ExtensionDesc) bool { + // TODO: Check types, field numbers, etc.? + if epb, doki := pb.(extensionsMap); doki { + _, ok := epb.ExtensionMap()[extension.Field] + return ok + } else if epb, doki := pb.(extensionsBytes); doki { + ext := epb.GetExtensions() + buf := *ext + o := 0 + for o < len(buf) { + tag, n := DecodeVarint(buf[o:]) + fieldNum := int32(tag >> 3) + if int32(fieldNum) == extension.Field { + return true + } + wireType := int(tag & 0x7) + o += n + l, err := size(buf[o:], wireType) + if err != nil { + return false + } + o += l + } + return false + } + panic("unreachable") +} + +func deleteExtension(pb extensionsBytes, theFieldNum int32, offset int) int { + ext := pb.GetExtensions() + for offset < len(*ext) { + tag, n1 := DecodeVarint((*ext)[offset:]) + fieldNum := int32(tag >> 3) + wireType := int(tag & 0x7) + n2, err := size((*ext)[offset+n1:], wireType) + if err != nil { + panic(err) + } + newOffset := offset + n1 + n2 + if fieldNum == theFieldNum { + *ext = append((*ext)[:offset], (*ext)[newOffset:]...) + return offset + } + offset = newOffset + } + return -1 +} + +func clearExtension(pb extendableProto, fieldNum int32) { + if epb, doki := pb.(extensionsMap); doki { + delete(epb.ExtensionMap(), fieldNum) + } else if epb, doki := pb.(extensionsBytes); doki { + offset := 0 + for offset != -1 { + offset = deleteExtension(epb, fieldNum, offset) + } + } else { + panic("unreachable") + } +} + +// ClearExtension removes the given extension from pb. +func ClearExtension(pb extendableProto, extension *ExtensionDesc) { + // TODO: Check types, field numbers, etc.? + clearExtension(pb, extension.Field) +} + +// GetExtension parses and returns the given extension of pb. +// If the extension is not present it returns ErrMissingExtension. +func GetExtension(pb extendableProto, extension *ExtensionDesc) (interface{}, error) { + if err := checkExtensionTypes(pb, extension); err != nil { + return nil, err + } + + if epb, doki := pb.(extensionsMap); doki { + e, ok := epb.ExtensionMap()[extension.Field] + if !ok { + return nil, ErrMissingExtension + } + if e.value != nil { + // Already decoded. Check the descriptor, though. + if e.desc != extension { + // This shouldn't happen. If it does, it means that + // GetExtension was called twice with two different + // descriptors with the same field number. + return nil, errors.New("proto: descriptor conflict") + } + return e.value, nil + } + + v, err := decodeExtension(e.enc, extension) + if err != nil { + return nil, err + } + + // Remember the decoded version and drop the encoded version. + // That way it is safe to mutate what we return. + e.value = v + e.desc = extension + e.enc = nil + return e.value, nil + } else if epb, doki := pb.(extensionsBytes); doki { + ext := epb.GetExtensions() + o := 0 + for o < len(*ext) { + tag, n := DecodeVarint((*ext)[o:]) + fieldNum := int32(tag >> 3) + wireType := int(tag & 0x7) + l, err := size((*ext)[o+n:], wireType) + if err != nil { + return nil, err + } + if int32(fieldNum) == extension.Field { + v, err := decodeExtension((*ext)[o:o+n+l], extension) + if err != nil { + return nil, err + } + return v, nil + } + o += n + l + } + } + panic("unreachable") +} + +// decodeExtension decodes an extension encoded in b. +func decodeExtension(b []byte, extension *ExtensionDesc) (interface{}, error) { + o := NewBuffer(b) + + t := reflect.TypeOf(extension.ExtensionType) + rep := extension.repeated() + + props := extensionProperties(extension) + + // t is a pointer to a struct, pointer to basic type or a slice. + // Allocate a "field" to store the pointer/slice itself; the + // pointer/slice will be stored here. We pass + // the address of this field to props.dec. + // This passes a zero field and a *t and lets props.dec + // interpret it as a *struct{ x t }. + value := reflect.New(t).Elem() + + for { + // Discard wire type and field number varint. It isn't needed. + if _, err := o.DecodeVarint(); err != nil { + return nil, err + } + + if err := props.dec(o, props, toStructPointer(value.Addr())); err != nil { + return nil, err + } + + if !rep || o.index >= len(o.buf) { + break + } + } + return value.Interface(), nil +} + +// GetExtensions returns a slice of the extensions present in pb that are also listed in es. +// The returned slice has the same length as es; missing extensions will appear as nil elements. +func GetExtensions(pb Message, es []*ExtensionDesc) (extensions []interface{}, err error) { + epb, ok := pb.(extendableProto) + if !ok { + err = errors.New("proto: not an extendable proto") + return + } + extensions = make([]interface{}, len(es)) + for i, e := range es { + extensions[i], err = GetExtension(epb, e) + if err == ErrMissingExtension { + err = nil + } + if err != nil { + return + } + } + return +} + +// SetExtension sets the specified extension of pb to the specified value. +func SetExtension(pb extendableProto, extension *ExtensionDesc, value interface{}) error { + if err := checkExtensionTypes(pb, extension); err != nil { + return err + } + typ := reflect.TypeOf(extension.ExtensionType) + if typ != reflect.TypeOf(value) { + return errors.New("proto: bad extension value type") + } + + if epb, doki := pb.(extensionsMap); doki { + epb.ExtensionMap()[extension.Field] = Extension{desc: extension, value: value} + } else if epb, doki := pb.(extensionsBytes); doki { + ClearExtension(pb, extension) + ext := epb.GetExtensions() + et := reflect.TypeOf(extension.ExtensionType) + props := extensionProperties(extension) + p := NewBuffer(nil) + x := reflect.New(et) + x.Elem().Set(reflect.ValueOf(value)) + if err := props.enc(p, props, toStructPointer(x)); err != nil { + return err + } + *ext = append(*ext, p.buf...) + } + return nil +} + +// A global registry of extensions. +// The generated code will register the generated descriptors by calling RegisterExtension. + +var extensionMaps = make(map[reflect.Type]map[int32]*ExtensionDesc) + +// RegisterExtension is called from the generated code. +func RegisterExtension(desc *ExtensionDesc) { + st := reflect.TypeOf(desc.ExtendedType).Elem() + m := extensionMaps[st] + if m == nil { + m = make(map[int32]*ExtensionDesc) + extensionMaps[st] = m + } + if _, ok := m[desc.Field]; ok { + panic("proto: duplicate extension registered: " + st.String() + " " + strconv.Itoa(int(desc.Field))) + } + m[desc.Field] = desc +} + +// RegisteredExtensions returns a map of the registered extensions of a +// protocol buffer struct, indexed by the extension number. +// The argument pb should be a nil pointer to the struct type. +func RegisteredExtensions(pb Message) map[int32]*ExtensionDesc { + return extensionMaps[reflect.TypeOf(pb).Elem()] +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_gogo.go new file mode 100644 index 00000000000..8f7eb8264ef --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_gogo.go @@ -0,0 +1,189 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strings" +) + +func GetBoolExtension(pb extendableProto, extension *ExtensionDesc, ifnotset bool) bool { + if reflect.ValueOf(pb).IsNil() { + return ifnotset + } + value, err := GetExtension(pb, extension) + if err != nil { + return ifnotset + } + if value == nil { + return ifnotset + } + if value.(*bool) == nil { + return ifnotset + } + return *(value.(*bool)) +} + +func (this *Extension) Equal(that *Extension) bool { + return bytes.Equal(this.enc, that.enc) +} + +func SizeOfExtensionMap(m map[int32]Extension) (n int) { + return sizeExtensionMap(m) +} + +type sortableMapElem struct { + field int32 + ext Extension +} + +func newSortableExtensionsFromMap(m map[int32]Extension) sortableExtensions { + s := make(sortableExtensions, 0, len(m)) + for k, v := range m { + s = append(s, &sortableMapElem{field: k, ext: v}) + } + return s +} + +type sortableExtensions []*sortableMapElem + +func (this sortableExtensions) Len() int { return len(this) } + +func (this sortableExtensions) Swap(i, j int) { this[i], this[j] = this[j], this[i] } + +func (this sortableExtensions) Less(i, j int) bool { return this[i].field < this[j].field } + +func (this sortableExtensions) String() string { + sort.Sort(this) + ss := make([]string, len(this)) + for i := range this { + ss[i] = fmt.Sprintf("%d: %v", this[i].field, this[i].ext) + } + return "map[" + strings.Join(ss, ",") + "]" +} + +func StringFromExtensionsMap(m map[int32]Extension) string { + return newSortableExtensionsFromMap(m).String() +} + +func StringFromExtensionsBytes(ext []byte) string { + m, err := BytesToExtensionsMap(ext) + if err != nil { + panic(err) + } + return StringFromExtensionsMap(m) +} + +func EncodeExtensionMap(m map[int32]Extension, data []byte) (n int, err error) { + if err := encodeExtensionMap(m); err != nil { + return 0, err + } + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, int(k)) + } + sort.Ints(keys) + for _, k := range keys { + n += copy(data[n:], m[int32(k)].enc) + } + return n, nil +} + +func GetRawExtension(m map[int32]Extension, id int32) ([]byte, error) { + if m[id].value == nil || m[id].desc == nil { + return m[id].enc, nil + } + if err := encodeExtensionMap(m); err != nil { + return nil, err + } + return m[id].enc, nil +} + +func size(buf []byte, wire int) (int, error) { + switch wire { + case WireVarint: + _, n := DecodeVarint(buf) + return n, nil + case WireFixed64: + return 8, nil + case WireBytes: + v, n := DecodeVarint(buf) + return int(v) + n, nil + case WireFixed32: + return 4, nil + case WireStartGroup: + offset := 0 + for { + u, n := DecodeVarint(buf[offset:]) + fwire := int(u & 0x7) + offset += n + if fwire == WireEndGroup { + return offset, nil + } + s, err := size(buf[offset:], wire) + if err != nil { + return 0, err + } + offset += s + } + } + return 0, fmt.Errorf("proto: can't get size for unknown wire type %d", wire) +} + +func BytesToExtensionsMap(buf []byte) (map[int32]Extension, error) { + m := make(map[int32]Extension) + i := 0 + for i < len(buf) { + tag, n := DecodeVarint(buf[i:]) + if n <= 0 { + return nil, fmt.Errorf("unable to decode varint") + } + fieldNum := int32(tag >> 3) + wireType := int(tag & 0x7) + l, err := size(buf[i+n:], wireType) + if err != nil { + return nil, err + } + end := i + int(l) + n + m[int32(fieldNum)] = Extension{enc: buf[i:end]} + i = end + } + return m, nil +} + +func NewExtension(e []byte) Extension { + ee := Extension{enc: make([]byte, len(e))} + copy(ee.enc, e) + return ee +} + +func (this Extension) GoString() string { + return fmt.Sprintf("proto.NewExtension(%#v)", this.enc) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_test.go new file mode 100644 index 00000000000..3aabfef0458 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/extensions_test.go @@ -0,0 +1,60 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2014 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "testing" + + pb "./testdata" + "code.google.com/p/gogoprotobuf/proto" +) + +func TestGetExtensionsWithMissingExtensions(t *testing.T) { + msg := &pb.MyMessage{} + ext1 := &pb.Ext{} + if err := proto.SetExtension(msg, pb.E_Ext_More, ext1); err != nil { + t.Fatalf("Could not set ext1: %s", ext1) + } + exts, err := proto.GetExtensions(msg, []*proto.ExtensionDesc{ + pb.E_Ext_More, + pb.E_Ext_Text, + }) + if err != nil { + t.Fatalf("GetExtensions() failed: %s", err) + } + if exts[0] != ext1 { + t.Errorf("ext1 not in returned extensions: %T %v", exts[0], exts[0]) + } + if exts[1] != nil { + t.Errorf("ext2 in returned extensions: %T %v", exts[1], exts[1]) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib.go new file mode 100644 index 00000000000..e70c0fddc56 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib.go @@ -0,0 +1,740 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* + Package proto converts data structures to and from the wire format of + protocol buffers. It works in concert with the Go source code generated + for .proto files by the protocol compiler. + + A summary of the properties of the protocol buffer interface + for a protocol buffer variable v: + + - Names are turned from camel_case to CamelCase for export. + - There are no methods on v to set fields; just treat + them as structure fields. + - There are getters that return a field's value if set, + and return the field's default value if unset. + The getters work even if the receiver is a nil message. + - The zero value for a struct is its correct initialization state. + All desired fields must be set before marshaling. + - A Reset() method will restore a protobuf struct to its zero state. + - Non-repeated fields are pointers to the values; nil means unset. + That is, optional or required field int32 f becomes F *int32. + - Repeated fields are slices. + - Helper functions are available to aid the setting of fields. + Helpers for getting values are superseded by the + GetFoo methods and their use is deprecated. + msg.Foo = proto.String("hello") // set field + - Constants are defined to hold the default values of all fields that + have them. They have the form Default_StructName_FieldName. + Because the getter methods handle defaulted values, + direct use of these constants should be rare. + - Enums are given type names and maps from names to values. + Enum values are prefixed with the enum's type name. Enum types have + a String method, and a Enum method to assist in message construction. + - Nested groups and enums have type names prefixed with the name of + the surrounding message type. + - Extensions are given descriptor names that start with E_, + followed by an underscore-delimited list of the nested messages + that contain it (if any) followed by the CamelCased name of the + extension field itself. HasExtension, ClearExtension, GetExtension + and SetExtension are functions for manipulating extensions. + - Marshal and Unmarshal are functions to encode and decode the wire format. + + The simplest way to describe this is to see an example. + Given file test.proto, containing + + package example; + + enum FOO { X = 17; }; + + message Test { + required string label = 1; + optional int32 type = 2 [default=77]; + repeated int64 reps = 3; + optional group OptionalGroup = 4 { + required string RequiredField = 5; + } + } + + The resulting file, test.pb.go, is: + + package example + + import "code.google.com/p/gogoprotobuf/proto" + + type FOO int32 + const ( + FOO_X FOO = 17 + ) + var FOO_name = map[int32]string{ + 17: "X", + } + var FOO_value = map[string]int32{ + "X": 17, + } + + func (x FOO) Enum() *FOO { + p := new(FOO) + *p = x + return p + } + func (x FOO) String() string { + return proto.EnumName(FOO_name, int32(x)) + } + + type Test struct { + Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` + Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` + Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"` + Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + func (this *Test) Reset() { *this = Test{} } + func (this *Test) String() string { return proto.CompactTextString(this) } + const Default_Test_Type int32 = 77 + + func (this *Test) GetLabel() string { + if this != nil && this.Label != nil { + return *this.Label + } + return "" + } + + func (this *Test) GetType() int32 { + if this != nil && this.Type != nil { + return *this.Type + } + return Default_Test_Type + } + + func (this *Test) GetOptionalgroup() *Test_OptionalGroup { + if this != nil { + return this.Optionalgroup + } + return nil + } + + type Test_OptionalGroup struct { + RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + func (this *Test_OptionalGroup) Reset() { *this = Test_OptionalGroup{} } + func (this *Test_OptionalGroup) String() string { return proto.CompactTextString(this) } + + func (this *Test_OptionalGroup) GetRequiredField() string { + if this != nil && this.RequiredField != nil { + return *this.RequiredField + } + return "" + } + + func init() { + proto.RegisterEnum("example.FOO", FOO_name, FOO_value) + } + + To create and play with a Test object: + + package main + + import ( + "log" + + "code.google.com/p/gogoprotobuf/proto" + "./example.pb" + ) + + func main() { + test := &example.Test{ + Label: proto.String("hello"), + Type: proto.Int32(17), + Optionalgroup: &example.Test_OptionalGroup{ + RequiredField: proto.String("good bye"), + }, + } + data, err := proto.Marshal(test) + if err != nil { + log.Fatal("marshaling error: ", err) + } + newTest := new(example.Test) + err = proto.Unmarshal(data, newTest) + if err != nil { + log.Fatal("unmarshaling error: ", err) + } + // Now test and newTest contain the same data. + if test.GetLabel() != newTest.GetLabel() { + log.Fatalf("data mismatch %q != %q", test.GetLabel(), newTest.GetLabel()) + } + // etc. + } +*/ +package proto + +import ( + "encoding/json" + "fmt" + "log" + "reflect" + "strconv" + "sync" +) + +// Message is implemented by generated protocol buffer messages. +type Message interface { + Reset() + String() string + ProtoMessage() +} + +// Stats records allocation details about the protocol buffer encoders +// and decoders. Useful for tuning the library itself. +type Stats struct { + Emalloc uint64 // mallocs in encode + Dmalloc uint64 // mallocs in decode + Encode uint64 // number of encodes + Decode uint64 // number of decodes + Chit uint64 // number of cache hits + Cmiss uint64 // number of cache misses + Size uint64 // number of sizes +} + +// Set to true to enable stats collection. +const collectStats = false + +var stats Stats + +// GetStats returns a copy of the global Stats structure. +func GetStats() Stats { return stats } + +// A Buffer is a buffer manager for marshaling and unmarshaling +// protocol buffers. It may be reused between invocations to +// reduce memory usage. It is not necessary to use a Buffer; +// the global functions Marshal and Unmarshal create a +// temporary Buffer and are fine for most applications. +type Buffer struct { + buf []byte // encode/decode byte stream + index int // write point + + // pools of basic types to amortize allocation. + bools []bool + uint32s []uint32 + uint64s []uint64 + + // extra pools, only used with pointer_reflect.go + int32s []int32 + int64s []int64 + float32s []float32 + float64s []float64 +} + +// NewBuffer allocates a new Buffer and initializes its internal data to +// the contents of the argument slice. +func NewBuffer(e []byte) *Buffer { + return &Buffer{buf: e} +} + +// Reset resets the Buffer, ready for marshaling a new protocol buffer. +func (p *Buffer) Reset() { + p.buf = p.buf[0:0] // for reading/writing + p.index = 0 // for reading +} + +// SetBuf replaces the internal buffer with the slice, +// ready for unmarshaling the contents of the slice. +func (p *Buffer) SetBuf(s []byte) { + p.buf = s + p.index = 0 +} + +// Bytes returns the contents of the Buffer. +func (p *Buffer) Bytes() []byte { return p.buf } + +/* + * Helper routines for simplifying the creation of optional fields of basic type. + */ + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { + return &v +} + +// Int32 is a helper routine that allocates a new int32 value +// to store v and returns a pointer to it. +func Int32(v int32) *int32 { + return &v +} + +// Int is a helper routine that allocates a new int32 value +// to store v and returns a pointer to it, but unlike Int32 +// its argument value is an int. +func Int(v int) *int32 { + p := new(int32) + *p = int32(v) + return p +} + +// Int64 is a helper routine that allocates a new int64 value +// to store v and returns a pointer to it. +func Int64(v int64) *int64 { + return &v +} + +// Float32 is a helper routine that allocates a new float32 value +// to store v and returns a pointer to it. +func Float32(v float32) *float32 { + return &v +} + +// Float64 is a helper routine that allocates a new float64 value +// to store v and returns a pointer to it. +func Float64(v float64) *float64 { + return &v +} + +// Uint32 is a helper routine that allocates a new uint32 value +// to store v and returns a pointer to it. +func Uint32(v uint32) *uint32 { + p := new(uint32) + *p = v + return p +} + +// Uint64 is a helper routine that allocates a new uint64 value +// to store v and returns a pointer to it. +func Uint64(v uint64) *uint64 { + return &v +} + +// String is a helper routine that allocates a new string value +// to store v and returns a pointer to it. +func String(v string) *string { + return &v +} + +// EnumName is a helper function to simplify printing protocol buffer enums +// by name. Given an enum map and a value, it returns a useful string. +func EnumName(m map[int32]string, v int32) string { + s, ok := m[v] + if ok { + return s + } + return strconv.Itoa(int(v)) +} + +// UnmarshalJSONEnum is a helper function to simplify recovering enum int values +// from their JSON-encoded representation. Given a map from the enum's symbolic +// names to its int values, and a byte buffer containing the JSON-encoded +// value, it returns an int32 that can be cast to the enum type by the caller. +// +// The function can deal with both JSON representations, numeric and symbolic. +func UnmarshalJSONEnum(m map[string]int32, data []byte, enumName string) (int32, error) { + if data[0] == '"' { + // New style: enums are strings. + var repr string + if err := json.Unmarshal(data, &repr); err != nil { + return -1, err + } + val, ok := m[repr] + if !ok { + return 0, fmt.Errorf("unrecognized enum %s value %q", enumName, repr) + } + return val, nil + } + // Old style: enums are ints. + var val int32 + if err := json.Unmarshal(data, &val); err != nil { + return 0, fmt.Errorf("cannot unmarshal %#q into enum %s", data, enumName) + } + return val, nil +} + +// DebugPrint dumps the encoded data in b in a debugging format with a header +// including the string s. Used in testing but made available for general debugging. +func (o *Buffer) DebugPrint(s string, b []byte) { + var u uint64 + + obuf := o.buf + index := o.index + o.buf = b + o.index = 0 + depth := 0 + + fmt.Printf("\n--- %s ---\n", s) + +out: + for { + for i := 0; i < depth; i++ { + fmt.Print(" ") + } + + index := o.index + if index == len(o.buf) { + break + } + + op, err := o.DecodeVarint() + if err != nil { + fmt.Printf("%3d: fetching op err %v\n", index, err) + break out + } + tag := op >> 3 + wire := op & 7 + + switch wire { + default: + fmt.Printf("%3d: t=%3d unknown wire=%d\n", + index, tag, wire) + break out + + case WireBytes: + var r []byte + + r, err = o.DecodeRawBytes(false) + if err != nil { + break out + } + fmt.Printf("%3d: t=%3d bytes [%d]", index, tag, len(r)) + if len(r) <= 6 { + for i := 0; i < len(r); i++ { + fmt.Printf(" %.2x", r[i]) + } + } else { + for i := 0; i < 3; i++ { + fmt.Printf(" %.2x", r[i]) + } + fmt.Printf(" ..") + for i := len(r) - 3; i < len(r); i++ { + fmt.Printf(" %.2x", r[i]) + } + } + fmt.Printf("\n") + + case WireFixed32: + u, err = o.DecodeFixed32() + if err != nil { + fmt.Printf("%3d: t=%3d fix32 err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d fix32 %d\n", index, tag, u) + + case WireFixed64: + u, err = o.DecodeFixed64() + if err != nil { + fmt.Printf("%3d: t=%3d fix64 err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d fix64 %d\n", index, tag, u) + break + + case WireVarint: + u, err = o.DecodeVarint() + if err != nil { + fmt.Printf("%3d: t=%3d varint err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d varint %d\n", index, tag, u) + + case WireStartGroup: + if err != nil { + fmt.Printf("%3d: t=%3d start err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d start\n", index, tag) + depth++ + + case WireEndGroup: + depth-- + if err != nil { + fmt.Printf("%3d: t=%3d end err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d end\n", index, tag) + } + } + + if depth != 0 { + fmt.Printf("%3d: start-end not balanced %d\n", o.index, depth) + } + fmt.Printf("\n") + + o.buf = obuf + o.index = index +} + +// SetDefaults sets unset protocol buffer fields to their default values. +// It only modifies fields that are both unset and have defined defaults. +// It recursively sets default values in any non-nil sub-messages. +func SetDefaults(pb Message) { + setDefaults(reflect.ValueOf(pb), true, false) +} + +// v is a pointer to a struct. +func setDefaults(v reflect.Value, recur, zeros bool) { + v = v.Elem() + + defaultMu.RLock() + dm, ok := defaults[v.Type()] + defaultMu.RUnlock() + if !ok { + dm = buildDefaultMessage(v.Type()) + defaultMu.Lock() + defaults[v.Type()] = dm + defaultMu.Unlock() + } + + for _, sf := range dm.scalars { + f := v.Field(sf.index) + if !f.IsNil() { + // field already set + continue + } + dv := sf.value + if dv == nil && !zeros { + // no explicit default, and don't want to set zeros + continue + } + fptr := f.Addr().Interface() // **T + // TODO: Consider batching the allocations we do here. + switch sf.kind { + case reflect.Bool: + b := new(bool) + if dv != nil { + *b = dv.(bool) + } + *(fptr.(**bool)) = b + case reflect.Float32: + f := new(float32) + if dv != nil { + *f = dv.(float32) + } + *(fptr.(**float32)) = f + case reflect.Float64: + f := new(float64) + if dv != nil { + *f = dv.(float64) + } + *(fptr.(**float64)) = f + case reflect.Int32: + // might be an enum + if ft := f.Type(); ft != int32PtrType { + // enum + f.Set(reflect.New(ft.Elem())) + if dv != nil { + f.Elem().SetInt(int64(dv.(int32))) + } + } else { + // int32 field + i := new(int32) + if dv != nil { + *i = dv.(int32) + } + *(fptr.(**int32)) = i + } + case reflect.Int64: + i := new(int64) + if dv != nil { + *i = dv.(int64) + } + *(fptr.(**int64)) = i + case reflect.String: + s := new(string) + if dv != nil { + *s = dv.(string) + } + *(fptr.(**string)) = s + case reflect.Uint8: + // exceptional case: []byte + var b []byte + if dv != nil { + db := dv.([]byte) + b = make([]byte, len(db)) + copy(b, db) + } else { + b = []byte{} + } + *(fptr.(*[]byte)) = b + case reflect.Uint32: + u := new(uint32) + if dv != nil { + *u = dv.(uint32) + } + *(fptr.(**uint32)) = u + case reflect.Uint64: + u := new(uint64) + if dv != nil { + *u = dv.(uint64) + } + *(fptr.(**uint64)) = u + default: + log.Printf("proto: can't set default for field %v (sf.kind=%v)", f, sf.kind) + } + } + + for _, ni := range dm.nested { + f := v.Field(ni) + if f.IsNil() { + continue + } + // f is *T or []*T + if f.Kind() == reflect.Ptr { + setDefaults(f, recur, zeros) + } else { + for i := 0; i < f.Len(); i++ { + e := f.Index(i) + if e.IsNil() { + continue + } + setDefaults(e, recur, zeros) + } + } + } +} + +var ( + // defaults maps a protocol buffer struct type to a slice of the fields, + // with its scalar fields set to their proto-declared non-zero default values. + defaultMu sync.RWMutex + defaults = make(map[reflect.Type]defaultMessage) + + int32PtrType = reflect.TypeOf((*int32)(nil)) +) + +// defaultMessage represents information about the default values of a message. +type defaultMessage struct { + scalars []scalarField + nested []int // struct field index of nested messages +} + +type scalarField struct { + index int // struct field index + kind reflect.Kind // element type (the T in *T or []T) + value interface{} // the proto-declared default value, or nil +} + +func ptrToStruct(t reflect.Type) bool { + return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct +} + +// t is a struct type. +func buildDefaultMessage(t reflect.Type) (dm defaultMessage) { + sprop := GetProperties(t) + for _, prop := range sprop.Prop { + fi, ok := sprop.decoderTags.get(prop.Tag) + if !ok { + // XXX_unrecognized + continue + } + ft := t.Field(fi).Type + + // nested messages + if ptrToStruct(ft) || (ft.Kind() == reflect.Slice && ptrToStruct(ft.Elem())) { + dm.nested = append(dm.nested, fi) + continue + } + + sf := scalarField{ + index: fi, + kind: ft.Elem().Kind(), + } + + // scalar fields without defaults + if prop.Default == "" { + dm.scalars = append(dm.scalars, sf) + continue + } + + // a scalar field: either *T or []byte + switch ft.Elem().Kind() { + case reflect.Bool: + x, err := strconv.ParseBool(prop.Default) + if err != nil { + log.Printf("proto: bad default bool %q: %v", prop.Default, err) + continue + } + sf.value = x + case reflect.Float32: + x, err := strconv.ParseFloat(prop.Default, 32) + if err != nil { + log.Printf("proto: bad default float32 %q: %v", prop.Default, err) + continue + } + sf.value = float32(x) + case reflect.Float64: + x, err := strconv.ParseFloat(prop.Default, 64) + if err != nil { + log.Printf("proto: bad default float64 %q: %v", prop.Default, err) + continue + } + sf.value = x + case reflect.Int32: + x, err := strconv.ParseInt(prop.Default, 10, 32) + if err != nil { + log.Printf("proto: bad default int32 %q: %v", prop.Default, err) + continue + } + sf.value = int32(x) + case reflect.Int64: + x, err := strconv.ParseInt(prop.Default, 10, 64) + if err != nil { + log.Printf("proto: bad default int64 %q: %v", prop.Default, err) + continue + } + sf.value = x + case reflect.String: + sf.value = prop.Default + case reflect.Uint8: + // []byte (not *uint8) + sf.value = []byte(prop.Default) + case reflect.Uint32: + x, err := strconv.ParseUint(prop.Default, 10, 32) + if err != nil { + log.Printf("proto: bad default uint32 %q: %v", prop.Default, err) + continue + } + sf.value = uint32(x) + case reflect.Uint64: + x, err := strconv.ParseUint(prop.Default, 10, 64) + if err != nil { + log.Printf("proto: bad default uint64 %q: %v", prop.Default, err) + continue + } + sf.value = x + default: + log.Printf("proto: unhandled def kind %v", ft.Elem().Kind()) + continue + } + + dm.scalars = append(dm.scalars, sf) + } + + return dm +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib_gogo.go new file mode 100644 index 00000000000..06278e7f360 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/lib_gogo.go @@ -0,0 +1,40 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "encoding/json" + "strconv" +) + +func MarshalJSONEnum(m map[int32]string, value int32) ([]byte, error) { + s, ok := m[value] + if !ok { + s = strconv.Itoa(int(value)) + } + return json.Marshal(s) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/message_set.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/message_set.go new file mode 100644 index 00000000000..6ddcc30ade3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/message_set.go @@ -0,0 +1,216 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Support for message sets. + */ + +import ( + "errors" + "reflect" + "sort" +) + +// ErrNoMessageTypeId occurs when a protocol buffer does not have a message type ID. +// A message type ID is required for storing a protocol buffer in a message set. +var ErrNoMessageTypeId = errors.New("proto does not have a message type ID") + +// The first two types (_MessageSet_Item and MessageSet) +// model what the protocol compiler produces for the following protocol message: +// message MessageSet { +// repeated group Item = 1 { +// required int32 type_id = 2; +// required string message = 3; +// }; +// } +// That is the MessageSet wire format. We can't use a proto to generate these +// because that would introduce a circular dependency between it and this package. +// +// When a proto1 proto has a field that looks like: +// optional message info = 3; +// the protocol compiler produces a field in the generated struct that looks like: +// Info *_proto_.MessageSet `protobuf:"bytes,3,opt,name=info"` +// The package is automatically inserted so there is no need for that proto file to +// import this package. + +type _MessageSet_Item struct { + TypeId *int32 `protobuf:"varint,2,req,name=type_id"` + Message []byte `protobuf:"bytes,3,req,name=message"` +} + +type MessageSet struct { + Item []*_MessageSet_Item `protobuf:"group,1,rep"` + XXX_unrecognized []byte + // TODO: caching? +} + +// Make sure MessageSet is a Message. +var _ Message = (*MessageSet)(nil) + +// messageTypeIder is an interface satisfied by a protocol buffer type +// that may be stored in a MessageSet. +type messageTypeIder interface { + MessageTypeId() int32 +} + +func (ms *MessageSet) find(pb Message) *_MessageSet_Item { + mti, ok := pb.(messageTypeIder) + if !ok { + return nil + } + id := mti.MessageTypeId() + for _, item := range ms.Item { + if *item.TypeId == id { + return item + } + } + return nil +} + +func (ms *MessageSet) Has(pb Message) bool { + if ms.find(pb) != nil { + return true + } + return false +} + +func (ms *MessageSet) Unmarshal(pb Message) error { + if item := ms.find(pb); item != nil { + return Unmarshal(item.Message, pb) + } + if _, ok := pb.(messageTypeIder); !ok { + return ErrNoMessageTypeId + } + return nil // TODO: return error instead? +} + +func (ms *MessageSet) Marshal(pb Message) error { + msg, err := Marshal(pb) + if err != nil { + return err + } + if item := ms.find(pb); item != nil { + // reuse existing item + item.Message = msg + return nil + } + + mti, ok := pb.(messageTypeIder) + if !ok { + return ErrNoMessageTypeId + } + + mtid := mti.MessageTypeId() + ms.Item = append(ms.Item, &_MessageSet_Item{ + TypeId: &mtid, + Message: msg, + }) + return nil +} + +func (ms *MessageSet) Reset() { *ms = MessageSet{} } +func (ms *MessageSet) String() string { return CompactTextString(ms) } +func (*MessageSet) ProtoMessage() {} + +// Support for the message_set_wire_format message option. + +func skipVarint(buf []byte) []byte { + i := 0 + for ; buf[i]&0x80 != 0; i++ { + } + return buf[i+1:] +} + +// MarshalMessageSet encodes the extension map represented by m in the message set wire format. +// It is called by generated Marshal methods on protocol buffer messages with the message_set_wire_format option. +func MarshalMessageSet(m map[int32]Extension) ([]byte, error) { + if err := encodeExtensionMap(m); err != nil { + return nil, err + } + + // Sort extension IDs to provide a deterministic encoding. + // See also enc_map in encode.go. + ids := make([]int, 0, len(m)) + for id := range m { + ids = append(ids, int(id)) + } + sort.Ints(ids) + + ms := &MessageSet{Item: make([]*_MessageSet_Item, 0, len(m))} + for _, id := range ids { + e := m[int32(id)] + // Remove the wire type and field number varint, as well as the length varint. + msg := skipVarint(skipVarint(e.enc)) + + ms.Item = append(ms.Item, &_MessageSet_Item{ + TypeId: Int32(int32(id)), + Message: msg, + }) + } + return Marshal(ms) +} + +// UnmarshalMessageSet decodes the extension map encoded in buf in the message set wire format. +// It is called by generated Unmarshal methods on protocol buffer messages with the message_set_wire_format option. +func UnmarshalMessageSet(buf []byte, m map[int32]Extension) error { + ms := new(MessageSet) + if err := Unmarshal(buf, ms); err != nil { + return err + } + for _, item := range ms.Item { + // restore wire type and field number varint, plus length varint. + b := EncodeVarint(uint64(*item.TypeId)<<3 | WireBytes) + b = append(b, EncodeVarint(uint64(len(item.Message)))...) + b = append(b, item.Message...) + + m[*item.TypeId] = Extension{enc: b} + } + return nil +} + +// A global registry of types that can be used in a MessageSet. + +var messageSetMap = make(map[int32]messageSetDesc) + +type messageSetDesc struct { + t reflect.Type // pointer to struct + name string +} + +// RegisterMessageSetType is called from the generated code. +func RegisterMessageSetType(i messageTypeIder, name string) { + messageSetMap[i.MessageTypeId()] = messageSetDesc{ + t: reflect.TypeOf(i), + name: name, + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_reflect.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_reflect.go new file mode 100644 index 00000000000..61141ba8580 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_reflect.go @@ -0,0 +1,384 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// +build appengine,!appenginevm + +// This file contains an implementation of proto field accesses using package reflect. +// It is slower than the code in pointer_unsafe.go but it avoids package unsafe and can +// be used on App Engine. + +package proto + +import ( + "math" + "reflect" +) + +// A structPointer is a pointer to a struct. +type structPointer struct { + v reflect.Value +} + +// toStructPointer returns a structPointer equivalent to the given reflect value. +// The reflect value must itself be a pointer to a struct. +func toStructPointer(v reflect.Value) structPointer { + return structPointer{v} +} + +// IsNil reports whether p is nil. +func structPointer_IsNil(p structPointer) bool { + return p.v.IsNil() +} + +// Interface returns the struct pointer as an interface value. +func structPointer_Interface(p structPointer, _ reflect.Type) interface{} { + return p.v.Interface() +} + +// A field identifies a field in a struct, accessible from a structPointer. +// In this implementation, a field is identified by the sequence of field indices +// passed to reflect's FieldByIndex. +type field []int + +// toField returns a field equivalent to the given reflect field. +func toField(f *reflect.StructField) field { + return f.Index +} + +// invalidField is an invalid field identifier. +var invalidField = field(nil) + +// IsValid reports whether the field identifier is valid. +func (f field) IsValid() bool { return f != nil } + +// field returns the given field in the struct as a reflect value. +func structPointer_field(p structPointer, f field) reflect.Value { + // Special case: an extension map entry with a value of type T + // passes a *T to the struct-handling code with a zero field, + // expecting that it will be treated as equivalent to *struct{ X T }, + // which has the same memory layout. We have to handle that case + // specially, because reflect will panic if we call FieldByIndex on a + // non-struct. + if f == nil { + return p.v.Elem() + } + + return p.v.Elem().FieldByIndex(f) +} + +// ifield returns the given field in the struct as an interface value. +func structPointer_ifield(p structPointer, f field) interface{} { + return structPointer_field(p, f).Addr().Interface() +} + +// Bytes returns the address of a []byte field in the struct. +func structPointer_Bytes(p structPointer, f field) *[]byte { + return structPointer_ifield(p, f).(*[]byte) +} + +// BytesSlice returns the address of a [][]byte field in the struct. +func structPointer_BytesSlice(p structPointer, f field) *[][]byte { + return structPointer_ifield(p, f).(*[][]byte) +} + +// Bool returns the address of a *bool field in the struct. +func structPointer_Bool(p structPointer, f field) **bool { + return structPointer_ifield(p, f).(**bool) +} + +// BoolSlice returns the address of a []bool field in the struct. +func structPointer_BoolSlice(p structPointer, f field) *[]bool { + return structPointer_ifield(p, f).(*[]bool) +} + +// String returns the address of a *string field in the struct. +func structPointer_String(p structPointer, f field) **string { + return structPointer_ifield(p, f).(**string) +} + +// StringSlice returns the address of a []string field in the struct. +func structPointer_StringSlice(p structPointer, f field) *[]string { + return structPointer_ifield(p, f).(*[]string) +} + +// ExtMap returns the address of an extension map field in the struct. +func structPointer_ExtMap(p structPointer, f field) *map[int32]Extension { + return structPointer_ifield(p, f).(*map[int32]Extension) +} + +// SetStructPointer writes a *struct field in the struct. +func structPointer_SetStructPointer(p structPointer, f field, q structPointer) { + structPointer_field(p, f).Set(q.v) +} + +// GetStructPointer reads a *struct field in the struct. +func structPointer_GetStructPointer(p structPointer, f field) structPointer { + return structPointer{structPointer_field(p, f)} +} + +// StructPointerSlice the address of a []*struct field in the struct. +func structPointer_StructPointerSlice(p structPointer, f field) structPointerSlice { + return structPointerSlice{structPointer_field(p, f)} +} + +// A structPointerSlice represents the address of a slice of pointers to structs +// (themselves messages or groups). That is, v.Type() is *[]*struct{...}. +type structPointerSlice struct { + v reflect.Value +} + +func (p structPointerSlice) Len() int { return p.v.Len() } +func (p structPointerSlice) Index(i int) structPointer { return structPointer{p.v.Index(i)} } +func (p structPointerSlice) Append(q structPointer) { + p.v.Set(reflect.Append(p.v, q.v)) +} + +var ( + int32Type = reflect.TypeOf(int32(0)) + uint32Type = reflect.TypeOf(uint32(0)) + float32Type = reflect.TypeOf(float32(0)) + int64Type = reflect.TypeOf(int64(0)) + uint64Type = reflect.TypeOf(uint64(0)) + float64Type = reflect.TypeOf(float64(0)) +) + +// A word32 represents a field of type *int32, *uint32, *float32, or *enum. +// That is, v.Type() is *int32, *uint32, *float32, or *enum and v is assignable. +type word32 struct { + v reflect.Value +} + +// IsNil reports whether p is nil. +func word32_IsNil(p word32) bool { + return p.v.IsNil() +} + +// Set sets p to point at a newly allocated word with bits set to x. +func word32_Set(p word32, o *Buffer, x uint32) { + t := p.v.Type().Elem() + switch t { + case int32Type: + if len(o.int32s) == 0 { + o.int32s = make([]int32, uint32PoolSize) + } + o.int32s[0] = int32(x) + p.v.Set(reflect.ValueOf(&o.int32s[0])) + o.int32s = o.int32s[1:] + return + case uint32Type: + if len(o.uint32s) == 0 { + o.uint32s = make([]uint32, uint32PoolSize) + } + o.uint32s[0] = x + p.v.Set(reflect.ValueOf(&o.uint32s[0])) + o.uint32s = o.uint32s[1:] + return + case float32Type: + if len(o.float32s) == 0 { + o.float32s = make([]float32, uint32PoolSize) + } + o.float32s[0] = math.Float32frombits(x) + p.v.Set(reflect.ValueOf(&o.float32s[0])) + o.float32s = o.float32s[1:] + return + } + + // must be enum + p.v.Set(reflect.New(t)) + p.v.Elem().SetInt(int64(int32(x))) +} + +// Get gets the bits pointed at by p, as a uint32. +func word32_Get(p word32) uint32 { + elem := p.v.Elem() + switch elem.Kind() { + case reflect.Int32: + return uint32(elem.Int()) + case reflect.Uint32: + return uint32(elem.Uint()) + case reflect.Float32: + return math.Float32bits(float32(elem.Float())) + } + panic("unreachable") +} + +// Word32 returns a reference to a *int32, *uint32, *float32, or *enum field in the struct. +func structPointer_Word32(p structPointer, f field) word32 { + return word32{structPointer_field(p, f)} +} + +// A word32Slice is a slice of 32-bit values. +// That is, v.Type() is []int32, []uint32, []float32, or []enum. +type word32Slice struct { + v reflect.Value +} + +func (p word32Slice) Append(x uint32) { + n, m := p.v.Len(), p.v.Cap() + if n < m { + p.v.SetLen(n + 1) + } else { + t := p.v.Type().Elem() + p.v.Set(reflect.Append(p.v, reflect.Zero(t))) + } + elem := p.v.Index(n) + switch elem.Kind() { + case reflect.Int32: + elem.SetInt(int64(int32(x))) + case reflect.Uint32: + elem.SetUint(uint64(x)) + case reflect.Float32: + elem.SetFloat(float64(math.Float32frombits(x))) + } +} + +func (p word32Slice) Len() int { + return p.v.Len() +} + +func (p word32Slice) Index(i int) uint32 { + elem := p.v.Index(i) + switch elem.Kind() { + case reflect.Int32: + return uint32(elem.Int()) + case reflect.Uint32: + return uint32(elem.Uint()) + case reflect.Float32: + return math.Float32bits(float32(elem.Float())) + } + panic("unreachable") +} + +// Word32Slice returns a reference to a []int32, []uint32, []float32, or []enum field in the struct. +func structPointer_Word32Slice(p structPointer, f field) word32Slice { + return word32Slice{structPointer_field(p, f)} +} + +// word64 is like word32 but for 64-bit values. +type word64 struct { + v reflect.Value +} + +func word64_Set(p word64, o *Buffer, x uint64) { + t := p.v.Type().Elem() + switch t { + case int64Type: + if len(o.int64s) == 0 { + o.int64s = make([]int64, uint64PoolSize) + } + o.int64s[0] = int64(x) + p.v.Set(reflect.ValueOf(&o.int64s[0])) + o.int64s = o.int64s[1:] + return + case uint64Type: + if len(o.uint64s) == 0 { + o.uint64s = make([]uint64, uint64PoolSize) + } + o.uint64s[0] = x + p.v.Set(reflect.ValueOf(&o.uint64s[0])) + o.uint64s = o.uint64s[1:] + return + case float64Type: + if len(o.float64s) == 0 { + o.float64s = make([]float64, uint64PoolSize) + } + o.float64s[0] = math.Float64frombits(x) + p.v.Set(reflect.ValueOf(&o.float64s[0])) + o.float64s = o.float64s[1:] + return + } + panic("unreachable") +} + +func word64_IsNil(p word64) bool { + return p.v.IsNil() +} + +func word64_Get(p word64) uint64 { + elem := p.v.Elem() + switch elem.Kind() { + case reflect.Int64: + return uint64(elem.Int()) + case reflect.Uint64: + return elem.Uint() + case reflect.Float64: + return math.Float64bits(elem.Float()) + } + panic("unreachable") +} + +func structPointer_Word64(p structPointer, f field) word64 { + return word64{structPointer_field(p, f)} +} + +type word64Slice struct { + v reflect.Value +} + +func (p word64Slice) Append(x uint64) { + n, m := p.v.Len(), p.v.Cap() + if n < m { + p.v.SetLen(n + 1) + } else { + t := p.v.Type().Elem() + p.v.Set(reflect.Append(p.v, reflect.Zero(t))) + } + elem := p.v.Index(n) + switch elem.Kind() { + case reflect.Int64: + elem.SetInt(int64(int64(x))) + case reflect.Uint64: + elem.SetUint(uint64(x)) + case reflect.Float64: + elem.SetFloat(float64(math.Float64frombits(x))) + } +} + +func (p word64Slice) Len() int { + return p.v.Len() +} + +func (p word64Slice) Index(i int) uint64 { + elem := p.v.Index(i) + switch elem.Kind() { + case reflect.Int64: + return uint64(elem.Int()) + case reflect.Uint64: + return uint64(elem.Uint()) + case reflect.Float64: + return math.Float64bits(float64(elem.Float())) + } + panic("unreachable") +} + +func structPointer_Word64Slice(p structPointer, f field) word64Slice { + return word64Slice{structPointer_field(p, f)} +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe.go new file mode 100644 index 00000000000..27a536c88db --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe.go @@ -0,0 +1,218 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// +build !appengine appenginevm + +// This file contains the implementation of the proto field accesses using package unsafe. + +package proto + +import ( + "reflect" + "unsafe" +) + +// NOTE: These type_Foo functions would more idiomatically be methods, +// but Go does not allow methods on pointer types, and we must preserve +// some pointer type for the garbage collector. We use these +// funcs with clunky names as our poor approximation to methods. +// +// An alternative would be +// type structPointer struct { p unsafe.Pointer } +// but that does not registerize as well. + +// A structPointer is a pointer to a struct. +type structPointer unsafe.Pointer + +// toStructPointer returns a structPointer equivalent to the given reflect value. +func toStructPointer(v reflect.Value) structPointer { + return structPointer(unsafe.Pointer(v.Pointer())) +} + +// IsNil reports whether p is nil. +func structPointer_IsNil(p structPointer) bool { + return p == nil +} + +// Interface returns the struct pointer, assumed to have element type t, +// as an interface value. +func structPointer_Interface(p structPointer, t reflect.Type) interface{} { + return reflect.NewAt(t, unsafe.Pointer(p)).Interface() +} + +// A field identifies a field in a struct, accessible from a structPointer. +// In this implementation, a field is identified by its byte offset from the start of the struct. +type field uintptr + +// toField returns a field equivalent to the given reflect field. +func toField(f *reflect.StructField) field { + return field(f.Offset) +} + +// invalidField is an invalid field identifier. +const invalidField = ^field(0) + +// IsValid reports whether the field identifier is valid. +func (f field) IsValid() bool { + return f != ^field(0) +} + +// Bytes returns the address of a []byte field in the struct. +func structPointer_Bytes(p structPointer, f field) *[]byte { + return (*[]byte)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// BytesSlice returns the address of a [][]byte field in the struct. +func structPointer_BytesSlice(p structPointer, f field) *[][]byte { + return (*[][]byte)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// Bool returns the address of a *bool field in the struct. +func structPointer_Bool(p structPointer, f field) **bool { + return (**bool)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// BoolSlice returns the address of a []bool field in the struct. +func structPointer_BoolSlice(p structPointer, f field) *[]bool { + return (*[]bool)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// String returns the address of a *string field in the struct. +func structPointer_String(p structPointer, f field) **string { + return (**string)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// StringSlice returns the address of a []string field in the struct. +func structPointer_StringSlice(p structPointer, f field) *[]string { + return (*[]string)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// ExtMap returns the address of an extension map field in the struct. +func structPointer_ExtMap(p structPointer, f field) *map[int32]Extension { + return (*map[int32]Extension)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// SetStructPointer writes a *struct field in the struct. +func structPointer_SetStructPointer(p structPointer, f field, q structPointer) { + *(*structPointer)(unsafe.Pointer(uintptr(p) + uintptr(f))) = q +} + +// GetStructPointer reads a *struct field in the struct. +func structPointer_GetStructPointer(p structPointer, f field) structPointer { + return *(*structPointer)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// StructPointerSlice the address of a []*struct field in the struct. +func structPointer_StructPointerSlice(p structPointer, f field) *structPointerSlice { + return (*structPointerSlice)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// A structPointerSlice represents a slice of pointers to structs (themselves submessages or groups). +type structPointerSlice []structPointer + +func (v *structPointerSlice) Len() int { return len(*v) } +func (v *structPointerSlice) Index(i int) structPointer { return (*v)[i] } +func (v *structPointerSlice) Append(p structPointer) { *v = append(*v, p) } + +// A word32 is the address of a "pointer to 32-bit value" field. +type word32 **uint32 + +// IsNil reports whether *v is nil. +func word32_IsNil(p word32) bool { + return *p == nil +} + +// Set sets *v to point at a newly allocated word set to x. +func word32_Set(p word32, o *Buffer, x uint32) { + if len(o.uint32s) == 0 { + o.uint32s = make([]uint32, uint32PoolSize) + } + o.uint32s[0] = x + *p = &o.uint32s[0] + o.uint32s = o.uint32s[1:] +} + +// Get gets the value pointed at by *v. +func word32_Get(p word32) uint32 { + return **p +} + +// Word32 returns the address of a *int32, *uint32, *float32, or *enum field in the struct. +func structPointer_Word32(p structPointer, f field) word32 { + return word32((**uint32)(unsafe.Pointer(uintptr(p) + uintptr(f)))) +} + +// A word32Slice is a slice of 32-bit values. +type word32Slice []uint32 + +func (v *word32Slice) Append(x uint32) { *v = append(*v, x) } +func (v *word32Slice) Len() int { return len(*v) } +func (v *word32Slice) Index(i int) uint32 { return (*v)[i] } + +// Word32Slice returns the address of a []int32, []uint32, []float32, or []enum field in the struct. +func structPointer_Word32Slice(p structPointer, f field) *word32Slice { + return (*word32Slice)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// word64 is like word32 but for 64-bit values. +type word64 **uint64 + +func word64_Set(p word64, o *Buffer, x uint64) { + if len(o.uint64s) == 0 { + o.uint64s = make([]uint64, uint64PoolSize) + } + o.uint64s[0] = x + *p = &o.uint64s[0] + o.uint64s = o.uint64s[1:] +} + +func word64_IsNil(p word64) bool { + return *p == nil +} + +func word64_Get(p word64) uint64 { + return **p +} + +func structPointer_Word64(p structPointer, f field) word64 { + return word64((**uint64)(unsafe.Pointer(uintptr(p) + uintptr(f)))) +} + +// word64Slice is like word32Slice but for 64-bit values. +type word64Slice []uint64 + +func (v *word64Slice) Append(x uint64) { *v = append(*v, x) } +func (v *word64Slice) Len() int { return len(*v) } +func (v *word64Slice) Index(i int) uint64 { return (*v)[i] } + +func structPointer_Word64Slice(p structPointer, f field) *word64Slice { + return (*word64Slice)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe_gogo.go new file mode 100644 index 00000000000..befeeed6835 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/pointer_unsafe_gogo.go @@ -0,0 +1,166 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// +build !appengine + +// This file contains the implementation of the proto field accesses using package unsafe. + +package proto + +import ( + "reflect" + "unsafe" +) + +func structPointer_InterfaceAt(p structPointer, f field, t reflect.Type) interface{} { + point := unsafe.Pointer(uintptr(p) + uintptr(f)) + r := reflect.NewAt(t, point) + return r.Interface() +} + +func structPointer_InterfaceRef(p structPointer, f field, t reflect.Type) interface{} { + point := unsafe.Pointer(uintptr(p) + uintptr(f)) + r := reflect.NewAt(t, point) + if r.Elem().IsNil() { + return nil + } + return r.Elem().Interface() +} + +func copyUintPtr(oldptr, newptr uintptr, size int) { + oldbytes := make([]byte, 0) + oldslice := (*reflect.SliceHeader)(unsafe.Pointer(&oldbytes)) + oldslice.Data = oldptr + oldslice.Len = size + oldslice.Cap = size + newbytes := make([]byte, 0) + newslice := (*reflect.SliceHeader)(unsafe.Pointer(&newbytes)) + newslice.Data = newptr + newslice.Len = size + newslice.Cap = size + copy(newbytes, oldbytes) +} + +func structPointer_Copy(oldptr structPointer, newptr structPointer, size int) { + copyUintPtr(uintptr(oldptr), uintptr(newptr), size) +} + +func appendStructPointer(base structPointer, f field, typ reflect.Type) structPointer { + size := typ.Elem().Size() + oldHeader := structPointer_GetSliceHeader(base, f) + newLen := oldHeader.Len + 1 + slice := reflect.MakeSlice(typ, newLen, newLen) + bas := toStructPointer(slice) + for i := 0; i < oldHeader.Len; i++ { + newElemptr := uintptr(bas) + uintptr(i)*size + oldElemptr := oldHeader.Data + uintptr(i)*size + copyUintPtr(oldElemptr, newElemptr, int(size)) + } + + oldHeader.Data = uintptr(bas) + oldHeader.Len = newLen + oldHeader.Cap = newLen + + return structPointer(unsafe.Pointer(uintptr(unsafe.Pointer(bas)) + uintptr(uintptr(newLen-1)*size))) +} + +// RefBool returns a *bool field in the struct. +func structPointer_RefBool(p structPointer, f field) *bool { + return (*bool)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +// RefString returns the address of a string field in the struct. +func structPointer_RefString(p structPointer, f field) *string { + return (*string)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +func structPointer_FieldPointer(p structPointer, f field) structPointer { + return structPointer(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +func structPointer_GetRefStructPointer(p structPointer, f field) structPointer { + return structPointer((*structPointer)(unsafe.Pointer(uintptr(p) + uintptr(f)))) +} + +func structPointer_GetSliceHeader(p structPointer, f field) *reflect.SliceHeader { + return (*reflect.SliceHeader)(unsafe.Pointer(uintptr(p) + uintptr(f))) +} + +func structPointer_Add(p structPointer, size field) structPointer { + return structPointer(unsafe.Pointer(uintptr(p) + uintptr(size))) +} + +func structPointer_Len(p structPointer, f field) int { + return len(*(*[]interface{})(unsafe.Pointer(structPointer_GetRefStructPointer(p, f)))) +} + +// refWord32 is the address of a 32-bit value field. +type refWord32 *uint32 + +func refWord32_IsNil(p refWord32) bool { + return p == nil +} + +func refWord32_Set(p refWord32, o *Buffer, x uint32) { + if len(o.uint32s) == 0 { + o.uint32s = make([]uint32, uint32PoolSize) + } + o.uint32s[0] = x + *p = o.uint32s[0] + o.uint32s = o.uint32s[1:] +} + +func refWord32_Get(p refWord32) uint32 { + return *p +} + +func structPointer_RefWord32(p structPointer, f field) refWord32 { + return refWord32((*uint32)(unsafe.Pointer(uintptr(p) + uintptr(f)))) +} + +// refWord64 is like refWord32 but for 32-bit values. +type refWord64 *uint64 + +func refWord64_Set(p refWord64, o *Buffer, x uint64) { + if len(o.uint64s) == 0 { + o.uint64s = make([]uint64, uint64PoolSize) + } + o.uint64s[0] = x + *p = o.uint64s[0] + o.uint64s = o.uint64s[1:] +} + +func refWord64_IsNil(p refWord64) bool { + return p == nil +} + +func refWord64_Get(p refWord64) uint64 { + return *p +} + +func structPointer_RefWord64(p structPointer, f field) refWord64 { + return refWord64((*uint64)(unsafe.Pointer(uintptr(p) + uintptr(f)))) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties.go new file mode 100644 index 00000000000..d77530b7f9a --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties.go @@ -0,0 +1,670 @@ +// Extensions for Protocol Buffers to create more go like structures. +// +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for encoding data into the wire format for protocol buffers. + */ + +import ( + "fmt" + "os" + "reflect" + "sort" + "strconv" + "strings" + "sync" +) + +const debug bool = false + +// Constants that identify the encoding of a value on the wire. +const ( + WireVarint = 0 + WireFixed64 = 1 + WireBytes = 2 + WireStartGroup = 3 + WireEndGroup = 4 + WireFixed32 = 5 +) + +const startSize = 10 // initial slice/string sizes + +// Encoders are defined in encode.go +// An encoder outputs the full representation of a field, including its +// tag and encoder type. +type encoder func(p *Buffer, prop *Properties, base structPointer) error + +// A valueEncoder encodes a single integer in a particular encoding. +type valueEncoder func(o *Buffer, x uint64) error + +// Sizers are defined in encode.go +// A sizer returns the encoded size of a field, including its tag and encoder +// type. +type sizer func(prop *Properties, base structPointer) int + +// A valueSizer returns the encoded size of a single integer in a particular +// encoding. +type valueSizer func(x uint64) int + +// Decoders are defined in decode.go +// A decoder creates a value from its wire representation. +// Unrecognized subelements are saved in unrec. +type decoder func(p *Buffer, prop *Properties, base structPointer) error + +// A valueDecoder decodes a single integer in a particular encoding. +type valueDecoder func(o *Buffer) (x uint64, err error) + +// tagMap is an optimization over map[int]int for typical protocol buffer +// use-cases. Encoded protocol buffers are often in tag order with small tag +// numbers. +type tagMap struct { + fastTags []int + slowTags map[int]int +} + +// tagMapFastLimit is the upper bound on the tag number that will be stored in +// the tagMap slice rather than its map. +const tagMapFastLimit = 1024 + +func (p *tagMap) get(t int) (int, bool) { + if t > 0 && t < tagMapFastLimit { + if t >= len(p.fastTags) { + return 0, false + } + fi := p.fastTags[t] + return fi, fi >= 0 + } + fi, ok := p.slowTags[t] + return fi, ok +} + +func (p *tagMap) put(t int, fi int) { + if t > 0 && t < tagMapFastLimit { + for len(p.fastTags) < t+1 { + p.fastTags = append(p.fastTags, -1) + } + p.fastTags[t] = fi + return + } + if p.slowTags == nil { + p.slowTags = make(map[int]int) + } + p.slowTags[t] = fi +} + +// StructProperties represents properties for all the fields of a struct. +// decoderTags and decoderOrigNames should only be used by the decoder. +type StructProperties struct { + Prop []*Properties // properties for each field + reqCount int // required count + decoderTags tagMap // map from proto tag to struct field number + decoderOrigNames map[string]int // map from original name to struct field number + order []int // list of struct field numbers in tag order + unrecField field // field id of the XXX_unrecognized []byte field + extendable bool // is this an extendable proto +} + +// Implement the sorting interface so we can sort the fields in tag order, as recommended by the spec. +// See encode.go, (*Buffer).enc_struct. + +func (sp *StructProperties) Len() int { return len(sp.order) } +func (sp *StructProperties) Less(i, j int) bool { + return sp.Prop[sp.order[i]].Tag < sp.Prop[sp.order[j]].Tag +} +func (sp *StructProperties) Swap(i, j int) { sp.order[i], sp.order[j] = sp.order[j], sp.order[i] } + +// Properties represents the protocol-specific behavior of a single struct field. +type Properties struct { + Name string // name of the field, for error messages + OrigName string // original name before protocol compiler (always set) + Wire string + WireType int + Tag int + Required bool + Optional bool + Repeated bool + Packed bool // relevant for repeated primitives only + Enum string // set for enum types only + Default string // default value + CustomType string + def_uint64 uint64 + + enc encoder + valEnc valueEncoder // set for bool and numeric types only + field field + tagcode []byte // encoding of EncodeVarint((Tag<<3)|WireType) + tagbuf [8]byte + stype reflect.Type // set for struct types only + sstype reflect.Type // set for slices of structs types only + ctype reflect.Type // set for custom types only + sprop *StructProperties // set for struct types only + isMarshaler bool + isUnmarshaler bool + + size sizer + valSize valueSizer // set for bool and numeric types only + + dec decoder + valDec valueDecoder // set for bool and numeric types only + + // If this is a packable field, this will be the decoder for the packed version of the field. + packedDec decoder +} + +// String formats the properties in the protobuf struct field tag style. +func (p *Properties) String() string { + s := p.Wire + s = "," + s += strconv.Itoa(p.Tag) + if p.Required { + s += ",req" + } + if p.Optional { + s += ",opt" + } + if p.Repeated { + s += ",rep" + } + if p.Packed { + s += ",packed" + } + if p.OrigName != p.Name { + s += ",name=" + p.OrigName + } + if len(p.Enum) > 0 { + s += ",enum=" + p.Enum + } + if len(p.Default) > 0 { + s += ",def=" + p.Default + } + return s +} + +// Parse populates p by parsing a string in the protobuf struct field tag style. +func (p *Properties) Parse(s string) { + // "bytes,49,opt,name=foo,def=hello!" + fields := strings.Split(s, ",") // breaks def=, but handled below. + if len(fields) < 2 { + fmt.Fprintf(os.Stderr, "proto: tag has too few fields: %q\n", s) + return + } + + p.Wire = fields[0] + switch p.Wire { + case "varint": + p.WireType = WireVarint + p.valEnc = (*Buffer).EncodeVarint + p.valDec = (*Buffer).DecodeVarint + p.valSize = sizeVarint + case "fixed32": + p.WireType = WireFixed32 + p.valEnc = (*Buffer).EncodeFixed32 + p.valDec = (*Buffer).DecodeFixed32 + p.valSize = sizeFixed32 + case "fixed64": + p.WireType = WireFixed64 + p.valEnc = (*Buffer).EncodeFixed64 + p.valDec = (*Buffer).DecodeFixed64 + p.valSize = sizeFixed64 + case "zigzag32": + p.WireType = WireVarint + p.valEnc = (*Buffer).EncodeZigzag32 + p.valDec = (*Buffer).DecodeZigzag32 + p.valSize = sizeZigzag32 + case "zigzag64": + p.WireType = WireVarint + p.valEnc = (*Buffer).EncodeZigzag64 + p.valDec = (*Buffer).DecodeZigzag64 + p.valSize = sizeZigzag64 + case "bytes", "group": + p.WireType = WireBytes + // no numeric converter for non-numeric types + default: + fmt.Fprintf(os.Stderr, "proto: tag has unknown wire type: %q\n", s) + return + } + + var err error + p.Tag, err = strconv.Atoi(fields[1]) + if err != nil { + return + } + + for i := 2; i < len(fields); i++ { + f := fields[i] + switch { + case f == "req": + p.Required = true + case f == "opt": + p.Optional = true + case f == "rep": + p.Repeated = true + case f == "packed": + p.Packed = true + case strings.HasPrefix(f, "name="): + p.OrigName = f[5:] + case strings.HasPrefix(f, "enum="): + p.Enum = f[5:] + case strings.HasPrefix(f, "def="): + p.Default = f[4:] // rest of string + if i+1 < len(fields) { + // Commas aren't escaped, and def is always last. + p.Default += "," + strings.Join(fields[i+1:], ",") + break + } + case strings.HasPrefix(f, "embedded="): + p.OrigName = strings.Split(f, "=")[1] + case strings.HasPrefix(f, "customtype="): + p.CustomType = strings.Split(f, "=")[1] + } + } +} + +func logNoSliceEnc(t1, t2 reflect.Type) { + fmt.Fprintf(os.Stderr, "proto: no slice oenc for %T = []%T\n", t1, t2) +} + +var protoMessageType = reflect.TypeOf((*Message)(nil)).Elem() + +// Initialize the fields for encoding and decoding. +func (p *Properties) setEncAndDec(typ reflect.Type, lockGetProp bool) { + p.enc = nil + p.dec = nil + p.size = nil + if len(p.CustomType) > 0 { + p.setCustomEncAndDec(typ) + p.setTag(lockGetProp) + return + } + switch t1 := typ; t1.Kind() { + default: + if !p.setNonNullableEncAndDec(t1) { + fmt.Fprintf(os.Stderr, "proto: no coders for %T\n", t1) + } + case reflect.Ptr: + switch t2 := t1.Elem(); t2.Kind() { + default: + fmt.Fprintf(os.Stderr, "proto: no encoder function for %T -> %T\n", t1, t2) + break + case reflect.Bool: + p.enc = (*Buffer).enc_bool + p.dec = (*Buffer).dec_bool + p.size = size_bool + case reflect.Int32, reflect.Uint32: + p.enc = (*Buffer).enc_int32 + p.dec = (*Buffer).dec_int32 + p.size = size_int32 + case reflect.Int64, reflect.Uint64: + p.enc = (*Buffer).enc_int64 + p.dec = (*Buffer).dec_int64 + p.size = size_int64 + case reflect.Float32: + p.enc = (*Buffer).enc_int32 // can just treat them as bits + p.dec = (*Buffer).dec_int32 + p.size = size_int32 + case reflect.Float64: + p.enc = (*Buffer).enc_int64 // can just treat them as bits + p.dec = (*Buffer).dec_int64 + p.size = size_int64 + case reflect.String: + p.enc = (*Buffer).enc_string + p.dec = (*Buffer).dec_string + p.size = size_string + case reflect.Struct: + p.stype = t1.Elem() + p.isMarshaler = isMarshaler(t1) + p.isUnmarshaler = isUnmarshaler(t1) + if p.Wire == "bytes" { + p.enc = (*Buffer).enc_struct_message + p.dec = (*Buffer).dec_struct_message + p.size = size_struct_message + } else { + p.enc = (*Buffer).enc_struct_group + p.dec = (*Buffer).dec_struct_group + p.size = size_struct_group + } + } + + case reflect.Slice: + switch t2 := t1.Elem(); t2.Kind() { + default: + logNoSliceEnc(t1, t2) + break + case reflect.Bool: + if p.Packed { + p.enc = (*Buffer).enc_slice_packed_bool + p.size = size_slice_packed_bool + } else { + p.enc = (*Buffer).enc_slice_bool + p.size = size_slice_bool + } + p.dec = (*Buffer).dec_slice_bool + p.packedDec = (*Buffer).dec_slice_packed_bool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + switch t2.Bits() { + case 32: + if p.Packed { + p.enc = (*Buffer).enc_slice_packed_int32 + p.size = size_slice_packed_int32 + } else { + p.enc = (*Buffer).enc_slice_int32 + p.size = size_slice_int32 + } + p.dec = (*Buffer).dec_slice_int32 + p.packedDec = (*Buffer).dec_slice_packed_int32 + case 64: + if p.Packed { + p.enc = (*Buffer).enc_slice_packed_int64 + p.size = size_slice_packed_int64 + } else { + p.enc = (*Buffer).enc_slice_int64 + p.size = size_slice_int64 + } + p.dec = (*Buffer).dec_slice_int64 + p.packedDec = (*Buffer).dec_slice_packed_int64 + case 8: + if t2.Kind() == reflect.Uint8 { + p.enc = (*Buffer).enc_slice_byte + p.dec = (*Buffer).dec_slice_byte + p.size = size_slice_byte + } + default: + logNoSliceEnc(t1, t2) + break + } + case reflect.Float32, reflect.Float64: + switch t2.Bits() { + case 32: + // can just treat them as bits + if p.Packed { + p.enc = (*Buffer).enc_slice_packed_int32 + p.size = size_slice_packed_int32 + } else { + p.enc = (*Buffer).enc_slice_int32 + p.size = size_slice_int32 + } + p.dec = (*Buffer).dec_slice_int32 + p.packedDec = (*Buffer).dec_slice_packed_int32 + case 64: + // can just treat them as bits + if p.Packed { + p.enc = (*Buffer).enc_slice_packed_int64 + p.size = size_slice_packed_int64 + } else { + p.enc = (*Buffer).enc_slice_int64 + p.size = size_slice_int64 + } + p.dec = (*Buffer).dec_slice_int64 + p.packedDec = (*Buffer).dec_slice_packed_int64 + default: + logNoSliceEnc(t1, t2) + break + } + case reflect.String: + p.enc = (*Buffer).enc_slice_string + p.dec = (*Buffer).dec_slice_string + p.size = size_slice_string + case reflect.Ptr: + switch t3 := t2.Elem(); t3.Kind() { + default: + fmt.Fprintf(os.Stderr, "proto: no ptr oenc for %T -> %T -> %T\n", t1, t2, t3) + break + case reflect.Struct: + p.stype = t2.Elem() + p.isMarshaler = isMarshaler(t2) + p.isUnmarshaler = isUnmarshaler(t2) + if p.Wire == "bytes" { + p.enc = (*Buffer).enc_slice_struct_message + p.dec = (*Buffer).dec_slice_struct_message + p.size = size_slice_struct_message + } else { + p.enc = (*Buffer).enc_slice_struct_group + p.dec = (*Buffer).dec_slice_struct_group + p.size = size_slice_struct_group + } + } + case reflect.Slice: + switch t2.Elem().Kind() { + default: + fmt.Fprintf(os.Stderr, "proto: no slice elem oenc for %T -> %T -> %T\n", t1, t2, t2.Elem()) + break + case reflect.Uint8: + p.enc = (*Buffer).enc_slice_slice_byte + p.dec = (*Buffer).dec_slice_slice_byte + p.size = size_slice_slice_byte + } + case reflect.Struct: + p.setSliceOfNonPointerStructs(t1) + } + } + p.setTag(lockGetProp) +} + +func (p *Properties) setTag(lockGetProp bool) { + // precalculate tag code + wire := p.WireType + if p.Packed { + wire = WireBytes + } + x := uint32(p.Tag)<<3 | uint32(wire) + i := 0 + for i = 0; x > 127; i++ { + p.tagbuf[i] = 0x80 | uint8(x&0x7F) + x >>= 7 + } + p.tagbuf[i] = uint8(x) + p.tagcode = p.tagbuf[0 : i+1] + + if p.stype != nil { + if lockGetProp { + p.sprop = GetProperties(p.stype) + } else { + p.sprop = getPropertiesLocked(p.stype) + } + } +} + +var ( + marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() + unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem() +) + +// isMarshaler reports whether type t implements Marshaler. +func isMarshaler(t reflect.Type) bool { + return t.Implements(marshalerType) +} + +// isUnmarshaler reports whether type t implements Unmarshaler. +func isUnmarshaler(t reflect.Type) bool { + return t.Implements(unmarshalerType) +} + +// Init populates the properties from a protocol buffer struct tag. +func (p *Properties) Init(typ reflect.Type, name, tag string, f *reflect.StructField) { + p.init(typ, name, tag, f, true) +} + +func (p *Properties) init(typ reflect.Type, name, tag string, f *reflect.StructField, lockGetProp bool) { + // "bytes,49,opt,def=hello!" + p.Name = name + p.OrigName = name + if f != nil { + p.field = toField(f) + } + if tag == "" { + return + } + p.Parse(tag) + p.setEncAndDec(typ, lockGetProp) +} + +var ( + mutex sync.Mutex + propertiesMap = make(map[reflect.Type]*StructProperties) +) + +// GetProperties returns the list of properties for the type represented by t. +func GetProperties(t reflect.Type) *StructProperties { + mutex.Lock() + sprop := getPropertiesLocked(t) + mutex.Unlock() + return sprop +} + +// getPropertiesLocked requires that mutex is held. +func getPropertiesLocked(t reflect.Type) *StructProperties { + if prop, ok := propertiesMap[t]; ok { + if collectStats { + stats.Chit++ + } + return prop + } + if collectStats { + stats.Cmiss++ + } + + prop := new(StructProperties) + // in case of recursive protos, fill this in now. + propertiesMap[t] = prop + + // build properties + prop.extendable = reflect.PtrTo(t).Implements(extendableProtoType) + prop.unrecField = invalidField + prop.Prop = make([]*Properties, t.NumField()) + prop.order = make([]int, t.NumField()) + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + p := new(Properties) + name := f.Name + p.init(f.Type, name, f.Tag.Get("protobuf"), &f, false) + + if f.Name == "XXX_extensions" { // special case + if len(f.Tag.Get("protobuf")) > 0 { + p.enc = (*Buffer).enc_ext_slice_byte + p.dec = nil // not needed + p.size = size_ext_slice_byte + } else { + p.enc = (*Buffer).enc_map + p.dec = nil // not needed + p.size = size_map + } + } + if f.Name == "XXX_unrecognized" { // special case + prop.unrecField = toField(&f) + } + prop.Prop[i] = p + prop.order[i] = i + if debug { + print(i, " ", f.Name, " ", t.String(), " ") + if p.Tag > 0 { + print(p.String()) + } + print("\n") + } + if p.enc == nil && !strings.HasPrefix(f.Name, "XXX_") { + fmt.Fprintln(os.Stderr, "proto: no encoder for", f.Name, f.Type.String(), "[GetProperties]") + } + } + + // Re-order prop.order. + sort.Sort(prop) + + // build required counts + // build tags + reqCount := 0 + prop.decoderOrigNames = make(map[string]int) + for i, p := range prop.Prop { + if strings.HasPrefix(p.Name, "XXX_") { + // Internal fields should not appear in tags/origNames maps. + // They are handled specially when encoding and decoding. + continue + } + if p.Required { + reqCount++ + } + prop.decoderTags.put(p.Tag, i) + prop.decoderOrigNames[p.OrigName] = i + } + prop.reqCount = reqCount + + return prop +} + +// Return the Properties object for the x[0]'th field of the structure. +func propByIndex(t reflect.Type, x []int) *Properties { + if len(x) != 1 { + fmt.Fprintf(os.Stderr, "proto: field index dimension %d (not 1) for type %s\n", len(x), t) + return nil + } + prop := GetProperties(t) + return prop.Prop[x[0]] +} + +// Get the address and type of a pointer to a struct from an interface. +func getbase(pb Message) (t reflect.Type, b structPointer, err error) { + if pb == nil { + err = ErrNil + return + } + // get the reflect type of the pointer to the struct. + t = reflect.TypeOf(pb) + // get the address of the struct. + value := reflect.ValueOf(pb) + b = toStructPointer(value) + return +} + +// A global registry of enum types. +// The generated code will register the generated maps by calling RegisterEnum. + +var enumValueMaps = make(map[string]map[string]int32) +var enumStringMaps = make(map[string]map[int32]string) + +// RegisterEnum is called from the generated code to install the enum descriptor +// maps into the global table to aid parsing text format protocol buffers. +func RegisterEnum(typeName string, unusedNameMap map[int32]string, valueMap map[string]int32) { + if _, ok := enumValueMaps[typeName]; ok { + panic("proto: duplicate enum registered: " + typeName) + } + enumValueMaps[typeName] = valueMap + if _, ok := enumStringMaps[typeName]; ok { + panic("proto: duplicate enum registered: " + typeName) + } + enumStringMaps[typeName] = unusedNameMap +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties_gogo.go new file mode 100644 index 00000000000..08498e6dc28 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/properties_gogo.go @@ -0,0 +1,107 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "fmt" + "os" + "reflect" +) + +func (p *Properties) setCustomEncAndDec(typ reflect.Type) { + p.ctype = typ + if p.Repeated { + p.enc = (*Buffer).enc_custom_slice_bytes + p.dec = (*Buffer).dec_custom_slice_bytes + p.size = size_custom_slice_bytes + } else if typ.Kind() == reflect.Ptr { + p.enc = (*Buffer).enc_custom_bytes + p.dec = (*Buffer).dec_custom_bytes + p.size = size_custom_bytes + } else { + p.enc = (*Buffer).enc_custom_ref_bytes + p.dec = (*Buffer).dec_custom_ref_bytes + p.size = size_custom_ref_bytes + } +} + +func (p *Properties) setNonNullableEncAndDec(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Bool: + p.enc = (*Buffer).enc_ref_bool + p.dec = (*Buffer).dec_ref_bool + p.size = size_ref_bool + case reflect.Int32, reflect.Uint32: + p.enc = (*Buffer).enc_ref_int32 + p.dec = (*Buffer).dec_ref_int32 + p.size = size_ref_int32 + case reflect.Int64, reflect.Uint64: + p.enc = (*Buffer).enc_ref_int64 + p.dec = (*Buffer).dec_ref_int64 + p.size = size_ref_int64 + case reflect.Float32: + p.enc = (*Buffer).enc_ref_int32 // can just treat them as bits + p.dec = (*Buffer).dec_ref_int32 + p.size = size_ref_int32 + case reflect.Float64: + p.enc = (*Buffer).enc_ref_int64 // can just treat them as bits + p.dec = (*Buffer).dec_ref_int64 + p.size = size_ref_int64 + case reflect.String: + p.dec = (*Buffer).dec_ref_string + p.enc = (*Buffer).enc_ref_string + p.size = size_ref_string + case reflect.Struct: + p.stype = typ + p.isMarshaler = isMarshaler(typ) + p.isUnmarshaler = isUnmarshaler(typ) + if p.Wire == "bytes" { + p.enc = (*Buffer).enc_ref_struct_message + p.dec = (*Buffer).dec_ref_struct_message + p.size = size_ref_struct_message + } else { + fmt.Fprintf(os.Stderr, "proto: no coders for struct %T\n", typ) + } + default: + return false + } + return true +} + +func (p *Properties) setSliceOfNonPointerStructs(typ reflect.Type) { + t2 := typ.Elem() + p.sstype = typ + p.stype = t2 + p.isMarshaler = isMarshaler(t2) + p.isUnmarshaler = isUnmarshaler(t2) + p.enc = (*Buffer).enc_slice_ref_struct_message + p.dec = (*Buffer).dec_slice_ref_struct_message + p.size = size_slice_ref_struct_message + if p.Wire != "bytes" { + fmt.Fprintf(os.Stderr, "proto: no ptr oenc for %T -> %T \n", typ, t2) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size2_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size2_test.go new file mode 100644 index 00000000000..55902a4a9cb --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size2_test.go @@ -0,0 +1,63 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "testing" +) + +// This is a separate file and package from size_test.go because that one uses +// generated messages and thus may not be in package proto without having a circular +// dependency, whereas this file tests unexported details of size.go. + +func TestVarintSize(t *testing.T) { + // Check the edge cases carefully. + testCases := []struct { + n uint64 + size int + }{ + {0, 1}, + {1, 1}, + {127, 1}, + {128, 2}, + {16383, 2}, + {16384, 3}, + {1<<63 - 1, 9}, + {1 << 63, 10}, + } + for _, tc := range testCases { + size := sizeVarint(tc.n) + if size != tc.size { + t.Errorf("sizeVarint(%d) = %d, want %d", tc.n, size, tc.size) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size_test.go new file mode 100644 index 00000000000..484ed66a3d3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/size_test.go @@ -0,0 +1,118 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "log" + "testing" + + pb "./testdata" + . "code.google.com/p/gogoprotobuf/proto" +) + +var messageWithExtension1 = &pb.MyMessage{Count: Int32(7)} + +// messageWithExtension2 is in equal_test.go. +var messageWithExtension3 = &pb.MyMessage{Count: Int32(8)} + +func init() { + if err := SetExtension(messageWithExtension1, pb.E_Ext_More, &pb.Ext{Data: String("Abbott")}); err != nil { + log.Panicf("SetExtension: %v", err) + } + if err := SetExtension(messageWithExtension3, pb.E_Ext_More, &pb.Ext{Data: String("Costello")}); err != nil { + log.Panicf("SetExtension: %v", err) + } + + // Force messageWithExtension3 to have the extension encoded. + Marshal(messageWithExtension3) + +} + +var SizeTests = []struct { + desc string + pb Message +}{ + {"empty", &pb.OtherMessage{}}, + // Basic types. + {"bool", &pb.Defaults{F_Bool: Bool(true)}}, + {"int32", &pb.Defaults{F_Int32: Int32(12)}}, + {"small int64", &pb.Defaults{F_Int64: Int64(1)}}, + {"big int64", &pb.Defaults{F_Int64: Int64(1 << 20)}}, + {"fixed32", &pb.Defaults{F_Fixed32: Uint32(71)}}, + {"fixed64", &pb.Defaults{F_Fixed64: Uint64(72)}}, + {"uint32", &pb.Defaults{F_Uint32: Uint32(123)}}, + {"uint64", &pb.Defaults{F_Uint64: Uint64(124)}}, + {"float", &pb.Defaults{F_Float: Float32(12.6)}}, + {"double", &pb.Defaults{F_Double: Float64(13.9)}}, + {"string", &pb.Defaults{F_String: String("niles")}}, + {"bytes", &pb.Defaults{F_Bytes: []byte("wowsa")}}, + {"bytes, empty", &pb.Defaults{F_Bytes: []byte{}}}, + {"sint32", &pb.Defaults{F_Sint32: Int32(65)}}, + {"sint64", &pb.Defaults{F_Sint64: Int64(67)}}, + {"enum", &pb.Defaults{F_Enum: pb.Defaults_BLUE.Enum()}}, + // Repeated. + {"empty repeated bool", &pb.MoreRepeated{Bools: []bool{}}}, + {"repeated bool", &pb.MoreRepeated{Bools: []bool{false, true, true, false}}}, + {"packed repeated bool", &pb.MoreRepeated{BoolsPacked: []bool{false, true, true, false, true, true, true}}}, + {"repeated int32", &pb.MoreRepeated{Ints: []int32{1, 12203, 1729}}}, + {"repeated int32 packed", &pb.MoreRepeated{IntsPacked: []int32{1, 12203, 1729}}}, + {"repeated int64 packed", &pb.MoreRepeated{Int64SPacked: []int64{ + // Need enough large numbers to verify that the header is counting the number of bytes + // for the field, not the number of elements. + 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, + 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, 1 << 62, + }}}, + {"repeated string", &pb.MoreRepeated{Strings: []string{"r", "ken", "gri"}}}, + {"repeated fixed", &pb.MoreRepeated{Fixeds: []uint32{1, 2, 3, 4}}}, + // Nested. + {"nested", &pb.OldMessage{Nested: &pb.OldMessage_Nested{Name: String("whatever")}}}, + {"group", &pb.GroupOld{G: &pb.GroupOld_G{X: Int32(12345)}}}, + // Other things. + {"unrecognized", &pb.MoreRepeated{XXX_unrecognized: []byte{13<<3 | 0, 4}}}, + {"extension (unencoded)", messageWithExtension1}, + {"extension (encoded)", messageWithExtension3}, +} + +func TestSize(t *testing.T) { + for _, tc := range SizeTests { + size := Size(tc.pb) + b, err := Marshal(tc.pb) + if err != nil { + t.Errorf("%v: Marshal failed: %v", tc.desc, err) + continue + } + if size != len(b) { + t.Errorf("%v: Size(%v) = %d, want %d", tc.desc, tc.pb, size, len(b)) + t.Logf("%v: bytes: %#v", tc.desc, b) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/skip_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/skip_gogo.go new file mode 100644 index 00000000000..31010d5c019 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/skip_gogo.go @@ -0,0 +1,117 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "fmt" + "io" +) + +func Skip(data []byte) (n int, err error) { + l := len(data) + index := 0 + for index < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if index >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[index] + index++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for { + if index >= l { + return 0, io.ErrUnexpectedEOF + } + index++ + if data[index-1] < 0x80 { + break + } + } + return index, nil + case 1: + index += 8 + return index, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if index >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[index] + index++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + index += length + return index, nil + case 3: + for { + var wire uint64 + var start int = index + for shift := uint(0); ; shift += 7 { + if index >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[index] + index++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + if wireType == 4 { + break + } + next, err := Skip(data[start:]) + if err != nil { + return 0, err + } + index = start + next + } + return index, nil + case 4: + return index, nil + case 5: + index += 4 + return index, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/Makefile b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/Makefile new file mode 100644 index 00000000000..4cdf08456c3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/Makefile @@ -0,0 +1,47 @@ +# Go support for Protocol Buffers - Google's data interchange format +# +# Copyright 2010 The Go Authors. All rights reserved. +# http://code.google.com/p/goprotobuf/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +all: regenerate + +regenerate: + rm -f test.pb.go + protoc --gogo_out=. test.proto + +# The following rules are just aids to development. Not needed for typical testing. + +diff: regenerate + hg diff test.pb.go + +restore: + cp test.pb.go.golden test.pb.go + +preserve: + cp test.pb.go test.pb.go.golden diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/golden_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/golden_test.go new file mode 100644 index 00000000000..5a8f7ef3d6c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/golden_test.go @@ -0,0 +1,86 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Verify that the compiler output for test.proto is unchanged. + +package testdata + +import ( + "crypto/sha1" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" +) + +// sum returns in string form (for easy comparison) the SHA-1 hash of the named file. +func sum(t *testing.T, name string) string { + data, err := ioutil.ReadFile(name) + if err != nil { + t.Fatal(err) + } + t.Logf("sum(%q): length is %d", name, len(data)) + hash := sha1.New() + _, err = hash.Write(data) + if err != nil { + t.Fatal(err) + } + return fmt.Sprintf("% x", hash.Sum(nil)) +} + +func run(t *testing.T, name string, args ...string) { + cmd := exec.Command(name, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + t.Fatal(err) + } +} + +func TestGolden(t *testing.T) { + // Compute the original checksum. + goldenSum := sum(t, "test.pb.go") + // Run the proto compiler. + run(t, "protoc", "--gogo_out="+os.TempDir(), "test.proto") + newFile := filepath.Join(os.TempDir(), "test.pb.go") + defer os.Remove(newFile) + // Compute the new checksum. + newSum := sum(t, newFile) + // Verify + if newSum != goldenSum { + run(t, "diff", "-u", "test.pb.go", newFile) + t.Fatal("Code generated by protoc-gen-go has changed; update test.pb.go") + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go new file mode 100644 index 00000000000..192dd3747fd --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go @@ -0,0 +1,2324 @@ +// Code generated by protoc-gen-gogo. +// source: test.proto +// DO NOT EDIT! + +/* +Package testdata is a generated protocol buffer package. + +It is generated from these files: + test.proto + +It has these top-level messages: + GoEnum + GoTestField + GoTest + GoSkipTest + NonPackedTest + PackedTest + MaxTag + OldMessage + NewMessage + InnerMessage + OtherMessage + MyMessage + Ext + MyMessageSet + Empty + MessageList + Strings + Defaults + SubDefaults + RepeatedEnum + MoreRepeated + GroupOld + GroupNew + FloatingPoint +*/ +package testdata + +import proto "code.google.com/p/gogoprotobuf/proto" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = math.Inf + +type FOO int32 + +const ( + FOO_FOO1 FOO = 1 +) + +var FOO_name = map[int32]string{ + 1: "FOO1", +} +var FOO_value = map[string]int32{ + "FOO1": 1, +} + +func (x FOO) Enum() *FOO { + p := new(FOO) + *p = x + return p +} +func (x FOO) String() string { + return proto.EnumName(FOO_name, int32(x)) +} +func (x *FOO) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO") + if err != nil { + return err + } + *x = FOO(value) + return nil +} + +// An enum, for completeness. +type GoTest_KIND int32 + +const ( + GoTest_VOID GoTest_KIND = 0 + // Basic types + GoTest_BOOL GoTest_KIND = 1 + GoTest_BYTES GoTest_KIND = 2 + GoTest_FINGERPRINT GoTest_KIND = 3 + GoTest_FLOAT GoTest_KIND = 4 + GoTest_INT GoTest_KIND = 5 + GoTest_STRING GoTest_KIND = 6 + GoTest_TIME GoTest_KIND = 7 + // Groupings + GoTest_TUPLE GoTest_KIND = 8 + GoTest_ARRAY GoTest_KIND = 9 + GoTest_MAP GoTest_KIND = 10 + // Table types + GoTest_TABLE GoTest_KIND = 11 + // Functions + GoTest_FUNCTION GoTest_KIND = 12 +) + +var GoTest_KIND_name = map[int32]string{ + 0: "VOID", + 1: "BOOL", + 2: "BYTES", + 3: "FINGERPRINT", + 4: "FLOAT", + 5: "INT", + 6: "STRING", + 7: "TIME", + 8: "TUPLE", + 9: "ARRAY", + 10: "MAP", + 11: "TABLE", + 12: "FUNCTION", +} +var GoTest_KIND_value = map[string]int32{ + "VOID": 0, + "BOOL": 1, + "BYTES": 2, + "FINGERPRINT": 3, + "FLOAT": 4, + "INT": 5, + "STRING": 6, + "TIME": 7, + "TUPLE": 8, + "ARRAY": 9, + "MAP": 10, + "TABLE": 11, + "FUNCTION": 12, +} + +func (x GoTest_KIND) Enum() *GoTest_KIND { + p := new(GoTest_KIND) + *p = x + return p +} +func (x GoTest_KIND) String() string { + return proto.EnumName(GoTest_KIND_name, int32(x)) +} +func (x *GoTest_KIND) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(GoTest_KIND_value, data, "GoTest_KIND") + if err != nil { + return err + } + *x = GoTest_KIND(value) + return nil +} + +type MyMessage_Color int32 + +const ( + MyMessage_RED MyMessage_Color = 0 + MyMessage_GREEN MyMessage_Color = 1 + MyMessage_BLUE MyMessage_Color = 2 +) + +var MyMessage_Color_name = map[int32]string{ + 0: "RED", + 1: "GREEN", + 2: "BLUE", +} +var MyMessage_Color_value = map[string]int32{ + "RED": 0, + "GREEN": 1, + "BLUE": 2, +} + +func (x MyMessage_Color) Enum() *MyMessage_Color { + p := new(MyMessage_Color) + *p = x + return p +} +func (x MyMessage_Color) String() string { + return proto.EnumName(MyMessage_Color_name, int32(x)) +} +func (x *MyMessage_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(MyMessage_Color_value, data, "MyMessage_Color") + if err != nil { + return err + } + *x = MyMessage_Color(value) + return nil +} + +type Defaults_Color int32 + +const ( + Defaults_RED Defaults_Color = 0 + Defaults_GREEN Defaults_Color = 1 + Defaults_BLUE Defaults_Color = 2 +) + +var Defaults_Color_name = map[int32]string{ + 0: "RED", + 1: "GREEN", + 2: "BLUE", +} +var Defaults_Color_value = map[string]int32{ + "RED": 0, + "GREEN": 1, + "BLUE": 2, +} + +func (x Defaults_Color) Enum() *Defaults_Color { + p := new(Defaults_Color) + *p = x + return p +} +func (x Defaults_Color) String() string { + return proto.EnumName(Defaults_Color_name, int32(x)) +} +func (x *Defaults_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Defaults_Color_value, data, "Defaults_Color") + if err != nil { + return err + } + *x = Defaults_Color(value) + return nil +} + +type RepeatedEnum_Color int32 + +const ( + RepeatedEnum_RED RepeatedEnum_Color = 1 +) + +var RepeatedEnum_Color_name = map[int32]string{ + 1: "RED", +} +var RepeatedEnum_Color_value = map[string]int32{ + "RED": 1, +} + +func (x RepeatedEnum_Color) Enum() *RepeatedEnum_Color { + p := new(RepeatedEnum_Color) + *p = x + return p +} +func (x RepeatedEnum_Color) String() string { + return proto.EnumName(RepeatedEnum_Color_name, int32(x)) +} +func (x *RepeatedEnum_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(RepeatedEnum_Color_value, data, "RepeatedEnum_Color") + if err != nil { + return err + } + *x = RepeatedEnum_Color(value) + return nil +} + +type GoEnum struct { + Foo *FOO `protobuf:"varint,1,req,name=foo,enum=testdata.FOO" json:"foo,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoEnum) Reset() { *m = GoEnum{} } +func (m *GoEnum) String() string { return proto.CompactTextString(m) } +func (*GoEnum) ProtoMessage() {} + +func (m *GoEnum) GetFoo() FOO { + if m != nil && m.Foo != nil { + return *m.Foo + } + return FOO_FOO1 +} + +type GoTestField struct { + Label *string `protobuf:"bytes,1,req" json:"Label,omitempty"` + Type *string `protobuf:"bytes,2,req" json:"Type,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTestField) Reset() { *m = GoTestField{} } +func (m *GoTestField) String() string { return proto.CompactTextString(m) } +func (*GoTestField) ProtoMessage() {} + +func (m *GoTestField) GetLabel() string { + if m != nil && m.Label != nil { + return *m.Label + } + return "" +} + +func (m *GoTestField) GetType() string { + if m != nil && m.Type != nil { + return *m.Type + } + return "" +} + +type GoTest struct { + // Some typical parameters + Kind *GoTest_KIND `protobuf:"varint,1,req,enum=testdata.GoTest_KIND" json:"Kind,omitempty"` + Table *string `protobuf:"bytes,2,opt" json:"Table,omitempty"` + Param *int32 `protobuf:"varint,3,opt" json:"Param,omitempty"` + // Required, repeated and optional foreign fields. + RequiredField *GoTestField `protobuf:"bytes,4,req" json:"RequiredField,omitempty"` + RepeatedField []*GoTestField `protobuf:"bytes,5,rep" json:"RepeatedField,omitempty"` + OptionalField *GoTestField `protobuf:"bytes,6,opt" json:"OptionalField,omitempty"` + // Required fields of all basic types + F_BoolRequired *bool `protobuf:"varint,10,req,name=F_Bool_required" json:"F_Bool_required,omitempty"` + F_Int32Required *int32 `protobuf:"varint,11,req,name=F_Int32_required" json:"F_Int32_required,omitempty"` + F_Int64Required *int64 `protobuf:"varint,12,req,name=F_Int64_required" json:"F_Int64_required,omitempty"` + F_Fixed32Required *uint32 `protobuf:"fixed32,13,req,name=F_Fixed32_required" json:"F_Fixed32_required,omitempty"` + F_Fixed64Required *uint64 `protobuf:"fixed64,14,req,name=F_Fixed64_required" json:"F_Fixed64_required,omitempty"` + F_Uint32Required *uint32 `protobuf:"varint,15,req,name=F_Uint32_required" json:"F_Uint32_required,omitempty"` + F_Uint64Required *uint64 `protobuf:"varint,16,req,name=F_Uint64_required" json:"F_Uint64_required,omitempty"` + F_FloatRequired *float32 `protobuf:"fixed32,17,req,name=F_Float_required" json:"F_Float_required,omitempty"` + F_DoubleRequired *float64 `protobuf:"fixed64,18,req,name=F_Double_required" json:"F_Double_required,omitempty"` + F_StringRequired *string `protobuf:"bytes,19,req,name=F_String_required" json:"F_String_required,omitempty"` + F_BytesRequired []byte `protobuf:"bytes,101,req,name=F_Bytes_required" json:"F_Bytes_required,omitempty"` + F_Sint32Required *int32 `protobuf:"zigzag32,102,req,name=F_Sint32_required" json:"F_Sint32_required,omitempty"` + F_Sint64Required *int64 `protobuf:"zigzag64,103,req,name=F_Sint64_required" json:"F_Sint64_required,omitempty"` + // Repeated fields of all basic types + F_BoolRepeated []bool `protobuf:"varint,20,rep,name=F_Bool_repeated" json:"F_Bool_repeated,omitempty"` + F_Int32Repeated []int32 `protobuf:"varint,21,rep,name=F_Int32_repeated" json:"F_Int32_repeated,omitempty"` + F_Int64Repeated []int64 `protobuf:"varint,22,rep,name=F_Int64_repeated" json:"F_Int64_repeated,omitempty"` + F_Fixed32Repeated []uint32 `protobuf:"fixed32,23,rep,name=F_Fixed32_repeated" json:"F_Fixed32_repeated,omitempty"` + F_Fixed64Repeated []uint64 `protobuf:"fixed64,24,rep,name=F_Fixed64_repeated" json:"F_Fixed64_repeated,omitempty"` + F_Uint32Repeated []uint32 `protobuf:"varint,25,rep,name=F_Uint32_repeated" json:"F_Uint32_repeated,omitempty"` + F_Uint64Repeated []uint64 `protobuf:"varint,26,rep,name=F_Uint64_repeated" json:"F_Uint64_repeated,omitempty"` + F_FloatRepeated []float32 `protobuf:"fixed32,27,rep,name=F_Float_repeated" json:"F_Float_repeated,omitempty"` + F_DoubleRepeated []float64 `protobuf:"fixed64,28,rep,name=F_Double_repeated" json:"F_Double_repeated,omitempty"` + F_StringRepeated []string `protobuf:"bytes,29,rep,name=F_String_repeated" json:"F_String_repeated,omitempty"` + F_BytesRepeated [][]byte `protobuf:"bytes,201,rep,name=F_Bytes_repeated" json:"F_Bytes_repeated,omitempty"` + F_Sint32Repeated []int32 `protobuf:"zigzag32,202,rep,name=F_Sint32_repeated" json:"F_Sint32_repeated,omitempty"` + F_Sint64Repeated []int64 `protobuf:"zigzag64,203,rep,name=F_Sint64_repeated" json:"F_Sint64_repeated,omitempty"` + // Optional fields of all basic types + F_BoolOptional *bool `protobuf:"varint,30,opt,name=F_Bool_optional" json:"F_Bool_optional,omitempty"` + F_Int32Optional *int32 `protobuf:"varint,31,opt,name=F_Int32_optional" json:"F_Int32_optional,omitempty"` + F_Int64Optional *int64 `protobuf:"varint,32,opt,name=F_Int64_optional" json:"F_Int64_optional,omitempty"` + F_Fixed32Optional *uint32 `protobuf:"fixed32,33,opt,name=F_Fixed32_optional" json:"F_Fixed32_optional,omitempty"` + F_Fixed64Optional *uint64 `protobuf:"fixed64,34,opt,name=F_Fixed64_optional" json:"F_Fixed64_optional,omitempty"` + F_Uint32Optional *uint32 `protobuf:"varint,35,opt,name=F_Uint32_optional" json:"F_Uint32_optional,omitempty"` + F_Uint64Optional *uint64 `protobuf:"varint,36,opt,name=F_Uint64_optional" json:"F_Uint64_optional,omitempty"` + F_FloatOptional *float32 `protobuf:"fixed32,37,opt,name=F_Float_optional" json:"F_Float_optional,omitempty"` + F_DoubleOptional *float64 `protobuf:"fixed64,38,opt,name=F_Double_optional" json:"F_Double_optional,omitempty"` + F_StringOptional *string `protobuf:"bytes,39,opt,name=F_String_optional" json:"F_String_optional,omitempty"` + F_BytesOptional []byte `protobuf:"bytes,301,opt,name=F_Bytes_optional" json:"F_Bytes_optional,omitempty"` + F_Sint32Optional *int32 `protobuf:"zigzag32,302,opt,name=F_Sint32_optional" json:"F_Sint32_optional,omitempty"` + F_Sint64Optional *int64 `protobuf:"zigzag64,303,opt,name=F_Sint64_optional" json:"F_Sint64_optional,omitempty"` + // Default-valued fields of all basic types + F_BoolDefaulted *bool `protobuf:"varint,40,opt,name=F_Bool_defaulted,def=1" json:"F_Bool_defaulted,omitempty"` + F_Int32Defaulted *int32 `protobuf:"varint,41,opt,name=F_Int32_defaulted,def=32" json:"F_Int32_defaulted,omitempty"` + F_Int64Defaulted *int64 `protobuf:"varint,42,opt,name=F_Int64_defaulted,def=64" json:"F_Int64_defaulted,omitempty"` + F_Fixed32Defaulted *uint32 `protobuf:"fixed32,43,opt,name=F_Fixed32_defaulted,def=320" json:"F_Fixed32_defaulted,omitempty"` + F_Fixed64Defaulted *uint64 `protobuf:"fixed64,44,opt,name=F_Fixed64_defaulted,def=640" json:"F_Fixed64_defaulted,omitempty"` + F_Uint32Defaulted *uint32 `protobuf:"varint,45,opt,name=F_Uint32_defaulted,def=3200" json:"F_Uint32_defaulted,omitempty"` + F_Uint64Defaulted *uint64 `protobuf:"varint,46,opt,name=F_Uint64_defaulted,def=6400" json:"F_Uint64_defaulted,omitempty"` + F_FloatDefaulted *float32 `protobuf:"fixed32,47,opt,name=F_Float_defaulted,def=314159" json:"F_Float_defaulted,omitempty"` + F_DoubleDefaulted *float64 `protobuf:"fixed64,48,opt,name=F_Double_defaulted,def=271828" json:"F_Double_defaulted,omitempty"` + F_StringDefaulted *string `protobuf:"bytes,49,opt,name=F_String_defaulted,def=hello, \"world!\"\n" json:"F_String_defaulted,omitempty"` + F_BytesDefaulted []byte `protobuf:"bytes,401,opt,name=F_Bytes_defaulted,def=Bignose" json:"F_Bytes_defaulted,omitempty"` + F_Sint32Defaulted *int32 `protobuf:"zigzag32,402,opt,name=F_Sint32_defaulted,def=-32" json:"F_Sint32_defaulted,omitempty"` + F_Sint64Defaulted *int64 `protobuf:"zigzag64,403,opt,name=F_Sint64_defaulted,def=-64" json:"F_Sint64_defaulted,omitempty"` + // Packed repeated fields (no string or bytes). + F_BoolRepeatedPacked []bool `protobuf:"varint,50,rep,packed,name=F_Bool_repeated_packed" json:"F_Bool_repeated_packed,omitempty"` + F_Int32RepeatedPacked []int32 `protobuf:"varint,51,rep,packed,name=F_Int32_repeated_packed" json:"F_Int32_repeated_packed,omitempty"` + F_Int64RepeatedPacked []int64 `protobuf:"varint,52,rep,packed,name=F_Int64_repeated_packed" json:"F_Int64_repeated_packed,omitempty"` + F_Fixed32RepeatedPacked []uint32 `protobuf:"fixed32,53,rep,packed,name=F_Fixed32_repeated_packed" json:"F_Fixed32_repeated_packed,omitempty"` + F_Fixed64RepeatedPacked []uint64 `protobuf:"fixed64,54,rep,packed,name=F_Fixed64_repeated_packed" json:"F_Fixed64_repeated_packed,omitempty"` + F_Uint32RepeatedPacked []uint32 `protobuf:"varint,55,rep,packed,name=F_Uint32_repeated_packed" json:"F_Uint32_repeated_packed,omitempty"` + F_Uint64RepeatedPacked []uint64 `protobuf:"varint,56,rep,packed,name=F_Uint64_repeated_packed" json:"F_Uint64_repeated_packed,omitempty"` + F_FloatRepeatedPacked []float32 `protobuf:"fixed32,57,rep,packed,name=F_Float_repeated_packed" json:"F_Float_repeated_packed,omitempty"` + F_DoubleRepeatedPacked []float64 `protobuf:"fixed64,58,rep,packed,name=F_Double_repeated_packed" json:"F_Double_repeated_packed,omitempty"` + F_Sint32RepeatedPacked []int32 `protobuf:"zigzag32,502,rep,packed,name=F_Sint32_repeated_packed" json:"F_Sint32_repeated_packed,omitempty"` + F_Sint64RepeatedPacked []int64 `protobuf:"zigzag64,503,rep,packed,name=F_Sint64_repeated_packed" json:"F_Sint64_repeated_packed,omitempty"` + Requiredgroup *GoTest_RequiredGroup `protobuf:"group,70,req,name=RequiredGroup" json:"requiredgroup,omitempty"` + Repeatedgroup []*GoTest_RepeatedGroup `protobuf:"group,80,rep,name=RepeatedGroup" json:"repeatedgroup,omitempty"` + Optionalgroup *GoTest_OptionalGroup `protobuf:"group,90,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest) Reset() { *m = GoTest{} } +func (m *GoTest) String() string { return proto.CompactTextString(m) } +func (*GoTest) ProtoMessage() {} + +const Default_GoTest_F_BoolDefaulted bool = true +const Default_GoTest_F_Int32Defaulted int32 = 32 +const Default_GoTest_F_Int64Defaulted int64 = 64 +const Default_GoTest_F_Fixed32Defaulted uint32 = 320 +const Default_GoTest_F_Fixed64Defaulted uint64 = 640 +const Default_GoTest_F_Uint32Defaulted uint32 = 3200 +const Default_GoTest_F_Uint64Defaulted uint64 = 6400 +const Default_GoTest_F_FloatDefaulted float32 = 314159 +const Default_GoTest_F_DoubleDefaulted float64 = 271828 +const Default_GoTest_F_StringDefaulted string = "hello, \"world!\"\n" + +var Default_GoTest_F_BytesDefaulted []byte = []byte("Bignose") + +const Default_GoTest_F_Sint32Defaulted int32 = -32 +const Default_GoTest_F_Sint64Defaulted int64 = -64 + +func (m *GoTest) GetKind() GoTest_KIND { + if m != nil && m.Kind != nil { + return *m.Kind + } + return GoTest_VOID +} + +func (m *GoTest) GetTable() string { + if m != nil && m.Table != nil { + return *m.Table + } + return "" +} + +func (m *GoTest) GetParam() int32 { + if m != nil && m.Param != nil { + return *m.Param + } + return 0 +} + +func (m *GoTest) GetRequiredField() *GoTestField { + if m != nil { + return m.RequiredField + } + return nil +} + +func (m *GoTest) GetRepeatedField() []*GoTestField { + if m != nil { + return m.RepeatedField + } + return nil +} + +func (m *GoTest) GetOptionalField() *GoTestField { + if m != nil { + return m.OptionalField + } + return nil +} + +func (m *GoTest) GetF_BoolRequired() bool { + if m != nil && m.F_BoolRequired != nil { + return *m.F_BoolRequired + } + return false +} + +func (m *GoTest) GetF_Int32Required() int32 { + if m != nil && m.F_Int32Required != nil { + return *m.F_Int32Required + } + return 0 +} + +func (m *GoTest) GetF_Int64Required() int64 { + if m != nil && m.F_Int64Required != nil { + return *m.F_Int64Required + } + return 0 +} + +func (m *GoTest) GetF_Fixed32Required() uint32 { + if m != nil && m.F_Fixed32Required != nil { + return *m.F_Fixed32Required + } + return 0 +} + +func (m *GoTest) GetF_Fixed64Required() uint64 { + if m != nil && m.F_Fixed64Required != nil { + return *m.F_Fixed64Required + } + return 0 +} + +func (m *GoTest) GetF_Uint32Required() uint32 { + if m != nil && m.F_Uint32Required != nil { + return *m.F_Uint32Required + } + return 0 +} + +func (m *GoTest) GetF_Uint64Required() uint64 { + if m != nil && m.F_Uint64Required != nil { + return *m.F_Uint64Required + } + return 0 +} + +func (m *GoTest) GetF_FloatRequired() float32 { + if m != nil && m.F_FloatRequired != nil { + return *m.F_FloatRequired + } + return 0 +} + +func (m *GoTest) GetF_DoubleRequired() float64 { + if m != nil && m.F_DoubleRequired != nil { + return *m.F_DoubleRequired + } + return 0 +} + +func (m *GoTest) GetF_StringRequired() string { + if m != nil && m.F_StringRequired != nil { + return *m.F_StringRequired + } + return "" +} + +func (m *GoTest) GetF_BytesRequired() []byte { + if m != nil { + return m.F_BytesRequired + } + return nil +} + +func (m *GoTest) GetF_Sint32Required() int32 { + if m != nil && m.F_Sint32Required != nil { + return *m.F_Sint32Required + } + return 0 +} + +func (m *GoTest) GetF_Sint64Required() int64 { + if m != nil && m.F_Sint64Required != nil { + return *m.F_Sint64Required + } + return 0 +} + +func (m *GoTest) GetF_BoolRepeated() []bool { + if m != nil { + return m.F_BoolRepeated + } + return nil +} + +func (m *GoTest) GetF_Int32Repeated() []int32 { + if m != nil { + return m.F_Int32Repeated + } + return nil +} + +func (m *GoTest) GetF_Int64Repeated() []int64 { + if m != nil { + return m.F_Int64Repeated + } + return nil +} + +func (m *GoTest) GetF_Fixed32Repeated() []uint32 { + if m != nil { + return m.F_Fixed32Repeated + } + return nil +} + +func (m *GoTest) GetF_Fixed64Repeated() []uint64 { + if m != nil { + return m.F_Fixed64Repeated + } + return nil +} + +func (m *GoTest) GetF_Uint32Repeated() []uint32 { + if m != nil { + return m.F_Uint32Repeated + } + return nil +} + +func (m *GoTest) GetF_Uint64Repeated() []uint64 { + if m != nil { + return m.F_Uint64Repeated + } + return nil +} + +func (m *GoTest) GetF_FloatRepeated() []float32 { + if m != nil { + return m.F_FloatRepeated + } + return nil +} + +func (m *GoTest) GetF_DoubleRepeated() []float64 { + if m != nil { + return m.F_DoubleRepeated + } + return nil +} + +func (m *GoTest) GetF_StringRepeated() []string { + if m != nil { + return m.F_StringRepeated + } + return nil +} + +func (m *GoTest) GetF_BytesRepeated() [][]byte { + if m != nil { + return m.F_BytesRepeated + } + return nil +} + +func (m *GoTest) GetF_Sint32Repeated() []int32 { + if m != nil { + return m.F_Sint32Repeated + } + return nil +} + +func (m *GoTest) GetF_Sint64Repeated() []int64 { + if m != nil { + return m.F_Sint64Repeated + } + return nil +} + +func (m *GoTest) GetF_BoolOptional() bool { + if m != nil && m.F_BoolOptional != nil { + return *m.F_BoolOptional + } + return false +} + +func (m *GoTest) GetF_Int32Optional() int32 { + if m != nil && m.F_Int32Optional != nil { + return *m.F_Int32Optional + } + return 0 +} + +func (m *GoTest) GetF_Int64Optional() int64 { + if m != nil && m.F_Int64Optional != nil { + return *m.F_Int64Optional + } + return 0 +} + +func (m *GoTest) GetF_Fixed32Optional() uint32 { + if m != nil && m.F_Fixed32Optional != nil { + return *m.F_Fixed32Optional + } + return 0 +} + +func (m *GoTest) GetF_Fixed64Optional() uint64 { + if m != nil && m.F_Fixed64Optional != nil { + return *m.F_Fixed64Optional + } + return 0 +} + +func (m *GoTest) GetF_Uint32Optional() uint32 { + if m != nil && m.F_Uint32Optional != nil { + return *m.F_Uint32Optional + } + return 0 +} + +func (m *GoTest) GetF_Uint64Optional() uint64 { + if m != nil && m.F_Uint64Optional != nil { + return *m.F_Uint64Optional + } + return 0 +} + +func (m *GoTest) GetF_FloatOptional() float32 { + if m != nil && m.F_FloatOptional != nil { + return *m.F_FloatOptional + } + return 0 +} + +func (m *GoTest) GetF_DoubleOptional() float64 { + if m != nil && m.F_DoubleOptional != nil { + return *m.F_DoubleOptional + } + return 0 +} + +func (m *GoTest) GetF_StringOptional() string { + if m != nil && m.F_StringOptional != nil { + return *m.F_StringOptional + } + return "" +} + +func (m *GoTest) GetF_BytesOptional() []byte { + if m != nil { + return m.F_BytesOptional + } + return nil +} + +func (m *GoTest) GetF_Sint32Optional() int32 { + if m != nil && m.F_Sint32Optional != nil { + return *m.F_Sint32Optional + } + return 0 +} + +func (m *GoTest) GetF_Sint64Optional() int64 { + if m != nil && m.F_Sint64Optional != nil { + return *m.F_Sint64Optional + } + return 0 +} + +func (m *GoTest) GetF_BoolDefaulted() bool { + if m != nil && m.F_BoolDefaulted != nil { + return *m.F_BoolDefaulted + } + return Default_GoTest_F_BoolDefaulted +} + +func (m *GoTest) GetF_Int32Defaulted() int32 { + if m != nil && m.F_Int32Defaulted != nil { + return *m.F_Int32Defaulted + } + return Default_GoTest_F_Int32Defaulted +} + +func (m *GoTest) GetF_Int64Defaulted() int64 { + if m != nil && m.F_Int64Defaulted != nil { + return *m.F_Int64Defaulted + } + return Default_GoTest_F_Int64Defaulted +} + +func (m *GoTest) GetF_Fixed32Defaulted() uint32 { + if m != nil && m.F_Fixed32Defaulted != nil { + return *m.F_Fixed32Defaulted + } + return Default_GoTest_F_Fixed32Defaulted +} + +func (m *GoTest) GetF_Fixed64Defaulted() uint64 { + if m != nil && m.F_Fixed64Defaulted != nil { + return *m.F_Fixed64Defaulted + } + return Default_GoTest_F_Fixed64Defaulted +} + +func (m *GoTest) GetF_Uint32Defaulted() uint32 { + if m != nil && m.F_Uint32Defaulted != nil { + return *m.F_Uint32Defaulted + } + return Default_GoTest_F_Uint32Defaulted +} + +func (m *GoTest) GetF_Uint64Defaulted() uint64 { + if m != nil && m.F_Uint64Defaulted != nil { + return *m.F_Uint64Defaulted + } + return Default_GoTest_F_Uint64Defaulted +} + +func (m *GoTest) GetF_FloatDefaulted() float32 { + if m != nil && m.F_FloatDefaulted != nil { + return *m.F_FloatDefaulted + } + return Default_GoTest_F_FloatDefaulted +} + +func (m *GoTest) GetF_DoubleDefaulted() float64 { + if m != nil && m.F_DoubleDefaulted != nil { + return *m.F_DoubleDefaulted + } + return Default_GoTest_F_DoubleDefaulted +} + +func (m *GoTest) GetF_StringDefaulted() string { + if m != nil && m.F_StringDefaulted != nil { + return *m.F_StringDefaulted + } + return Default_GoTest_F_StringDefaulted +} + +func (m *GoTest) GetF_BytesDefaulted() []byte { + if m != nil && m.F_BytesDefaulted != nil { + return m.F_BytesDefaulted + } + return append([]byte(nil), Default_GoTest_F_BytesDefaulted...) +} + +func (m *GoTest) GetF_Sint32Defaulted() int32 { + if m != nil && m.F_Sint32Defaulted != nil { + return *m.F_Sint32Defaulted + } + return Default_GoTest_F_Sint32Defaulted +} + +func (m *GoTest) GetF_Sint64Defaulted() int64 { + if m != nil && m.F_Sint64Defaulted != nil { + return *m.F_Sint64Defaulted + } + return Default_GoTest_F_Sint64Defaulted +} + +func (m *GoTest) GetF_BoolRepeatedPacked() []bool { + if m != nil { + return m.F_BoolRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Int32RepeatedPacked() []int32 { + if m != nil { + return m.F_Int32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Int64RepeatedPacked() []int64 { + if m != nil { + return m.F_Int64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Fixed32RepeatedPacked() []uint32 { + if m != nil { + return m.F_Fixed32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Fixed64RepeatedPacked() []uint64 { + if m != nil { + return m.F_Fixed64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Uint32RepeatedPacked() []uint32 { + if m != nil { + return m.F_Uint32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Uint64RepeatedPacked() []uint64 { + if m != nil { + return m.F_Uint64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_FloatRepeatedPacked() []float32 { + if m != nil { + return m.F_FloatRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_DoubleRepeatedPacked() []float64 { + if m != nil { + return m.F_DoubleRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Sint32RepeatedPacked() []int32 { + if m != nil { + return m.F_Sint32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Sint64RepeatedPacked() []int64 { + if m != nil { + return m.F_Sint64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetRequiredgroup() *GoTest_RequiredGroup { + if m != nil { + return m.Requiredgroup + } + return nil +} + +func (m *GoTest) GetRepeatedgroup() []*GoTest_RepeatedGroup { + if m != nil { + return m.Repeatedgroup + } + return nil +} + +func (m *GoTest) GetOptionalgroup() *GoTest_OptionalGroup { + if m != nil { + return m.Optionalgroup + } + return nil +} + +// Required, repeated, and optional groups. +type GoTest_RequiredGroup struct { + RequiredField *string `protobuf:"bytes,71,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_RequiredGroup) Reset() { *m = GoTest_RequiredGroup{} } +func (m *GoTest_RequiredGroup) String() string { return proto.CompactTextString(m) } +func (*GoTest_RequiredGroup) ProtoMessage() {} + +func (m *GoTest_RequiredGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +type GoTest_RepeatedGroup struct { + RequiredField *string `protobuf:"bytes,81,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_RepeatedGroup) Reset() { *m = GoTest_RepeatedGroup{} } +func (m *GoTest_RepeatedGroup) String() string { return proto.CompactTextString(m) } +func (*GoTest_RepeatedGroup) ProtoMessage() {} + +func (m *GoTest_RepeatedGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +type GoTest_OptionalGroup struct { + RequiredField *string `protobuf:"bytes,91,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_OptionalGroup) Reset() { *m = GoTest_OptionalGroup{} } +func (m *GoTest_OptionalGroup) String() string { return proto.CompactTextString(m) } +func (*GoTest_OptionalGroup) ProtoMessage() {} + +func (m *GoTest_OptionalGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +// For testing skipping of unrecognized fields. +// Numbers are all big, larger than tag numbers in GoTestField, +// the message used in the corresponding test. +type GoSkipTest struct { + SkipInt32 *int32 `protobuf:"varint,11,req,name=skip_int32" json:"skip_int32,omitempty"` + SkipFixed32 *uint32 `protobuf:"fixed32,12,req,name=skip_fixed32" json:"skip_fixed32,omitempty"` + SkipFixed64 *uint64 `protobuf:"fixed64,13,req,name=skip_fixed64" json:"skip_fixed64,omitempty"` + SkipString *string `protobuf:"bytes,14,req,name=skip_string" json:"skip_string,omitempty"` + Skipgroup *GoSkipTest_SkipGroup `protobuf:"group,15,req,name=SkipGroup" json:"skipgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoSkipTest) Reset() { *m = GoSkipTest{} } +func (m *GoSkipTest) String() string { return proto.CompactTextString(m) } +func (*GoSkipTest) ProtoMessage() {} + +func (m *GoSkipTest) GetSkipInt32() int32 { + if m != nil && m.SkipInt32 != nil { + return *m.SkipInt32 + } + return 0 +} + +func (m *GoSkipTest) GetSkipFixed32() uint32 { + if m != nil && m.SkipFixed32 != nil { + return *m.SkipFixed32 + } + return 0 +} + +func (m *GoSkipTest) GetSkipFixed64() uint64 { + if m != nil && m.SkipFixed64 != nil { + return *m.SkipFixed64 + } + return 0 +} + +func (m *GoSkipTest) GetSkipString() string { + if m != nil && m.SkipString != nil { + return *m.SkipString + } + return "" +} + +func (m *GoSkipTest) GetSkipgroup() *GoSkipTest_SkipGroup { + if m != nil { + return m.Skipgroup + } + return nil +} + +type GoSkipTest_SkipGroup struct { + GroupInt32 *int32 `protobuf:"varint,16,req,name=group_int32" json:"group_int32,omitempty"` + GroupString *string `protobuf:"bytes,17,req,name=group_string" json:"group_string,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoSkipTest_SkipGroup) Reset() { *m = GoSkipTest_SkipGroup{} } +func (m *GoSkipTest_SkipGroup) String() string { return proto.CompactTextString(m) } +func (*GoSkipTest_SkipGroup) ProtoMessage() {} + +func (m *GoSkipTest_SkipGroup) GetGroupInt32() int32 { + if m != nil && m.GroupInt32 != nil { + return *m.GroupInt32 + } + return 0 +} + +func (m *GoSkipTest_SkipGroup) GetGroupString() string { + if m != nil && m.GroupString != nil { + return *m.GroupString + } + return "" +} + +// For testing packed/non-packed decoder switching. +// A serialized instance of one should be deserializable as the other. +type NonPackedTest struct { + A []int32 `protobuf:"varint,1,rep,name=a" json:"a,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NonPackedTest) Reset() { *m = NonPackedTest{} } +func (m *NonPackedTest) String() string { return proto.CompactTextString(m) } +func (*NonPackedTest) ProtoMessage() {} + +func (m *NonPackedTest) GetA() []int32 { + if m != nil { + return m.A + } + return nil +} + +type PackedTest struct { + B []int32 `protobuf:"varint,1,rep,packed,name=b" json:"b,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PackedTest) Reset() { *m = PackedTest{} } +func (m *PackedTest) String() string { return proto.CompactTextString(m) } +func (*PackedTest) ProtoMessage() {} + +func (m *PackedTest) GetB() []int32 { + if m != nil { + return m.B + } + return nil +} + +type MaxTag struct { + // Maximum possible tag number. + LastField *string `protobuf:"bytes,536870911,opt,name=last_field" json:"last_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MaxTag) Reset() { *m = MaxTag{} } +func (m *MaxTag) String() string { return proto.CompactTextString(m) } +func (*MaxTag) ProtoMessage() {} + +func (m *MaxTag) GetLastField() string { + if m != nil && m.LastField != nil { + return *m.LastField + } + return "" +} + +type OldMessage struct { + Nested *OldMessage_Nested `protobuf:"bytes,1,opt,name=nested" json:"nested,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OldMessage) Reset() { *m = OldMessage{} } +func (m *OldMessage) String() string { return proto.CompactTextString(m) } +func (*OldMessage) ProtoMessage() {} + +func (m *OldMessage) GetNested() *OldMessage_Nested { + if m != nil { + return m.Nested + } + return nil +} + +type OldMessage_Nested struct { + Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OldMessage_Nested) Reset() { *m = OldMessage_Nested{} } +func (m *OldMessage_Nested) String() string { return proto.CompactTextString(m) } +func (*OldMessage_Nested) ProtoMessage() {} + +func (m *OldMessage_Nested) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +// NewMessage is wire compatible with OldMessage; +// imagine it as a future version. +type NewMessage struct { + Nested *NewMessage_Nested `protobuf:"bytes,1,opt,name=nested" json:"nested,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NewMessage) Reset() { *m = NewMessage{} } +func (m *NewMessage) String() string { return proto.CompactTextString(m) } +func (*NewMessage) ProtoMessage() {} + +func (m *NewMessage) GetNested() *NewMessage_Nested { + if m != nil { + return m.Nested + } + return nil +} + +type NewMessage_Nested struct { + Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + FoodGroup *string `protobuf:"bytes,2,opt,name=food_group" json:"food_group,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NewMessage_Nested) Reset() { *m = NewMessage_Nested{} } +func (m *NewMessage_Nested) String() string { return proto.CompactTextString(m) } +func (*NewMessage_Nested) ProtoMessage() {} + +func (m *NewMessage_Nested) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *NewMessage_Nested) GetFoodGroup() string { + if m != nil && m.FoodGroup != nil { + return *m.FoodGroup + } + return "" +} + +type InnerMessage struct { + Host *string `protobuf:"bytes,1,req,name=host" json:"host,omitempty"` + Port *int32 `protobuf:"varint,2,opt,name=port,def=4000" json:"port,omitempty"` + Connected *bool `protobuf:"varint,3,opt,name=connected" json:"connected,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *InnerMessage) Reset() { *m = InnerMessage{} } +func (m *InnerMessage) String() string { return proto.CompactTextString(m) } +func (*InnerMessage) ProtoMessage() {} + +const Default_InnerMessage_Port int32 = 4000 + +func (m *InnerMessage) GetHost() string { + if m != nil && m.Host != nil { + return *m.Host + } + return "" +} + +func (m *InnerMessage) GetPort() int32 { + if m != nil && m.Port != nil { + return *m.Port + } + return Default_InnerMessage_Port +} + +func (m *InnerMessage) GetConnected() bool { + if m != nil && m.Connected != nil { + return *m.Connected + } + return false +} + +type OtherMessage struct { + Key *int64 `protobuf:"varint,1,opt,name=key" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value" json:"value,omitempty"` + Weight *float32 `protobuf:"fixed32,3,opt,name=weight" json:"weight,omitempty"` + Inner *InnerMessage `protobuf:"bytes,4,opt,name=inner" json:"inner,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OtherMessage) Reset() { *m = OtherMessage{} } +func (m *OtherMessage) String() string { return proto.CompactTextString(m) } +func (*OtherMessage) ProtoMessage() {} + +func (m *OtherMessage) GetKey() int64 { + if m != nil && m.Key != nil { + return *m.Key + } + return 0 +} + +func (m *OtherMessage) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *OtherMessage) GetWeight() float32 { + if m != nil && m.Weight != nil { + return *m.Weight + } + return 0 +} + +func (m *OtherMessage) GetInner() *InnerMessage { + if m != nil { + return m.Inner + } + return nil +} + +type MyMessage struct { + Count *int32 `protobuf:"varint,1,req,name=count" json:"count,omitempty"` + Name *string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` + Quote *string `protobuf:"bytes,3,opt,name=quote" json:"quote,omitempty"` + Pet []string `protobuf:"bytes,4,rep,name=pet" json:"pet,omitempty"` + Inner *InnerMessage `protobuf:"bytes,5,opt,name=inner" json:"inner,omitempty"` + Others []*OtherMessage `protobuf:"bytes,6,rep,name=others" json:"others,omitempty"` + RepInner []*InnerMessage `protobuf:"bytes,12,rep,name=rep_inner" json:"rep_inner,omitempty"` + Bikeshed *MyMessage_Color `protobuf:"varint,7,opt,name=bikeshed,enum=testdata.MyMessage_Color" json:"bikeshed,omitempty"` + Somegroup *MyMessage_SomeGroup `protobuf:"group,8,opt,name=SomeGroup" json:"somegroup,omitempty"` + // This field becomes [][]byte in the generated code. + RepBytes [][]byte `protobuf:"bytes,10,rep,name=rep_bytes" json:"rep_bytes,omitempty"` + Bigfloat *float64 `protobuf:"fixed64,11,opt,name=bigfloat" json:"bigfloat,omitempty"` + XXX_extensions map[int32]proto.Extension `json:"-"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MyMessage) Reset() { *m = MyMessage{} } +func (m *MyMessage) String() string { return proto.CompactTextString(m) } +func (*MyMessage) ProtoMessage() {} + +var extRange_MyMessage = []proto.ExtensionRange{ + {100, 536870911}, +} + +func (*MyMessage) ExtensionRangeArray() []proto.ExtensionRange { + return extRange_MyMessage +} +func (m *MyMessage) ExtensionMap() map[int32]proto.Extension { + if m.XXX_extensions == nil { + m.XXX_extensions = make(map[int32]proto.Extension) + } + return m.XXX_extensions +} + +func (m *MyMessage) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *MyMessage) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *MyMessage) GetQuote() string { + if m != nil && m.Quote != nil { + return *m.Quote + } + return "" +} + +func (m *MyMessage) GetPet() []string { + if m != nil { + return m.Pet + } + return nil +} + +func (m *MyMessage) GetInner() *InnerMessage { + if m != nil { + return m.Inner + } + return nil +} + +func (m *MyMessage) GetOthers() []*OtherMessage { + if m != nil { + return m.Others + } + return nil +} + +func (m *MyMessage) GetRepInner() []*InnerMessage { + if m != nil { + return m.RepInner + } + return nil +} + +func (m *MyMessage) GetBikeshed() MyMessage_Color { + if m != nil && m.Bikeshed != nil { + return *m.Bikeshed + } + return MyMessage_RED +} + +func (m *MyMessage) GetSomegroup() *MyMessage_SomeGroup { + if m != nil { + return m.Somegroup + } + return nil +} + +func (m *MyMessage) GetRepBytes() [][]byte { + if m != nil { + return m.RepBytes + } + return nil +} + +func (m *MyMessage) GetBigfloat() float64 { + if m != nil && m.Bigfloat != nil { + return *m.Bigfloat + } + return 0 +} + +type MyMessage_SomeGroup struct { + GroupField *int32 `protobuf:"varint,9,opt,name=group_field" json:"group_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MyMessage_SomeGroup) Reset() { *m = MyMessage_SomeGroup{} } +func (m *MyMessage_SomeGroup) String() string { return proto.CompactTextString(m) } +func (*MyMessage_SomeGroup) ProtoMessage() {} + +func (m *MyMessage_SomeGroup) GetGroupField() int32 { + if m != nil && m.GroupField != nil { + return *m.GroupField + } + return 0 +} + +type Ext struct { + Data *string `protobuf:"bytes,1,opt,name=data" json:"data,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Ext) Reset() { *m = Ext{} } +func (m *Ext) String() string { return proto.CompactTextString(m) } +func (*Ext) ProtoMessage() {} + +func (m *Ext) GetData() string { + if m != nil && m.Data != nil { + return *m.Data + } + return "" +} + +var E_Ext_More = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*Ext)(nil), + Field: 103, + Name: "testdata.Ext.more", + Tag: "bytes,103,opt,name=more", +} + +var E_Ext_Text = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*string)(nil), + Field: 104, + Name: "testdata.Ext.text", + Tag: "bytes,104,opt,name=text", +} + +var E_Ext_Number = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*int32)(nil), + Field: 105, + Name: "testdata.Ext.number", + Tag: "varint,105,opt,name=number", +} + +type MyMessageSet struct { + XXX_extensions map[int32]proto.Extension `json:"-"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MyMessageSet) Reset() { *m = MyMessageSet{} } +func (m *MyMessageSet) String() string { return proto.CompactTextString(m) } +func (*MyMessageSet) ProtoMessage() {} + +func (m *MyMessageSet) Marshal() ([]byte, error) { + return proto.MarshalMessageSet(m.ExtensionMap()) +} +func (m *MyMessageSet) Unmarshal(buf []byte) error { + return proto.UnmarshalMessageSet(buf, m.ExtensionMap()) +} + +// ensure MyMessageSet satisfies proto.Marshaler and proto.Unmarshaler +var _ proto.Marshaler = (*MyMessageSet)(nil) +var _ proto.Unmarshaler = (*MyMessageSet)(nil) + +var extRange_MyMessageSet = []proto.ExtensionRange{ + {100, 2147483646}, +} + +func (*MyMessageSet) ExtensionRangeArray() []proto.ExtensionRange { + return extRange_MyMessageSet +} +func (m *MyMessageSet) ExtensionMap() map[int32]proto.Extension { + if m.XXX_extensions == nil { + m.XXX_extensions = make(map[int32]proto.Extension) + } + return m.XXX_extensions +} + +type Empty struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} + +type MessageList struct { + Message []*MessageList_Message `protobuf:"group,1,rep" json:"message,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MessageList) Reset() { *m = MessageList{} } +func (m *MessageList) String() string { return proto.CompactTextString(m) } +func (*MessageList) ProtoMessage() {} + +func (m *MessageList) GetMessage() []*MessageList_Message { + if m != nil { + return m.Message + } + return nil +} + +type MessageList_Message struct { + Name *string `protobuf:"bytes,2,req,name=name" json:"name,omitempty"` + Count *int32 `protobuf:"varint,3,req,name=count" json:"count,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MessageList_Message) Reset() { *m = MessageList_Message{} } +func (m *MessageList_Message) String() string { return proto.CompactTextString(m) } +func (*MessageList_Message) ProtoMessage() {} + +func (m *MessageList_Message) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *MessageList_Message) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +type Strings struct { + StringField *string `protobuf:"bytes,1,opt,name=string_field" json:"string_field,omitempty"` + BytesField []byte `protobuf:"bytes,2,opt,name=bytes_field" json:"bytes_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Strings) Reset() { *m = Strings{} } +func (m *Strings) String() string { return proto.CompactTextString(m) } +func (*Strings) ProtoMessage() {} + +func (m *Strings) GetStringField() string { + if m != nil && m.StringField != nil { + return *m.StringField + } + return "" +} + +func (m *Strings) GetBytesField() []byte { + if m != nil { + return m.BytesField + } + return nil +} + +type Defaults struct { + // Default-valued fields of all basic types. + // Same as GoTest, but copied here to make testing easier. + F_Bool *bool `protobuf:"varint,1,opt,def=1" json:"F_Bool,omitempty"` + F_Int32 *int32 `protobuf:"varint,2,opt,def=32" json:"F_Int32,omitempty"` + F_Int64 *int64 `protobuf:"varint,3,opt,def=64" json:"F_Int64,omitempty"` + F_Fixed32 *uint32 `protobuf:"fixed32,4,opt,def=320" json:"F_Fixed32,omitempty"` + F_Fixed64 *uint64 `protobuf:"fixed64,5,opt,def=640" json:"F_Fixed64,omitempty"` + F_Uint32 *uint32 `protobuf:"varint,6,opt,def=3200" json:"F_Uint32,omitempty"` + F_Uint64 *uint64 `protobuf:"varint,7,opt,def=6400" json:"F_Uint64,omitempty"` + F_Float *float32 `protobuf:"fixed32,8,opt,def=314159" json:"F_Float,omitempty"` + F_Double *float64 `protobuf:"fixed64,9,opt,def=271828" json:"F_Double,omitempty"` + F_String *string `protobuf:"bytes,10,opt,def=hello, \"world!\"\n" json:"F_String,omitempty"` + F_Bytes []byte `protobuf:"bytes,11,opt,def=Bignose" json:"F_Bytes,omitempty"` + F_Sint32 *int32 `protobuf:"zigzag32,12,opt,def=-32" json:"F_Sint32,omitempty"` + F_Sint64 *int64 `protobuf:"zigzag64,13,opt,def=-64" json:"F_Sint64,omitempty"` + F_Enum *Defaults_Color `protobuf:"varint,14,opt,enum=testdata.Defaults_Color,def=1" json:"F_Enum,omitempty"` + // More fields with crazy defaults. + F_Pinf *float32 `protobuf:"fixed32,15,opt,def=inf" json:"F_Pinf,omitempty"` + F_Ninf *float32 `protobuf:"fixed32,16,opt,def=-inf" json:"F_Ninf,omitempty"` + F_Nan *float32 `protobuf:"fixed32,17,opt,def=nan" json:"F_Nan,omitempty"` + // Sub-message. + Sub *SubDefaults `protobuf:"bytes,18,opt,name=sub" json:"sub,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Defaults) Reset() { *m = Defaults{} } +func (m *Defaults) String() string { return proto.CompactTextString(m) } +func (*Defaults) ProtoMessage() {} + +const Default_Defaults_F_Bool bool = true +const Default_Defaults_F_Int32 int32 = 32 +const Default_Defaults_F_Int64 int64 = 64 +const Default_Defaults_F_Fixed32 uint32 = 320 +const Default_Defaults_F_Fixed64 uint64 = 640 +const Default_Defaults_F_Uint32 uint32 = 3200 +const Default_Defaults_F_Uint64 uint64 = 6400 +const Default_Defaults_F_Float float32 = 314159 +const Default_Defaults_F_Double float64 = 271828 +const Default_Defaults_F_String string = "hello, \"world!\"\n" + +var Default_Defaults_F_Bytes []byte = []byte("Bignose") + +const Default_Defaults_F_Sint32 int32 = -32 +const Default_Defaults_F_Sint64 int64 = -64 +const Default_Defaults_F_Enum Defaults_Color = Defaults_GREEN + +var Default_Defaults_F_Pinf float32 = float32(math.Inf(1)) +var Default_Defaults_F_Ninf float32 = float32(math.Inf(-1)) +var Default_Defaults_F_Nan float32 = float32(math.NaN()) + +func (m *Defaults) GetF_Bool() bool { + if m != nil && m.F_Bool != nil { + return *m.F_Bool + } + return Default_Defaults_F_Bool +} + +func (m *Defaults) GetF_Int32() int32 { + if m != nil && m.F_Int32 != nil { + return *m.F_Int32 + } + return Default_Defaults_F_Int32 +} + +func (m *Defaults) GetF_Int64() int64 { + if m != nil && m.F_Int64 != nil { + return *m.F_Int64 + } + return Default_Defaults_F_Int64 +} + +func (m *Defaults) GetF_Fixed32() uint32 { + if m != nil && m.F_Fixed32 != nil { + return *m.F_Fixed32 + } + return Default_Defaults_F_Fixed32 +} + +func (m *Defaults) GetF_Fixed64() uint64 { + if m != nil && m.F_Fixed64 != nil { + return *m.F_Fixed64 + } + return Default_Defaults_F_Fixed64 +} + +func (m *Defaults) GetF_Uint32() uint32 { + if m != nil && m.F_Uint32 != nil { + return *m.F_Uint32 + } + return Default_Defaults_F_Uint32 +} + +func (m *Defaults) GetF_Uint64() uint64 { + if m != nil && m.F_Uint64 != nil { + return *m.F_Uint64 + } + return Default_Defaults_F_Uint64 +} + +func (m *Defaults) GetF_Float() float32 { + if m != nil && m.F_Float != nil { + return *m.F_Float + } + return Default_Defaults_F_Float +} + +func (m *Defaults) GetF_Double() float64 { + if m != nil && m.F_Double != nil { + return *m.F_Double + } + return Default_Defaults_F_Double +} + +func (m *Defaults) GetF_String() string { + if m != nil && m.F_String != nil { + return *m.F_String + } + return Default_Defaults_F_String +} + +func (m *Defaults) GetF_Bytes() []byte { + if m != nil && m.F_Bytes != nil { + return m.F_Bytes + } + return append([]byte(nil), Default_Defaults_F_Bytes...) +} + +func (m *Defaults) GetF_Sint32() int32 { + if m != nil && m.F_Sint32 != nil { + return *m.F_Sint32 + } + return Default_Defaults_F_Sint32 +} + +func (m *Defaults) GetF_Sint64() int64 { + if m != nil && m.F_Sint64 != nil { + return *m.F_Sint64 + } + return Default_Defaults_F_Sint64 +} + +func (m *Defaults) GetF_Enum() Defaults_Color { + if m != nil && m.F_Enum != nil { + return *m.F_Enum + } + return Default_Defaults_F_Enum +} + +func (m *Defaults) GetF_Pinf() float32 { + if m != nil && m.F_Pinf != nil { + return *m.F_Pinf + } + return Default_Defaults_F_Pinf +} + +func (m *Defaults) GetF_Ninf() float32 { + if m != nil && m.F_Ninf != nil { + return *m.F_Ninf + } + return Default_Defaults_F_Ninf +} + +func (m *Defaults) GetF_Nan() float32 { + if m != nil && m.F_Nan != nil { + return *m.F_Nan + } + return Default_Defaults_F_Nan +} + +func (m *Defaults) GetSub() *SubDefaults { + if m != nil { + return m.Sub + } + return nil +} + +type SubDefaults struct { + N *int64 `protobuf:"varint,1,opt,name=n,def=7" json:"n,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SubDefaults) Reset() { *m = SubDefaults{} } +func (m *SubDefaults) String() string { return proto.CompactTextString(m) } +func (*SubDefaults) ProtoMessage() {} + +const Default_SubDefaults_N int64 = 7 + +func (m *SubDefaults) GetN() int64 { + if m != nil && m.N != nil { + return *m.N + } + return Default_SubDefaults_N +} + +type RepeatedEnum struct { + Color []RepeatedEnum_Color `protobuf:"varint,1,rep,name=color,enum=testdata.RepeatedEnum_Color" json:"color,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *RepeatedEnum) Reset() { *m = RepeatedEnum{} } +func (m *RepeatedEnum) String() string { return proto.CompactTextString(m) } +func (*RepeatedEnum) ProtoMessage() {} + +func (m *RepeatedEnum) GetColor() []RepeatedEnum_Color { + if m != nil { + return m.Color + } + return nil +} + +type MoreRepeated struct { + Bools []bool `protobuf:"varint,1,rep,name=bools" json:"bools,omitempty"` + BoolsPacked []bool `protobuf:"varint,2,rep,packed,name=bools_packed" json:"bools_packed,omitempty"` + Ints []int32 `protobuf:"varint,3,rep,name=ints" json:"ints,omitempty"` + IntsPacked []int32 `protobuf:"varint,4,rep,packed,name=ints_packed" json:"ints_packed,omitempty"` + Int64SPacked []int64 `protobuf:"varint,7,rep,packed,name=int64s_packed" json:"int64s_packed,omitempty"` + Strings []string `protobuf:"bytes,5,rep,name=strings" json:"strings,omitempty"` + Fixeds []uint32 `protobuf:"fixed32,6,rep,name=fixeds" json:"fixeds,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MoreRepeated) Reset() { *m = MoreRepeated{} } +func (m *MoreRepeated) String() string { return proto.CompactTextString(m) } +func (*MoreRepeated) ProtoMessage() {} + +func (m *MoreRepeated) GetBools() []bool { + if m != nil { + return m.Bools + } + return nil +} + +func (m *MoreRepeated) GetBoolsPacked() []bool { + if m != nil { + return m.BoolsPacked + } + return nil +} + +func (m *MoreRepeated) GetInts() []int32 { + if m != nil { + return m.Ints + } + return nil +} + +func (m *MoreRepeated) GetIntsPacked() []int32 { + if m != nil { + return m.IntsPacked + } + return nil +} + +func (m *MoreRepeated) GetInt64SPacked() []int64 { + if m != nil { + return m.Int64SPacked + } + return nil +} + +func (m *MoreRepeated) GetStrings() []string { + if m != nil { + return m.Strings + } + return nil +} + +func (m *MoreRepeated) GetFixeds() []uint32 { + if m != nil { + return m.Fixeds + } + return nil +} + +type GroupOld struct { + G *GroupOld_G `protobuf:"group,101,opt" json:"g,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupOld) Reset() { *m = GroupOld{} } +func (m *GroupOld) String() string { return proto.CompactTextString(m) } +func (*GroupOld) ProtoMessage() {} + +func (m *GroupOld) GetG() *GroupOld_G { + if m != nil { + return m.G + } + return nil +} + +type GroupOld_G struct { + X *int32 `protobuf:"varint,2,opt,name=x" json:"x,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupOld_G) Reset() { *m = GroupOld_G{} } +func (m *GroupOld_G) String() string { return proto.CompactTextString(m) } +func (*GroupOld_G) ProtoMessage() {} + +func (m *GroupOld_G) GetX() int32 { + if m != nil && m.X != nil { + return *m.X + } + return 0 +} + +type GroupNew struct { + G *GroupNew_G `protobuf:"group,101,opt" json:"g,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupNew) Reset() { *m = GroupNew{} } +func (m *GroupNew) String() string { return proto.CompactTextString(m) } +func (*GroupNew) ProtoMessage() {} + +func (m *GroupNew) GetG() *GroupNew_G { + if m != nil { + return m.G + } + return nil +} + +type GroupNew_G struct { + X *int32 `protobuf:"varint,2,opt,name=x" json:"x,omitempty"` + Y *int32 `protobuf:"varint,3,opt,name=y" json:"y,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupNew_G) Reset() { *m = GroupNew_G{} } +func (m *GroupNew_G) String() string { return proto.CompactTextString(m) } +func (*GroupNew_G) ProtoMessage() {} + +func (m *GroupNew_G) GetX() int32 { + if m != nil && m.X != nil { + return *m.X + } + return 0 +} + +func (m *GroupNew_G) GetY() int32 { + if m != nil && m.Y != nil { + return *m.Y + } + return 0 +} + +type FloatingPoint struct { + F *float64 `protobuf:"fixed64,1,req,name=f" json:"f,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *FloatingPoint) Reset() { *m = FloatingPoint{} } +func (m *FloatingPoint) String() string { return proto.CompactTextString(m) } +func (*FloatingPoint) ProtoMessage() {} + +func (m *FloatingPoint) GetF() float64 { + if m != nil && m.F != nil { + return *m.F + } + return 0 +} + +var E_Greeting = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: ([]string)(nil), + Field: 106, + Name: "testdata.greeting", + Tag: "bytes,106,rep,name=greeting", +} + +var E_X201 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 201, + Name: "testdata.x201", + Tag: "bytes,201,opt,name=x201", +} + +var E_X202 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 202, + Name: "testdata.x202", + Tag: "bytes,202,opt,name=x202", +} + +var E_X203 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 203, + Name: "testdata.x203", + Tag: "bytes,203,opt,name=x203", +} + +var E_X204 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 204, + Name: "testdata.x204", + Tag: "bytes,204,opt,name=x204", +} + +var E_X205 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 205, + Name: "testdata.x205", + Tag: "bytes,205,opt,name=x205", +} + +var E_X206 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 206, + Name: "testdata.x206", + Tag: "bytes,206,opt,name=x206", +} + +var E_X207 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 207, + Name: "testdata.x207", + Tag: "bytes,207,opt,name=x207", +} + +var E_X208 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 208, + Name: "testdata.x208", + Tag: "bytes,208,opt,name=x208", +} + +var E_X209 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 209, + Name: "testdata.x209", + Tag: "bytes,209,opt,name=x209", +} + +var E_X210 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 210, + Name: "testdata.x210", + Tag: "bytes,210,opt,name=x210", +} + +var E_X211 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 211, + Name: "testdata.x211", + Tag: "bytes,211,opt,name=x211", +} + +var E_X212 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 212, + Name: "testdata.x212", + Tag: "bytes,212,opt,name=x212", +} + +var E_X213 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 213, + Name: "testdata.x213", + Tag: "bytes,213,opt,name=x213", +} + +var E_X214 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 214, + Name: "testdata.x214", + Tag: "bytes,214,opt,name=x214", +} + +var E_X215 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 215, + Name: "testdata.x215", + Tag: "bytes,215,opt,name=x215", +} + +var E_X216 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 216, + Name: "testdata.x216", + Tag: "bytes,216,opt,name=x216", +} + +var E_X217 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 217, + Name: "testdata.x217", + Tag: "bytes,217,opt,name=x217", +} + +var E_X218 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 218, + Name: "testdata.x218", + Tag: "bytes,218,opt,name=x218", +} + +var E_X219 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 219, + Name: "testdata.x219", + Tag: "bytes,219,opt,name=x219", +} + +var E_X220 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 220, + Name: "testdata.x220", + Tag: "bytes,220,opt,name=x220", +} + +var E_X221 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 221, + Name: "testdata.x221", + Tag: "bytes,221,opt,name=x221", +} + +var E_X222 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 222, + Name: "testdata.x222", + Tag: "bytes,222,opt,name=x222", +} + +var E_X223 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 223, + Name: "testdata.x223", + Tag: "bytes,223,opt,name=x223", +} + +var E_X224 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 224, + Name: "testdata.x224", + Tag: "bytes,224,opt,name=x224", +} + +var E_X225 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 225, + Name: "testdata.x225", + Tag: "bytes,225,opt,name=x225", +} + +var E_X226 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 226, + Name: "testdata.x226", + Tag: "bytes,226,opt,name=x226", +} + +var E_X227 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 227, + Name: "testdata.x227", + Tag: "bytes,227,opt,name=x227", +} + +var E_X228 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 228, + Name: "testdata.x228", + Tag: "bytes,228,opt,name=x228", +} + +var E_X229 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 229, + Name: "testdata.x229", + Tag: "bytes,229,opt,name=x229", +} + +var E_X230 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 230, + Name: "testdata.x230", + Tag: "bytes,230,opt,name=x230", +} + +var E_X231 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 231, + Name: "testdata.x231", + Tag: "bytes,231,opt,name=x231", +} + +var E_X232 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 232, + Name: "testdata.x232", + Tag: "bytes,232,opt,name=x232", +} + +var E_X233 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 233, + Name: "testdata.x233", + Tag: "bytes,233,opt,name=x233", +} + +var E_X234 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 234, + Name: "testdata.x234", + Tag: "bytes,234,opt,name=x234", +} + +var E_X235 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 235, + Name: "testdata.x235", + Tag: "bytes,235,opt,name=x235", +} + +var E_X236 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 236, + Name: "testdata.x236", + Tag: "bytes,236,opt,name=x236", +} + +var E_X237 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 237, + Name: "testdata.x237", + Tag: "bytes,237,opt,name=x237", +} + +var E_X238 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 238, + Name: "testdata.x238", + Tag: "bytes,238,opt,name=x238", +} + +var E_X239 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 239, + Name: "testdata.x239", + Tag: "bytes,239,opt,name=x239", +} + +var E_X240 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 240, + Name: "testdata.x240", + Tag: "bytes,240,opt,name=x240", +} + +var E_X241 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 241, + Name: "testdata.x241", + Tag: "bytes,241,opt,name=x241", +} + +var E_X242 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 242, + Name: "testdata.x242", + Tag: "bytes,242,opt,name=x242", +} + +var E_X243 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 243, + Name: "testdata.x243", + Tag: "bytes,243,opt,name=x243", +} + +var E_X244 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 244, + Name: "testdata.x244", + Tag: "bytes,244,opt,name=x244", +} + +var E_X245 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 245, + Name: "testdata.x245", + Tag: "bytes,245,opt,name=x245", +} + +var E_X246 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 246, + Name: "testdata.x246", + Tag: "bytes,246,opt,name=x246", +} + +var E_X247 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 247, + Name: "testdata.x247", + Tag: "bytes,247,opt,name=x247", +} + +var E_X248 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 248, + Name: "testdata.x248", + Tag: "bytes,248,opt,name=x248", +} + +var E_X249 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 249, + Name: "testdata.x249", + Tag: "bytes,249,opt,name=x249", +} + +var E_X250 = &proto.ExtensionDesc{ + ExtendedType: (*MyMessageSet)(nil), + ExtensionType: (*Empty)(nil), + Field: 250, + Name: "testdata.x250", + Tag: "bytes,250,opt,name=x250", +} + +func init() { + proto.RegisterEnum("testdata.FOO", FOO_name, FOO_value) + proto.RegisterEnum("testdata.GoTest_KIND", GoTest_KIND_name, GoTest_KIND_value) + proto.RegisterEnum("testdata.MyMessage_Color", MyMessage_Color_name, MyMessage_Color_value) + proto.RegisterEnum("testdata.Defaults_Color", Defaults_Color_name, Defaults_Color_value) + proto.RegisterEnum("testdata.RepeatedEnum_Color", RepeatedEnum_Color_name, RepeatedEnum_Color_value) + proto.RegisterExtension(E_Ext_More) + proto.RegisterExtension(E_Ext_Text) + proto.RegisterExtension(E_Ext_Number) + proto.RegisterExtension(E_Greeting) + proto.RegisterExtension(E_X201) + proto.RegisterExtension(E_X202) + proto.RegisterExtension(E_X203) + proto.RegisterExtension(E_X204) + proto.RegisterExtension(E_X205) + proto.RegisterExtension(E_X206) + proto.RegisterExtension(E_X207) + proto.RegisterExtension(E_X208) + proto.RegisterExtension(E_X209) + proto.RegisterExtension(E_X210) + proto.RegisterExtension(E_X211) + proto.RegisterExtension(E_X212) + proto.RegisterExtension(E_X213) + proto.RegisterExtension(E_X214) + proto.RegisterExtension(E_X215) + proto.RegisterExtension(E_X216) + proto.RegisterExtension(E_X217) + proto.RegisterExtension(E_X218) + proto.RegisterExtension(E_X219) + proto.RegisterExtension(E_X220) + proto.RegisterExtension(E_X221) + proto.RegisterExtension(E_X222) + proto.RegisterExtension(E_X223) + proto.RegisterExtension(E_X224) + proto.RegisterExtension(E_X225) + proto.RegisterExtension(E_X226) + proto.RegisterExtension(E_X227) + proto.RegisterExtension(E_X228) + proto.RegisterExtension(E_X229) + proto.RegisterExtension(E_X230) + proto.RegisterExtension(E_X231) + proto.RegisterExtension(E_X232) + proto.RegisterExtension(E_X233) + proto.RegisterExtension(E_X234) + proto.RegisterExtension(E_X235) + proto.RegisterExtension(E_X236) + proto.RegisterExtension(E_X237) + proto.RegisterExtension(E_X238) + proto.RegisterExtension(E_X239) + proto.RegisterExtension(E_X240) + proto.RegisterExtension(E_X241) + proto.RegisterExtension(E_X242) + proto.RegisterExtension(E_X243) + proto.RegisterExtension(E_X244) + proto.RegisterExtension(E_X245) + proto.RegisterExtension(E_X246) + proto.RegisterExtension(E_X247) + proto.RegisterExtension(E_X248) + proto.RegisterExtension(E_X249) + proto.RegisterExtension(E_X250) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go.golden b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go.golden new file mode 100644 index 00000000000..b79ce68e11e --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go.golden @@ -0,0 +1,1737 @@ +// Code generated by protoc-gen-gogo. +// source: test.proto +// DO NOT EDIT! + +package testdata + +import proto "code.google.com/p/gogoprotobuf/proto" +import json "encoding/json" +import math "math" + +import () + +// Reference proto, json, and math imports to suppress error if they are not otherwise used. +var _ = proto.Marshal +var _ = &json.SyntaxError{} +var _ = math.Inf + +type FOO int32 + +const ( + FOO_FOO1 FOO = 1 +) + +var FOO_name = map[int32]string{ + 1: "FOO1", +} +var FOO_value = map[string]int32{ + "FOO1": 1, +} + +func (x FOO) Enum() *FOO { + p := new(FOO) + *p = x + return p +} +func (x FOO) String() string { + return proto.EnumName(FOO_name, int32(x)) +} +func (x FOO) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *FOO) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO") + if err != nil { + return err + } + *x = FOO(value) + return nil +} + +type GoTest_KIND int32 + +const ( + GoTest_VOID GoTest_KIND = 0 + GoTest_BOOL GoTest_KIND = 1 + GoTest_BYTES GoTest_KIND = 2 + GoTest_FINGERPRINT GoTest_KIND = 3 + GoTest_FLOAT GoTest_KIND = 4 + GoTest_INT GoTest_KIND = 5 + GoTest_STRING GoTest_KIND = 6 + GoTest_TIME GoTest_KIND = 7 + GoTest_TUPLE GoTest_KIND = 8 + GoTest_ARRAY GoTest_KIND = 9 + GoTest_MAP GoTest_KIND = 10 + GoTest_TABLE GoTest_KIND = 11 + GoTest_FUNCTION GoTest_KIND = 12 +) + +var GoTest_KIND_name = map[int32]string{ + 0: "VOID", + 1: "BOOL", + 2: "BYTES", + 3: "FINGERPRINT", + 4: "FLOAT", + 5: "INT", + 6: "STRING", + 7: "TIME", + 8: "TUPLE", + 9: "ARRAY", + 10: "MAP", + 11: "TABLE", + 12: "FUNCTION", +} +var GoTest_KIND_value = map[string]int32{ + "VOID": 0, + "BOOL": 1, + "BYTES": 2, + "FINGERPRINT": 3, + "FLOAT": 4, + "INT": 5, + "STRING": 6, + "TIME": 7, + "TUPLE": 8, + "ARRAY": 9, + "MAP": 10, + "TABLE": 11, + "FUNCTION": 12, +} + +func (x GoTest_KIND) Enum() *GoTest_KIND { + p := new(GoTest_KIND) + *p = x + return p +} +func (x GoTest_KIND) String() string { + return proto.EnumName(GoTest_KIND_name, int32(x)) +} +func (x GoTest_KIND) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *GoTest_KIND) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(GoTest_KIND_value, data, "GoTest_KIND") + if err != nil { + return err + } + *x = GoTest_KIND(value) + return nil +} + +type MyMessage_Color int32 + +const ( + MyMessage_RED MyMessage_Color = 0 + MyMessage_GREEN MyMessage_Color = 1 + MyMessage_BLUE MyMessage_Color = 2 +) + +var MyMessage_Color_name = map[int32]string{ + 0: "RED", + 1: "GREEN", + 2: "BLUE", +} +var MyMessage_Color_value = map[string]int32{ + "RED": 0, + "GREEN": 1, + "BLUE": 2, +} + +func (x MyMessage_Color) Enum() *MyMessage_Color { + p := new(MyMessage_Color) + *p = x + return p +} +func (x MyMessage_Color) String() string { + return proto.EnumName(MyMessage_Color_name, int32(x)) +} +func (x MyMessage_Color) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *MyMessage_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(MyMessage_Color_value, data, "MyMessage_Color") + if err != nil { + return err + } + *x = MyMessage_Color(value) + return nil +} + +type Defaults_Color int32 + +const ( + Defaults_RED Defaults_Color = 0 + Defaults_GREEN Defaults_Color = 1 + Defaults_BLUE Defaults_Color = 2 +) + +var Defaults_Color_name = map[int32]string{ + 0: "RED", + 1: "GREEN", + 2: "BLUE", +} +var Defaults_Color_value = map[string]int32{ + "RED": 0, + "GREEN": 1, + "BLUE": 2, +} + +func (x Defaults_Color) Enum() *Defaults_Color { + p := new(Defaults_Color) + *p = x + return p +} +func (x Defaults_Color) String() string { + return proto.EnumName(Defaults_Color_name, int32(x)) +} +func (x Defaults_Color) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *Defaults_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Defaults_Color_value, data, "Defaults_Color") + if err != nil { + return err + } + *x = Defaults_Color(value) + return nil +} + +type RepeatedEnum_Color int32 + +const ( + RepeatedEnum_RED RepeatedEnum_Color = 1 +) + +var RepeatedEnum_Color_name = map[int32]string{ + 1: "RED", +} +var RepeatedEnum_Color_value = map[string]int32{ + "RED": 1, +} + +func (x RepeatedEnum_Color) Enum() *RepeatedEnum_Color { + p := new(RepeatedEnum_Color) + *p = x + return p +} +func (x RepeatedEnum_Color) String() string { + return proto.EnumName(RepeatedEnum_Color_name, int32(x)) +} +func (x RepeatedEnum_Color) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *RepeatedEnum_Color) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(RepeatedEnum_Color_value, data, "RepeatedEnum_Color") + if err != nil { + return err + } + *x = RepeatedEnum_Color(value) + return nil +} + +type GoEnum struct { + Foo *FOO `protobuf:"varint,1,req,name=foo,enum=testdata.FOO" json:"foo,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoEnum) Reset() { *m = GoEnum{} } +func (m *GoEnum) String() string { return proto.CompactTextString(m) } +func (*GoEnum) ProtoMessage() {} + +func (m *GoEnum) GetFoo() FOO { + if m != nil && m.Foo != nil { + return *m.Foo + } + return 0 +} + +type GoTestField struct { + Label *string `protobuf:"bytes,1,req" json:"Label,omitempty"` + Type *string `protobuf:"bytes,2,req" json:"Type,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTestField) Reset() { *m = GoTestField{} } +func (m *GoTestField) String() string { return proto.CompactTextString(m) } +func (*GoTestField) ProtoMessage() {} + +func (m *GoTestField) GetLabel() string { + if m != nil && m.Label != nil { + return *m.Label + } + return "" +} + +func (m *GoTestField) GetType() string { + if m != nil && m.Type != nil { + return *m.Type + } + return "" +} + +type GoTest struct { + Kind *GoTest_KIND `protobuf:"varint,1,req,enum=testdata.GoTest_KIND" json:"Kind,omitempty"` + Table *string `protobuf:"bytes,2,opt" json:"Table,omitempty"` + Param *int32 `protobuf:"varint,3,opt" json:"Param,omitempty"` + RequiredField *GoTestField `protobuf:"bytes,4,req" json:"RequiredField,omitempty"` + RepeatedField []*GoTestField `protobuf:"bytes,5,rep" json:"RepeatedField,omitempty"` + OptionalField *GoTestField `protobuf:"bytes,6,opt" json:"OptionalField,omitempty"` + F_BoolRequired *bool `protobuf:"varint,10,req,name=F_Bool_required" json:"F_Bool_required,omitempty"` + F_Int32Required *int32 `protobuf:"varint,11,req,name=F_Int32_required" json:"F_Int32_required,omitempty"` + F_Int64Required *int64 `protobuf:"varint,12,req,name=F_Int64_required" json:"F_Int64_required,omitempty"` + F_Fixed32Required *uint32 `protobuf:"fixed32,13,req,name=F_Fixed32_required" json:"F_Fixed32_required,omitempty"` + F_Fixed64Required *uint64 `protobuf:"fixed64,14,req,name=F_Fixed64_required" json:"F_Fixed64_required,omitempty"` + F_Uint32Required *uint32 `protobuf:"varint,15,req,name=F_Uint32_required" json:"F_Uint32_required,omitempty"` + F_Uint64Required *uint64 `protobuf:"varint,16,req,name=F_Uint64_required" json:"F_Uint64_required,omitempty"` + F_FloatRequired *float32 `protobuf:"fixed32,17,req,name=F_Float_required" json:"F_Float_required,omitempty"` + F_DoubleRequired *float64 `protobuf:"fixed64,18,req,name=F_Double_required" json:"F_Double_required,omitempty"` + F_StringRequired *string `protobuf:"bytes,19,req,name=F_String_required" json:"F_String_required,omitempty"` + F_BytesRequired []byte `protobuf:"bytes,101,req,name=F_Bytes_required" json:"F_Bytes_required,omitempty"` + F_Sint32Required *int32 `protobuf:"zigzag32,102,req,name=F_Sint32_required" json:"F_Sint32_required,omitempty"` + F_Sint64Required *int64 `protobuf:"zigzag64,103,req,name=F_Sint64_required" json:"F_Sint64_required,omitempty"` + F_BoolRepeated []bool `protobuf:"varint,20,rep,name=F_Bool_repeated" json:"F_Bool_repeated,omitempty"` + F_Int32Repeated []int32 `protobuf:"varint,21,rep,name=F_Int32_repeated" json:"F_Int32_repeated,omitempty"` + F_Int64Repeated []int64 `protobuf:"varint,22,rep,name=F_Int64_repeated" json:"F_Int64_repeated,omitempty"` + F_Fixed32Repeated []uint32 `protobuf:"fixed32,23,rep,name=F_Fixed32_repeated" json:"F_Fixed32_repeated,omitempty"` + F_Fixed64Repeated []uint64 `protobuf:"fixed64,24,rep,name=F_Fixed64_repeated" json:"F_Fixed64_repeated,omitempty"` + F_Uint32Repeated []uint32 `protobuf:"varint,25,rep,name=F_Uint32_repeated" json:"F_Uint32_repeated,omitempty"` + F_Uint64Repeated []uint64 `protobuf:"varint,26,rep,name=F_Uint64_repeated" json:"F_Uint64_repeated,omitempty"` + F_FloatRepeated []float32 `protobuf:"fixed32,27,rep,name=F_Float_repeated" json:"F_Float_repeated,omitempty"` + F_DoubleRepeated []float64 `protobuf:"fixed64,28,rep,name=F_Double_repeated" json:"F_Double_repeated,omitempty"` + F_StringRepeated []string `protobuf:"bytes,29,rep,name=F_String_repeated" json:"F_String_repeated,omitempty"` + F_BytesRepeated [][]byte `protobuf:"bytes,201,rep,name=F_Bytes_repeated" json:"F_Bytes_repeated,omitempty"` + F_Sint32Repeated []int32 `protobuf:"zigzag32,202,rep,name=F_Sint32_repeated" json:"F_Sint32_repeated,omitempty"` + F_Sint64Repeated []int64 `protobuf:"zigzag64,203,rep,name=F_Sint64_repeated" json:"F_Sint64_repeated,omitempty"` + F_BoolOptional *bool `protobuf:"varint,30,opt,name=F_Bool_optional" json:"F_Bool_optional,omitempty"` + F_Int32Optional *int32 `protobuf:"varint,31,opt,name=F_Int32_optional" json:"F_Int32_optional,omitempty"` + F_Int64Optional *int64 `protobuf:"varint,32,opt,name=F_Int64_optional" json:"F_Int64_optional,omitempty"` + F_Fixed32Optional *uint32 `protobuf:"fixed32,33,opt,name=F_Fixed32_optional" json:"F_Fixed32_optional,omitempty"` + F_Fixed64Optional *uint64 `protobuf:"fixed64,34,opt,name=F_Fixed64_optional" json:"F_Fixed64_optional,omitempty"` + F_Uint32Optional *uint32 `protobuf:"varint,35,opt,name=F_Uint32_optional" json:"F_Uint32_optional,omitempty"` + F_Uint64Optional *uint64 `protobuf:"varint,36,opt,name=F_Uint64_optional" json:"F_Uint64_optional,omitempty"` + F_FloatOptional *float32 `protobuf:"fixed32,37,opt,name=F_Float_optional" json:"F_Float_optional,omitempty"` + F_DoubleOptional *float64 `protobuf:"fixed64,38,opt,name=F_Double_optional" json:"F_Double_optional,omitempty"` + F_StringOptional *string `protobuf:"bytes,39,opt,name=F_String_optional" json:"F_String_optional,omitempty"` + F_BytesOptional []byte `protobuf:"bytes,301,opt,name=F_Bytes_optional" json:"F_Bytes_optional,omitempty"` + F_Sint32Optional *int32 `protobuf:"zigzag32,302,opt,name=F_Sint32_optional" json:"F_Sint32_optional,omitempty"` + F_Sint64Optional *int64 `protobuf:"zigzag64,303,opt,name=F_Sint64_optional" json:"F_Sint64_optional,omitempty"` + F_BoolDefaulted *bool `protobuf:"varint,40,opt,name=F_Bool_defaulted,def=1" json:"F_Bool_defaulted,omitempty"` + F_Int32Defaulted *int32 `protobuf:"varint,41,opt,name=F_Int32_defaulted,def=32" json:"F_Int32_defaulted,omitempty"` + F_Int64Defaulted *int64 `protobuf:"varint,42,opt,name=F_Int64_defaulted,def=64" json:"F_Int64_defaulted,omitempty"` + F_Fixed32Defaulted *uint32 `protobuf:"fixed32,43,opt,name=F_Fixed32_defaulted,def=320" json:"F_Fixed32_defaulted,omitempty"` + F_Fixed64Defaulted *uint64 `protobuf:"fixed64,44,opt,name=F_Fixed64_defaulted,def=640" json:"F_Fixed64_defaulted,omitempty"` + F_Uint32Defaulted *uint32 `protobuf:"varint,45,opt,name=F_Uint32_defaulted,def=3200" json:"F_Uint32_defaulted,omitempty"` + F_Uint64Defaulted *uint64 `protobuf:"varint,46,opt,name=F_Uint64_defaulted,def=6400" json:"F_Uint64_defaulted,omitempty"` + F_FloatDefaulted *float32 `protobuf:"fixed32,47,opt,name=F_Float_defaulted,def=314159" json:"F_Float_defaulted,omitempty"` + F_DoubleDefaulted *float64 `protobuf:"fixed64,48,opt,name=F_Double_defaulted,def=271828" json:"F_Double_defaulted,omitempty"` + F_StringDefaulted *string `protobuf:"bytes,49,opt,name=F_String_defaulted,def=hello, \"world!\"\n" json:"F_String_defaulted,omitempty"` + F_BytesDefaulted []byte `protobuf:"bytes,401,opt,name=F_Bytes_defaulted,def=Bignose" json:"F_Bytes_defaulted,omitempty"` + F_Sint32Defaulted *int32 `protobuf:"zigzag32,402,opt,name=F_Sint32_defaulted,def=-32" json:"F_Sint32_defaulted,omitempty"` + F_Sint64Defaulted *int64 `protobuf:"zigzag64,403,opt,name=F_Sint64_defaulted,def=-64" json:"F_Sint64_defaulted,omitempty"` + F_BoolRepeatedPacked []bool `protobuf:"varint,50,rep,packed,name=F_Bool_repeated_packed" json:"F_Bool_repeated_packed,omitempty"` + F_Int32RepeatedPacked []int32 `protobuf:"varint,51,rep,packed,name=F_Int32_repeated_packed" json:"F_Int32_repeated_packed,omitempty"` + F_Int64RepeatedPacked []int64 `protobuf:"varint,52,rep,packed,name=F_Int64_repeated_packed" json:"F_Int64_repeated_packed,omitempty"` + F_Fixed32RepeatedPacked []uint32 `protobuf:"fixed32,53,rep,packed,name=F_Fixed32_repeated_packed" json:"F_Fixed32_repeated_packed,omitempty"` + F_Fixed64RepeatedPacked []uint64 `protobuf:"fixed64,54,rep,packed,name=F_Fixed64_repeated_packed" json:"F_Fixed64_repeated_packed,omitempty"` + F_Uint32RepeatedPacked []uint32 `protobuf:"varint,55,rep,packed,name=F_Uint32_repeated_packed" json:"F_Uint32_repeated_packed,omitempty"` + F_Uint64RepeatedPacked []uint64 `protobuf:"varint,56,rep,packed,name=F_Uint64_repeated_packed" json:"F_Uint64_repeated_packed,omitempty"` + F_FloatRepeatedPacked []float32 `protobuf:"fixed32,57,rep,packed,name=F_Float_repeated_packed" json:"F_Float_repeated_packed,omitempty"` + F_DoubleRepeatedPacked []float64 `protobuf:"fixed64,58,rep,packed,name=F_Double_repeated_packed" json:"F_Double_repeated_packed,omitempty"` + F_Sint32RepeatedPacked []int32 `protobuf:"zigzag32,502,rep,packed,name=F_Sint32_repeated_packed" json:"F_Sint32_repeated_packed,omitempty"` + F_Sint64RepeatedPacked []int64 `protobuf:"zigzag64,503,rep,packed,name=F_Sint64_repeated_packed" json:"F_Sint64_repeated_packed,omitempty"` + Requiredgroup *GoTest_RequiredGroup `protobuf:"group,70,req,name=RequiredGroup" json:"requiredgroup,omitempty"` + Repeatedgroup []*GoTest_RepeatedGroup `protobuf:"group,80,rep,name=RepeatedGroup" json:"repeatedgroup,omitempty"` + Optionalgroup *GoTest_OptionalGroup `protobuf:"group,90,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest) Reset() { *m = GoTest{} } +func (m *GoTest) String() string { return proto.CompactTextString(m) } +func (*GoTest) ProtoMessage() {} + +const Default_GoTest_F_BoolDefaulted bool = true +const Default_GoTest_F_Int32Defaulted int32 = 32 +const Default_GoTest_F_Int64Defaulted int64 = 64 +const Default_GoTest_F_Fixed32Defaulted uint32 = 320 +const Default_GoTest_F_Fixed64Defaulted uint64 = 640 +const Default_GoTest_F_Uint32Defaulted uint32 = 3200 +const Default_GoTest_F_Uint64Defaulted uint64 = 6400 +const Default_GoTest_F_FloatDefaulted float32 = 314159 +const Default_GoTest_F_DoubleDefaulted float64 = 271828 +const Default_GoTest_F_StringDefaulted string = "hello, \"world!\"\n" + +var Default_GoTest_F_BytesDefaulted []byte = []byte("Bignose") + +const Default_GoTest_F_Sint32Defaulted int32 = -32 +const Default_GoTest_F_Sint64Defaulted int64 = -64 + +func (m *GoTest) GetKind() GoTest_KIND { + if m != nil && m.Kind != nil { + return *m.Kind + } + return 0 +} + +func (m *GoTest) GetTable() string { + if m != nil && m.Table != nil { + return *m.Table + } + return "" +} + +func (m *GoTest) GetParam() int32 { + if m != nil && m.Param != nil { + return *m.Param + } + return 0 +} + +func (m *GoTest) GetRequiredField() *GoTestField { + if m != nil { + return m.RequiredField + } + return nil +} + +func (m *GoTest) GetRepeatedField() []*GoTestField { + if m != nil { + return m.RepeatedField + } + return nil +} + +func (m *GoTest) GetOptionalField() *GoTestField { + if m != nil { + return m.OptionalField + } + return nil +} + +func (m *GoTest) GetF_BoolRequired() bool { + if m != nil && m.F_BoolRequired != nil { + return *m.F_BoolRequired + } + return false +} + +func (m *GoTest) GetF_Int32Required() int32 { + if m != nil && m.F_Int32Required != nil { + return *m.F_Int32Required + } + return 0 +} + +func (m *GoTest) GetF_Int64Required() int64 { + if m != nil && m.F_Int64Required != nil { + return *m.F_Int64Required + } + return 0 +} + +func (m *GoTest) GetF_Fixed32Required() uint32 { + if m != nil && m.F_Fixed32Required != nil { + return *m.F_Fixed32Required + } + return 0 +} + +func (m *GoTest) GetF_Fixed64Required() uint64 { + if m != nil && m.F_Fixed64Required != nil { + return *m.F_Fixed64Required + } + return 0 +} + +func (m *GoTest) GetF_Uint32Required() uint32 { + if m != nil && m.F_Uint32Required != nil { + return *m.F_Uint32Required + } + return 0 +} + +func (m *GoTest) GetF_Uint64Required() uint64 { + if m != nil && m.F_Uint64Required != nil { + return *m.F_Uint64Required + } + return 0 +} + +func (m *GoTest) GetF_FloatRequired() float32 { + if m != nil && m.F_FloatRequired != nil { + return *m.F_FloatRequired + } + return 0 +} + +func (m *GoTest) GetF_DoubleRequired() float64 { + if m != nil && m.F_DoubleRequired != nil { + return *m.F_DoubleRequired + } + return 0 +} + +func (m *GoTest) GetF_StringRequired() string { + if m != nil && m.F_StringRequired != nil { + return *m.F_StringRequired + } + return "" +} + +func (m *GoTest) GetF_BytesRequired() []byte { + if m != nil { + return m.F_BytesRequired + } + return nil +} + +func (m *GoTest) GetF_Sint32Required() int32 { + if m != nil && m.F_Sint32Required != nil { + return *m.F_Sint32Required + } + return 0 +} + +func (m *GoTest) GetF_Sint64Required() int64 { + if m != nil && m.F_Sint64Required != nil { + return *m.F_Sint64Required + } + return 0 +} + +func (m *GoTest) GetF_BoolRepeated() []bool { + if m != nil { + return m.F_BoolRepeated + } + return nil +} + +func (m *GoTest) GetF_Int32Repeated() []int32 { + if m != nil { + return m.F_Int32Repeated + } + return nil +} + +func (m *GoTest) GetF_Int64Repeated() []int64 { + if m != nil { + return m.F_Int64Repeated + } + return nil +} + +func (m *GoTest) GetF_Fixed32Repeated() []uint32 { + if m != nil { + return m.F_Fixed32Repeated + } + return nil +} + +func (m *GoTest) GetF_Fixed64Repeated() []uint64 { + if m != nil { + return m.F_Fixed64Repeated + } + return nil +} + +func (m *GoTest) GetF_Uint32Repeated() []uint32 { + if m != nil { + return m.F_Uint32Repeated + } + return nil +} + +func (m *GoTest) GetF_Uint64Repeated() []uint64 { + if m != nil { + return m.F_Uint64Repeated + } + return nil +} + +func (m *GoTest) GetF_FloatRepeated() []float32 { + if m != nil { + return m.F_FloatRepeated + } + return nil +} + +func (m *GoTest) GetF_DoubleRepeated() []float64 { + if m != nil { + return m.F_DoubleRepeated + } + return nil +} + +func (m *GoTest) GetF_StringRepeated() []string { + if m != nil { + return m.F_StringRepeated + } + return nil +} + +func (m *GoTest) GetF_BytesRepeated() [][]byte { + if m != nil { + return m.F_BytesRepeated + } + return nil +} + +func (m *GoTest) GetF_Sint32Repeated() []int32 { + if m != nil { + return m.F_Sint32Repeated + } + return nil +} + +func (m *GoTest) GetF_Sint64Repeated() []int64 { + if m != nil { + return m.F_Sint64Repeated + } + return nil +} + +func (m *GoTest) GetF_BoolOptional() bool { + if m != nil && m.F_BoolOptional != nil { + return *m.F_BoolOptional + } + return false +} + +func (m *GoTest) GetF_Int32Optional() int32 { + if m != nil && m.F_Int32Optional != nil { + return *m.F_Int32Optional + } + return 0 +} + +func (m *GoTest) GetF_Int64Optional() int64 { + if m != nil && m.F_Int64Optional != nil { + return *m.F_Int64Optional + } + return 0 +} + +func (m *GoTest) GetF_Fixed32Optional() uint32 { + if m != nil && m.F_Fixed32Optional != nil { + return *m.F_Fixed32Optional + } + return 0 +} + +func (m *GoTest) GetF_Fixed64Optional() uint64 { + if m != nil && m.F_Fixed64Optional != nil { + return *m.F_Fixed64Optional + } + return 0 +} + +func (m *GoTest) GetF_Uint32Optional() uint32 { + if m != nil && m.F_Uint32Optional != nil { + return *m.F_Uint32Optional + } + return 0 +} + +func (m *GoTest) GetF_Uint64Optional() uint64 { + if m != nil && m.F_Uint64Optional != nil { + return *m.F_Uint64Optional + } + return 0 +} + +func (m *GoTest) GetF_FloatOptional() float32 { + if m != nil && m.F_FloatOptional != nil { + return *m.F_FloatOptional + } + return 0 +} + +func (m *GoTest) GetF_DoubleOptional() float64 { + if m != nil && m.F_DoubleOptional != nil { + return *m.F_DoubleOptional + } + return 0 +} + +func (m *GoTest) GetF_StringOptional() string { + if m != nil && m.F_StringOptional != nil { + return *m.F_StringOptional + } + return "" +} + +func (m *GoTest) GetF_BytesOptional() []byte { + if m != nil { + return m.F_BytesOptional + } + return nil +} + +func (m *GoTest) GetF_Sint32Optional() int32 { + if m != nil && m.F_Sint32Optional != nil { + return *m.F_Sint32Optional + } + return 0 +} + +func (m *GoTest) GetF_Sint64Optional() int64 { + if m != nil && m.F_Sint64Optional != nil { + return *m.F_Sint64Optional + } + return 0 +} + +func (m *GoTest) GetF_BoolDefaulted() bool { + if m != nil && m.F_BoolDefaulted != nil { + return *m.F_BoolDefaulted + } + return Default_GoTest_F_BoolDefaulted +} + +func (m *GoTest) GetF_Int32Defaulted() int32 { + if m != nil && m.F_Int32Defaulted != nil { + return *m.F_Int32Defaulted + } + return Default_GoTest_F_Int32Defaulted +} + +func (m *GoTest) GetF_Int64Defaulted() int64 { + if m != nil && m.F_Int64Defaulted != nil { + return *m.F_Int64Defaulted + } + return Default_GoTest_F_Int64Defaulted +} + +func (m *GoTest) GetF_Fixed32Defaulted() uint32 { + if m != nil && m.F_Fixed32Defaulted != nil { + return *m.F_Fixed32Defaulted + } + return Default_GoTest_F_Fixed32Defaulted +} + +func (m *GoTest) GetF_Fixed64Defaulted() uint64 { + if m != nil && m.F_Fixed64Defaulted != nil { + return *m.F_Fixed64Defaulted + } + return Default_GoTest_F_Fixed64Defaulted +} + +func (m *GoTest) GetF_Uint32Defaulted() uint32 { + if m != nil && m.F_Uint32Defaulted != nil { + return *m.F_Uint32Defaulted + } + return Default_GoTest_F_Uint32Defaulted +} + +func (m *GoTest) GetF_Uint64Defaulted() uint64 { + if m != nil && m.F_Uint64Defaulted != nil { + return *m.F_Uint64Defaulted + } + return Default_GoTest_F_Uint64Defaulted +} + +func (m *GoTest) GetF_FloatDefaulted() float32 { + if m != nil && m.F_FloatDefaulted != nil { + return *m.F_FloatDefaulted + } + return Default_GoTest_F_FloatDefaulted +} + +func (m *GoTest) GetF_DoubleDefaulted() float64 { + if m != nil && m.F_DoubleDefaulted != nil { + return *m.F_DoubleDefaulted + } + return Default_GoTest_F_DoubleDefaulted +} + +func (m *GoTest) GetF_StringDefaulted() string { + if m != nil && m.F_StringDefaulted != nil { + return *m.F_StringDefaulted + } + return Default_GoTest_F_StringDefaulted +} + +func (m *GoTest) GetF_BytesDefaulted() []byte { + if m != nil && m.F_BytesDefaulted != nil { + return m.F_BytesDefaulted + } + return append([]byte(nil), Default_GoTest_F_BytesDefaulted...) +} + +func (m *GoTest) GetF_Sint32Defaulted() int32 { + if m != nil && m.F_Sint32Defaulted != nil { + return *m.F_Sint32Defaulted + } + return Default_GoTest_F_Sint32Defaulted +} + +func (m *GoTest) GetF_Sint64Defaulted() int64 { + if m != nil && m.F_Sint64Defaulted != nil { + return *m.F_Sint64Defaulted + } + return Default_GoTest_F_Sint64Defaulted +} + +func (m *GoTest) GetF_BoolRepeatedPacked() []bool { + if m != nil { + return m.F_BoolRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Int32RepeatedPacked() []int32 { + if m != nil { + return m.F_Int32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Int64RepeatedPacked() []int64 { + if m != nil { + return m.F_Int64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Fixed32RepeatedPacked() []uint32 { + if m != nil { + return m.F_Fixed32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Fixed64RepeatedPacked() []uint64 { + if m != nil { + return m.F_Fixed64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Uint32RepeatedPacked() []uint32 { + if m != nil { + return m.F_Uint32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Uint64RepeatedPacked() []uint64 { + if m != nil { + return m.F_Uint64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_FloatRepeatedPacked() []float32 { + if m != nil { + return m.F_FloatRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_DoubleRepeatedPacked() []float64 { + if m != nil { + return m.F_DoubleRepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Sint32RepeatedPacked() []int32 { + if m != nil { + return m.F_Sint32RepeatedPacked + } + return nil +} + +func (m *GoTest) GetF_Sint64RepeatedPacked() []int64 { + if m != nil { + return m.F_Sint64RepeatedPacked + } + return nil +} + +func (m *GoTest) GetRequiredgroup() *GoTest_RequiredGroup { + if m != nil { + return m.Requiredgroup + } + return nil +} + +func (m *GoTest) GetRepeatedgroup() []*GoTest_RepeatedGroup { + if m != nil { + return m.Repeatedgroup + } + return nil +} + +func (m *GoTest) GetOptionalgroup() *GoTest_OptionalGroup { + if m != nil { + return m.Optionalgroup + } + return nil +} + +type GoTest_RequiredGroup struct { + RequiredField *string `protobuf:"bytes,71,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_RequiredGroup) Reset() { *m = GoTest_RequiredGroup{} } + +func (m *GoTest_RequiredGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +type GoTest_RepeatedGroup struct { + RequiredField *string `protobuf:"bytes,81,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_RepeatedGroup) Reset() { *m = GoTest_RepeatedGroup{} } + +func (m *GoTest_RepeatedGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +type GoTest_OptionalGroup struct { + RequiredField *string `protobuf:"bytes,91,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoTest_OptionalGroup) Reset() { *m = GoTest_OptionalGroup{} } + +func (m *GoTest_OptionalGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +type GoSkipTest struct { + SkipInt32 *int32 `protobuf:"varint,11,req,name=skip_int32" json:"skip_int32,omitempty"` + SkipFixed32 *uint32 `protobuf:"fixed32,12,req,name=skip_fixed32" json:"skip_fixed32,omitempty"` + SkipFixed64 *uint64 `protobuf:"fixed64,13,req,name=skip_fixed64" json:"skip_fixed64,omitempty"` + SkipString *string `protobuf:"bytes,14,req,name=skip_string" json:"skip_string,omitempty"` + Skipgroup *GoSkipTest_SkipGroup `protobuf:"group,15,req,name=SkipGroup" json:"skipgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoSkipTest) Reset() { *m = GoSkipTest{} } +func (m *GoSkipTest) String() string { return proto.CompactTextString(m) } +func (*GoSkipTest) ProtoMessage() {} + +func (m *GoSkipTest) GetSkipInt32() int32 { + if m != nil && m.SkipInt32 != nil { + return *m.SkipInt32 + } + return 0 +} + +func (m *GoSkipTest) GetSkipFixed32() uint32 { + if m != nil && m.SkipFixed32 != nil { + return *m.SkipFixed32 + } + return 0 +} + +func (m *GoSkipTest) GetSkipFixed64() uint64 { + if m != nil && m.SkipFixed64 != nil { + return *m.SkipFixed64 + } + return 0 +} + +func (m *GoSkipTest) GetSkipString() string { + if m != nil && m.SkipString != nil { + return *m.SkipString + } + return "" +} + +func (m *GoSkipTest) GetSkipgroup() *GoSkipTest_SkipGroup { + if m != nil { + return m.Skipgroup + } + return nil +} + +type GoSkipTest_SkipGroup struct { + GroupInt32 *int32 `protobuf:"varint,16,req,name=group_int32" json:"group_int32,omitempty"` + GroupString *string `protobuf:"bytes,17,req,name=group_string" json:"group_string,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GoSkipTest_SkipGroup) Reset() { *m = GoSkipTest_SkipGroup{} } + +func (m *GoSkipTest_SkipGroup) GetGroupInt32() int32 { + if m != nil && m.GroupInt32 != nil { + return *m.GroupInt32 + } + return 0 +} + +func (m *GoSkipTest_SkipGroup) GetGroupString() string { + if m != nil && m.GroupString != nil { + return *m.GroupString + } + return "" +} + +type NonPackedTest struct { + A []int32 `protobuf:"varint,1,rep,name=a" json:"a,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NonPackedTest) Reset() { *m = NonPackedTest{} } +func (m *NonPackedTest) String() string { return proto.CompactTextString(m) } +func (*NonPackedTest) ProtoMessage() {} + +func (m *NonPackedTest) GetA() []int32 { + if m != nil { + return m.A + } + return nil +} + +type PackedTest struct { + B []int32 `protobuf:"varint,1,rep,packed,name=b" json:"b,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PackedTest) Reset() { *m = PackedTest{} } +func (m *PackedTest) String() string { return proto.CompactTextString(m) } +func (*PackedTest) ProtoMessage() {} + +func (m *PackedTest) GetB() []int32 { + if m != nil { + return m.B + } + return nil +} + +type MaxTag struct { + LastField *string `protobuf:"bytes,536870911,opt,name=last_field" json:"last_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MaxTag) Reset() { *m = MaxTag{} } +func (m *MaxTag) String() string { return proto.CompactTextString(m) } +func (*MaxTag) ProtoMessage() {} + +func (m *MaxTag) GetLastField() string { + if m != nil && m.LastField != nil { + return *m.LastField + } + return "" +} + +type OldMessage struct { + Nested *OldMessage_Nested `protobuf:"bytes,1,opt,name=nested" json:"nested,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OldMessage) Reset() { *m = OldMessage{} } +func (m *OldMessage) String() string { return proto.CompactTextString(m) } +func (*OldMessage) ProtoMessage() {} + +func (m *OldMessage) GetNested() *OldMessage_Nested { + if m != nil { + return m.Nested + } + return nil +} + +type OldMessage_Nested struct { + Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OldMessage_Nested) Reset() { *m = OldMessage_Nested{} } +func (m *OldMessage_Nested) String() string { return proto.CompactTextString(m) } +func (*OldMessage_Nested) ProtoMessage() {} + +func (m *OldMessage_Nested) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +type NewMessage struct { + Nested *NewMessage_Nested `protobuf:"bytes,1,opt,name=nested" json:"nested,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NewMessage) Reset() { *m = NewMessage{} } +func (m *NewMessage) String() string { return proto.CompactTextString(m) } +func (*NewMessage) ProtoMessage() {} + +func (m *NewMessage) GetNested() *NewMessage_Nested { + if m != nil { + return m.Nested + } + return nil +} + +type NewMessage_Nested struct { + Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + FoodGroup *string `protobuf:"bytes,2,opt,name=food_group" json:"food_group,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NewMessage_Nested) Reset() { *m = NewMessage_Nested{} } +func (m *NewMessage_Nested) String() string { return proto.CompactTextString(m) } +func (*NewMessage_Nested) ProtoMessage() {} + +func (m *NewMessage_Nested) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *NewMessage_Nested) GetFoodGroup() string { + if m != nil && m.FoodGroup != nil { + return *m.FoodGroup + } + return "" +} + +type InnerMessage struct { + Host *string `protobuf:"bytes,1,req,name=host" json:"host,omitempty"` + Port *int32 `protobuf:"varint,2,opt,name=port,def=4000" json:"port,omitempty"` + Connected *bool `protobuf:"varint,3,opt,name=connected" json:"connected,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *InnerMessage) Reset() { *m = InnerMessage{} } +func (m *InnerMessage) String() string { return proto.CompactTextString(m) } +func (*InnerMessage) ProtoMessage() {} + +const Default_InnerMessage_Port int32 = 4000 + +func (m *InnerMessage) GetHost() string { + if m != nil && m.Host != nil { + return *m.Host + } + return "" +} + +func (m *InnerMessage) GetPort() int32 { + if m != nil && m.Port != nil { + return *m.Port + } + return Default_InnerMessage_Port +} + +func (m *InnerMessage) GetConnected() bool { + if m != nil && m.Connected != nil { + return *m.Connected + } + return false +} + +type OtherMessage struct { + Key *int64 `protobuf:"varint,1,opt,name=key" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value" json:"value,omitempty"` + Weight *float32 `protobuf:"fixed32,3,opt,name=weight" json:"weight,omitempty"` + Inner *InnerMessage `protobuf:"bytes,4,opt,name=inner" json:"inner,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *OtherMessage) Reset() { *m = OtherMessage{} } +func (m *OtherMessage) String() string { return proto.CompactTextString(m) } +func (*OtherMessage) ProtoMessage() {} + +func (m *OtherMessage) GetKey() int64 { + if m != nil && m.Key != nil { + return *m.Key + } + return 0 +} + +func (m *OtherMessage) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *OtherMessage) GetWeight() float32 { + if m != nil && m.Weight != nil { + return *m.Weight + } + return 0 +} + +func (m *OtherMessage) GetInner() *InnerMessage { + if m != nil { + return m.Inner + } + return nil +} + +type MyMessage struct { + Count *int32 `protobuf:"varint,1,req,name=count" json:"count,omitempty"` + Name *string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` + Quote *string `protobuf:"bytes,3,opt,name=quote" json:"quote,omitempty"` + Pet []string `protobuf:"bytes,4,rep,name=pet" json:"pet,omitempty"` + Inner *InnerMessage `protobuf:"bytes,5,opt,name=inner" json:"inner,omitempty"` + Others []*OtherMessage `protobuf:"bytes,6,rep,name=others" json:"others,omitempty"` + Bikeshed *MyMessage_Color `protobuf:"varint,7,opt,name=bikeshed,enum=testdata.MyMessage_Color" json:"bikeshed,omitempty"` + Somegroup *MyMessage_SomeGroup `protobuf:"group,8,opt,name=SomeGroup" json:"somegroup,omitempty"` + RepBytes [][]byte `protobuf:"bytes,10,rep,name=rep_bytes" json:"rep_bytes,omitempty"` + Bigfloat *float64 `protobuf:"fixed64,11,opt,name=bigfloat" json:"bigfloat,omitempty"` + XXX_extensions map[int32]proto.Extension `json:"-"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MyMessage) Reset() { *m = MyMessage{} } +func (m *MyMessage) String() string { return proto.CompactTextString(m) } +func (*MyMessage) ProtoMessage() {} + +var extRange_MyMessage = []proto.ExtensionRange{ + {100, 536870911}, +} + +func (*MyMessage) ExtensionRangeArray() []proto.ExtensionRange { + return extRange_MyMessage +} +func (m *MyMessage) ExtensionMap() map[int32]proto.Extension { + if m.XXX_extensions == nil { + m.XXX_extensions = make(map[int32]proto.Extension) + } + return m.XXX_extensions +} + +func (m *MyMessage) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *MyMessage) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *MyMessage) GetQuote() string { + if m != nil && m.Quote != nil { + return *m.Quote + } + return "" +} + +func (m *MyMessage) GetPet() []string { + if m != nil { + return m.Pet + } + return nil +} + +func (m *MyMessage) GetInner() *InnerMessage { + if m != nil { + return m.Inner + } + return nil +} + +func (m *MyMessage) GetOthers() []*OtherMessage { + if m != nil { + return m.Others + } + return nil +} + +func (m *MyMessage) GetBikeshed() MyMessage_Color { + if m != nil && m.Bikeshed != nil { + return *m.Bikeshed + } + return 0 +} + +func (m *MyMessage) GetSomegroup() *MyMessage_SomeGroup { + if m != nil { + return m.Somegroup + } + return nil +} + +func (m *MyMessage) GetRepBytes() [][]byte { + if m != nil { + return m.RepBytes + } + return nil +} + +func (m *MyMessage) GetBigfloat() float64 { + if m != nil && m.Bigfloat != nil { + return *m.Bigfloat + } + return 0 +} + +type MyMessage_SomeGroup struct { + GroupField *int32 `protobuf:"varint,9,opt,name=group_field" json:"group_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MyMessage_SomeGroup) Reset() { *m = MyMessage_SomeGroup{} } + +func (m *MyMessage_SomeGroup) GetGroupField() int32 { + if m != nil && m.GroupField != nil { + return *m.GroupField + } + return 0 +} + +type Ext struct { + Data *string `protobuf:"bytes,1,opt,name=data" json:"data,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Ext) Reset() { *m = Ext{} } +func (m *Ext) String() string { return proto.CompactTextString(m) } +func (*Ext) ProtoMessage() {} + +func (m *Ext) GetData() string { + if m != nil && m.Data != nil { + return *m.Data + } + return "" +} + +var E_Ext_More = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*Ext)(nil), + Field: 103, + Name: "testdata.Ext.more", + Tag: "bytes,103,opt,name=more", +} + +var E_Ext_Text = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*string)(nil), + Field: 104, + Name: "testdata.Ext.text", + Tag: "bytes,104,opt,name=text", +} + +var E_Ext_Number = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: (*int32)(nil), + Field: 105, + Name: "testdata.Ext.number", + Tag: "varint,105,opt,name=number", +} + +type MessageList struct { + Message []*MessageList_Message `protobuf:"group,1,rep" json:"message,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MessageList) Reset() { *m = MessageList{} } +func (m *MessageList) String() string { return proto.CompactTextString(m) } +func (*MessageList) ProtoMessage() {} + +func (m *MessageList) GetMessage() []*MessageList_Message { + if m != nil { + return m.Message + } + return nil +} + +type MessageList_Message struct { + Name *string `protobuf:"bytes,2,req,name=name" json:"name,omitempty"` + Count *int32 `protobuf:"varint,3,req,name=count" json:"count,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MessageList_Message) Reset() { *m = MessageList_Message{} } + +func (m *MessageList_Message) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *MessageList_Message) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +type Strings struct { + StringField *string `protobuf:"bytes,1,opt,name=string_field" json:"string_field,omitempty"` + BytesField []byte `protobuf:"bytes,2,opt,name=bytes_field" json:"bytes_field,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Strings) Reset() { *m = Strings{} } +func (m *Strings) String() string { return proto.CompactTextString(m) } +func (*Strings) ProtoMessage() {} + +func (m *Strings) GetStringField() string { + if m != nil && m.StringField != nil { + return *m.StringField + } + return "" +} + +func (m *Strings) GetBytesField() []byte { + if m != nil { + return m.BytesField + } + return nil +} + +type Defaults struct { + F_Bool *bool `protobuf:"varint,1,opt,def=1" json:"F_Bool,omitempty"` + F_Int32 *int32 `protobuf:"varint,2,opt,def=32" json:"F_Int32,omitempty"` + F_Int64 *int64 `protobuf:"varint,3,opt,def=64" json:"F_Int64,omitempty"` + F_Fixed32 *uint32 `protobuf:"fixed32,4,opt,def=320" json:"F_Fixed32,omitempty"` + F_Fixed64 *uint64 `protobuf:"fixed64,5,opt,def=640" json:"F_Fixed64,omitempty"` + F_Uint32 *uint32 `protobuf:"varint,6,opt,def=3200" json:"F_Uint32,omitempty"` + F_Uint64 *uint64 `protobuf:"varint,7,opt,def=6400" json:"F_Uint64,omitempty"` + F_Float *float32 `protobuf:"fixed32,8,opt,def=314159" json:"F_Float,omitempty"` + F_Double *float64 `protobuf:"fixed64,9,opt,def=271828" json:"F_Double,omitempty"` + F_String *string `protobuf:"bytes,10,opt,def=hello, \"world!\"\n" json:"F_String,omitempty"` + F_Bytes []byte `protobuf:"bytes,11,opt,def=Bignose" json:"F_Bytes,omitempty"` + F_Sint32 *int32 `protobuf:"zigzag32,12,opt,def=-32" json:"F_Sint32,omitempty"` + F_Sint64 *int64 `protobuf:"zigzag64,13,opt,def=-64" json:"F_Sint64,omitempty"` + F_Enum *Defaults_Color `protobuf:"varint,14,opt,enum=testdata.Defaults_Color,def=1" json:"F_Enum,omitempty"` + F_Pinf *float32 `protobuf:"fixed32,15,opt,def=inf" json:"F_Pinf,omitempty"` + F_Ninf *float32 `protobuf:"fixed32,16,opt,def=-inf" json:"F_Ninf,omitempty"` + F_Nan *float32 `protobuf:"fixed32,17,opt,def=nan" json:"F_Nan,omitempty"` + Sub *SubDefaults `protobuf:"bytes,18,opt,name=sub" json:"sub,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Defaults) Reset() { *m = Defaults{} } +func (m *Defaults) String() string { return proto.CompactTextString(m) } +func (*Defaults) ProtoMessage() {} + +const Default_Defaults_F_Bool bool = true +const Default_Defaults_F_Int32 int32 = 32 +const Default_Defaults_F_Int64 int64 = 64 +const Default_Defaults_F_Fixed32 uint32 = 320 +const Default_Defaults_F_Fixed64 uint64 = 640 +const Default_Defaults_F_Uint32 uint32 = 3200 +const Default_Defaults_F_Uint64 uint64 = 6400 +const Default_Defaults_F_Float float32 = 314159 +const Default_Defaults_F_Double float64 = 271828 +const Default_Defaults_F_String string = "hello, \"world!\"\n" + +var Default_Defaults_F_Bytes []byte = []byte("Bignose") + +const Default_Defaults_F_Sint32 int32 = -32 +const Default_Defaults_F_Sint64 int64 = -64 +const Default_Defaults_F_Enum Defaults_Color = Defaults_GREEN + +var Default_Defaults_F_Pinf float32 = float32(math.Inf(1)) +var Default_Defaults_F_Ninf float32 = float32(math.Inf(-1)) +var Default_Defaults_F_Nan float32 = float32(math.NaN()) + +func (m *Defaults) GetF_Bool() bool { + if m != nil && m.F_Bool != nil { + return *m.F_Bool + } + return Default_Defaults_F_Bool +} + +func (m *Defaults) GetF_Int32() int32 { + if m != nil && m.F_Int32 != nil { + return *m.F_Int32 + } + return Default_Defaults_F_Int32 +} + +func (m *Defaults) GetF_Int64() int64 { + if m != nil && m.F_Int64 != nil { + return *m.F_Int64 + } + return Default_Defaults_F_Int64 +} + +func (m *Defaults) GetF_Fixed32() uint32 { + if m != nil && m.F_Fixed32 != nil { + return *m.F_Fixed32 + } + return Default_Defaults_F_Fixed32 +} + +func (m *Defaults) GetF_Fixed64() uint64 { + if m != nil && m.F_Fixed64 != nil { + return *m.F_Fixed64 + } + return Default_Defaults_F_Fixed64 +} + +func (m *Defaults) GetF_Uint32() uint32 { + if m != nil && m.F_Uint32 != nil { + return *m.F_Uint32 + } + return Default_Defaults_F_Uint32 +} + +func (m *Defaults) GetF_Uint64() uint64 { + if m != nil && m.F_Uint64 != nil { + return *m.F_Uint64 + } + return Default_Defaults_F_Uint64 +} + +func (m *Defaults) GetF_Float() float32 { + if m != nil && m.F_Float != nil { + return *m.F_Float + } + return Default_Defaults_F_Float +} + +func (m *Defaults) GetF_Double() float64 { + if m != nil && m.F_Double != nil { + return *m.F_Double + } + return Default_Defaults_F_Double +} + +func (m *Defaults) GetF_String() string { + if m != nil && m.F_String != nil { + return *m.F_String + } + return Default_Defaults_F_String +} + +func (m *Defaults) GetF_Bytes() []byte { + if m != nil && m.F_Bytes != nil { + return m.F_Bytes + } + return append([]byte(nil), Default_Defaults_F_Bytes...) +} + +func (m *Defaults) GetF_Sint32() int32 { + if m != nil && m.F_Sint32 != nil { + return *m.F_Sint32 + } + return Default_Defaults_F_Sint32 +} + +func (m *Defaults) GetF_Sint64() int64 { + if m != nil && m.F_Sint64 != nil { + return *m.F_Sint64 + } + return Default_Defaults_F_Sint64 +} + +func (m *Defaults) GetF_Enum() Defaults_Color { + if m != nil && m.F_Enum != nil { + return *m.F_Enum + } + return Default_Defaults_F_Enum +} + +func (m *Defaults) GetF_Pinf() float32 { + if m != nil && m.F_Pinf != nil { + return *m.F_Pinf + } + return Default_Defaults_F_Pinf +} + +func (m *Defaults) GetF_Ninf() float32 { + if m != nil && m.F_Ninf != nil { + return *m.F_Ninf + } + return Default_Defaults_F_Ninf +} + +func (m *Defaults) GetF_Nan() float32 { + if m != nil && m.F_Nan != nil { + return *m.F_Nan + } + return Default_Defaults_F_Nan +} + +func (m *Defaults) GetSub() *SubDefaults { + if m != nil { + return m.Sub + } + return nil +} + +type SubDefaults struct { + N *int64 `protobuf:"varint,1,opt,name=n,def=7" json:"n,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SubDefaults) Reset() { *m = SubDefaults{} } +func (m *SubDefaults) String() string { return proto.CompactTextString(m) } +func (*SubDefaults) ProtoMessage() {} + +const Default_SubDefaults_N int64 = 7 + +func (m *SubDefaults) GetN() int64 { + if m != nil && m.N != nil { + return *m.N + } + return Default_SubDefaults_N +} + +type RepeatedEnum struct { + Color []RepeatedEnum_Color `protobuf:"varint,1,rep,name=color,enum=testdata.RepeatedEnum_Color" json:"color,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *RepeatedEnum) Reset() { *m = RepeatedEnum{} } +func (m *RepeatedEnum) String() string { return proto.CompactTextString(m) } +func (*RepeatedEnum) ProtoMessage() {} + +func (m *RepeatedEnum) GetColor() []RepeatedEnum_Color { + if m != nil { + return m.Color + } + return nil +} + +type MoreRepeated struct { + Bools []bool `protobuf:"varint,1,rep,name=bools" json:"bools,omitempty"` + BoolsPacked []bool `protobuf:"varint,2,rep,packed,name=bools_packed" json:"bools_packed,omitempty"` + Ints []int32 `protobuf:"varint,3,rep,name=ints" json:"ints,omitempty"` + IntsPacked []int32 `protobuf:"varint,4,rep,packed,name=ints_packed" json:"ints_packed,omitempty"` + Strings []string `protobuf:"bytes,5,rep,name=strings" json:"strings,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *MoreRepeated) Reset() { *m = MoreRepeated{} } +func (m *MoreRepeated) String() string { return proto.CompactTextString(m) } +func (*MoreRepeated) ProtoMessage() {} + +func (m *MoreRepeated) GetBools() []bool { + if m != nil { + return m.Bools + } + return nil +} + +func (m *MoreRepeated) GetBoolsPacked() []bool { + if m != nil { + return m.BoolsPacked + } + return nil +} + +func (m *MoreRepeated) GetInts() []int32 { + if m != nil { + return m.Ints + } + return nil +} + +func (m *MoreRepeated) GetIntsPacked() []int32 { + if m != nil { + return m.IntsPacked + } + return nil +} + +func (m *MoreRepeated) GetStrings() []string { + if m != nil { + return m.Strings + } + return nil +} + +type GroupOld struct { + G *GroupOld_G `protobuf:"group,1,opt" json:"g,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupOld) Reset() { *m = GroupOld{} } +func (m *GroupOld) String() string { return proto.CompactTextString(m) } +func (*GroupOld) ProtoMessage() {} + +func (m *GroupOld) GetG() *GroupOld_G { + if m != nil { + return m.G + } + return nil +} + +type GroupOld_G struct { + X *int32 `protobuf:"varint,2,opt,name=x" json:"x,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupOld_G) Reset() { *m = GroupOld_G{} } + +func (m *GroupOld_G) GetX() int32 { + if m != nil && m.X != nil { + return *m.X + } + return 0 +} + +type GroupNew struct { + G *GroupNew_G `protobuf:"group,1,opt" json:"g,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupNew) Reset() { *m = GroupNew{} } +func (m *GroupNew) String() string { return proto.CompactTextString(m) } +func (*GroupNew) ProtoMessage() {} + +func (m *GroupNew) GetG() *GroupNew_G { + if m != nil { + return m.G + } + return nil +} + +type GroupNew_G struct { + X *int32 `protobuf:"varint,2,opt,name=x" json:"x,omitempty"` + Y *int32 `protobuf:"varint,3,opt,name=y" json:"y,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupNew_G) Reset() { *m = GroupNew_G{} } + +func (m *GroupNew_G) GetX() int32 { + if m != nil && m.X != nil { + return *m.X + } + return 0 +} + +func (m *GroupNew_G) GetY() int32 { + if m != nil && m.Y != nil { + return *m.Y + } + return 0 +} + +var E_Greeting = &proto.ExtensionDesc{ + ExtendedType: (*MyMessage)(nil), + ExtensionType: ([]string)(nil), + Field: 106, + Name: "testdata.greeting", + Tag: "bytes,106,rep,name=greeting", +} + +func init() { + proto.RegisterEnum("testdata.FOO", FOO_name, FOO_value) + proto.RegisterEnum("testdata.GoTest_KIND", GoTest_KIND_name, GoTest_KIND_value) + proto.RegisterEnum("testdata.MyMessage_Color", MyMessage_Color_name, MyMessage_Color_value) + proto.RegisterEnum("testdata.Defaults_Color", Defaults_Color_name, Defaults_Color_value) + proto.RegisterEnum("testdata.RepeatedEnum_Color", RepeatedEnum_Color_name, RepeatedEnum_Color_value) + proto.RegisterExtension(E_Ext_More) + proto.RegisterExtension(E_Ext_Text) + proto.RegisterExtension(E_Ext_Number) + proto.RegisterExtension(E_Greeting) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.proto b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.proto new file mode 100644 index 00000000000..4f4b3d168ed --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.proto @@ -0,0 +1,420 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// A feature-rich test file for the protocol compiler and libraries. + +syntax = "proto2"; + +package testdata; + +enum FOO { FOO1 = 1; }; + +message GoEnum { + required FOO foo = 1; +} + +message GoTestField { + required string Label = 1; + required string Type = 2; +} + +message GoTest { + // An enum, for completeness. + enum KIND { + VOID = 0; + + // Basic types + BOOL = 1; + BYTES = 2; + FINGERPRINT = 3; + FLOAT = 4; + INT = 5; + STRING = 6; + TIME = 7; + + // Groupings + TUPLE = 8; + ARRAY = 9; + MAP = 10; + + // Table types + TABLE = 11; + + // Functions + FUNCTION = 12; // last tag + }; + + // Some typical parameters + required KIND Kind = 1; + optional string Table = 2; + optional int32 Param = 3; + + // Required, repeated and optional foreign fields. + required GoTestField RequiredField = 4; + repeated GoTestField RepeatedField = 5; + optional GoTestField OptionalField = 6; + + // Required fields of all basic types + required bool F_Bool_required = 10; + required int32 F_Int32_required = 11; + required int64 F_Int64_required = 12; + required fixed32 F_Fixed32_required = 13; + required fixed64 F_Fixed64_required = 14; + required uint32 F_Uint32_required = 15; + required uint64 F_Uint64_required = 16; + required float F_Float_required = 17; + required double F_Double_required = 18; + required string F_String_required = 19; + required bytes F_Bytes_required = 101; + required sint32 F_Sint32_required = 102; + required sint64 F_Sint64_required = 103; + + // Repeated fields of all basic types + repeated bool F_Bool_repeated = 20; + repeated int32 F_Int32_repeated = 21; + repeated int64 F_Int64_repeated = 22; + repeated fixed32 F_Fixed32_repeated = 23; + repeated fixed64 F_Fixed64_repeated = 24; + repeated uint32 F_Uint32_repeated = 25; + repeated uint64 F_Uint64_repeated = 26; + repeated float F_Float_repeated = 27; + repeated double F_Double_repeated = 28; + repeated string F_String_repeated = 29; + repeated bytes F_Bytes_repeated = 201; + repeated sint32 F_Sint32_repeated = 202; + repeated sint64 F_Sint64_repeated = 203; + + // Optional fields of all basic types + optional bool F_Bool_optional = 30; + optional int32 F_Int32_optional = 31; + optional int64 F_Int64_optional = 32; + optional fixed32 F_Fixed32_optional = 33; + optional fixed64 F_Fixed64_optional = 34; + optional uint32 F_Uint32_optional = 35; + optional uint64 F_Uint64_optional = 36; + optional float F_Float_optional = 37; + optional double F_Double_optional = 38; + optional string F_String_optional = 39; + optional bytes F_Bytes_optional = 301; + optional sint32 F_Sint32_optional = 302; + optional sint64 F_Sint64_optional = 303; + + // Default-valued fields of all basic types + optional bool F_Bool_defaulted = 40 [default=true]; + optional int32 F_Int32_defaulted = 41 [default=32]; + optional int64 F_Int64_defaulted = 42 [default=64]; + optional fixed32 F_Fixed32_defaulted = 43 [default=320]; + optional fixed64 F_Fixed64_defaulted = 44 [default=640]; + optional uint32 F_Uint32_defaulted = 45 [default=3200]; + optional uint64 F_Uint64_defaulted = 46 [default=6400]; + optional float F_Float_defaulted = 47 [default=314159.]; + optional double F_Double_defaulted = 48 [default=271828.]; + optional string F_String_defaulted = 49 [default="hello, \"world!\"\n"]; + optional bytes F_Bytes_defaulted = 401 [default="Bignose"]; + optional sint32 F_Sint32_defaulted = 402 [default = -32]; + optional sint64 F_Sint64_defaulted = 403 [default = -64]; + + // Packed repeated fields (no string or bytes). + repeated bool F_Bool_repeated_packed = 50 [packed=true]; + repeated int32 F_Int32_repeated_packed = 51 [packed=true]; + repeated int64 F_Int64_repeated_packed = 52 [packed=true]; + repeated fixed32 F_Fixed32_repeated_packed = 53 [packed=true]; + repeated fixed64 F_Fixed64_repeated_packed = 54 [packed=true]; + repeated uint32 F_Uint32_repeated_packed = 55 [packed=true]; + repeated uint64 F_Uint64_repeated_packed = 56 [packed=true]; + repeated float F_Float_repeated_packed = 57 [packed=true]; + repeated double F_Double_repeated_packed = 58 [packed=true]; + repeated sint32 F_Sint32_repeated_packed = 502 [packed=true]; + repeated sint64 F_Sint64_repeated_packed = 503 [packed=true]; + + // Required, repeated, and optional groups. + required group RequiredGroup = 70 { + required string RequiredField = 71; + }; + + repeated group RepeatedGroup = 80 { + required string RequiredField = 81; + }; + + optional group OptionalGroup = 90 { + required string RequiredField = 91; + }; +} + +// For testing skipping of unrecognized fields. +// Numbers are all big, larger than tag numbers in GoTestField, +// the message used in the corresponding test. +message GoSkipTest { + required int32 skip_int32 = 11; + required fixed32 skip_fixed32 = 12; + required fixed64 skip_fixed64 = 13; + required string skip_string = 14; + required group SkipGroup = 15 { + required int32 group_int32 = 16; + required string group_string = 17; + } +} + +// For testing packed/non-packed decoder switching. +// A serialized instance of one should be deserializable as the other. +message NonPackedTest { + repeated int32 a = 1; +} + +message PackedTest { + repeated int32 b = 1 [packed=true]; +} + +message MaxTag { + // Maximum possible tag number. + optional string last_field = 536870911; +} + +message OldMessage { + message Nested { + optional string name = 1; + } + optional Nested nested = 1; +} + +// NewMessage is wire compatible with OldMessage; +// imagine it as a future version. +message NewMessage { + message Nested { + optional string name = 1; + optional string food_group = 2; + } + optional Nested nested = 1; +} + +// Smaller tests for ASCII formatting. + +message InnerMessage { + required string host = 1; + optional int32 port = 2 [default=4000]; + optional bool connected = 3; +} + +message OtherMessage { + optional int64 key = 1; + optional bytes value = 2; + optional float weight = 3; + optional InnerMessage inner = 4; +} + +message MyMessage { + required int32 count = 1; + optional string name = 2; + optional string quote = 3; + repeated string pet = 4; + optional InnerMessage inner = 5; + repeated OtherMessage others = 6; + repeated InnerMessage rep_inner = 12; + + enum Color { + RED = 0; + GREEN = 1; + BLUE = 2; + }; + optional Color bikeshed = 7; + + optional group SomeGroup = 8 { + optional int32 group_field = 9; + } + + // This field becomes [][]byte in the generated code. + repeated bytes rep_bytes = 10; + + optional double bigfloat = 11; + + extensions 100 to max; +} + +message Ext { + extend MyMessage { + optional Ext more = 103; + optional string text = 104; + optional int32 number = 105; + } + + optional string data = 1; +} + +extend MyMessage { + repeated string greeting = 106; +} + +message MyMessageSet { + option message_set_wire_format = true; + extensions 100 to max; +} + +message Empty { +} + +extend MyMessageSet { + optional Empty x201 = 201; + optional Empty x202 = 202; + optional Empty x203 = 203; + optional Empty x204 = 204; + optional Empty x205 = 205; + optional Empty x206 = 206; + optional Empty x207 = 207; + optional Empty x208 = 208; + optional Empty x209 = 209; + optional Empty x210 = 210; + optional Empty x211 = 211; + optional Empty x212 = 212; + optional Empty x213 = 213; + optional Empty x214 = 214; + optional Empty x215 = 215; + optional Empty x216 = 216; + optional Empty x217 = 217; + optional Empty x218 = 218; + optional Empty x219 = 219; + optional Empty x220 = 220; + optional Empty x221 = 221; + optional Empty x222 = 222; + optional Empty x223 = 223; + optional Empty x224 = 224; + optional Empty x225 = 225; + optional Empty x226 = 226; + optional Empty x227 = 227; + optional Empty x228 = 228; + optional Empty x229 = 229; + optional Empty x230 = 230; + optional Empty x231 = 231; + optional Empty x232 = 232; + optional Empty x233 = 233; + optional Empty x234 = 234; + optional Empty x235 = 235; + optional Empty x236 = 236; + optional Empty x237 = 237; + optional Empty x238 = 238; + optional Empty x239 = 239; + optional Empty x240 = 240; + optional Empty x241 = 241; + optional Empty x242 = 242; + optional Empty x243 = 243; + optional Empty x244 = 244; + optional Empty x245 = 245; + optional Empty x246 = 246; + optional Empty x247 = 247; + optional Empty x248 = 248; + optional Empty x249 = 249; + optional Empty x250 = 250; +} + +message MessageList { + repeated group Message = 1 { + required string name = 2; + required int32 count = 3; + } +} + +message Strings { + optional string string_field = 1; + optional bytes bytes_field = 2; +} + +message Defaults { + enum Color { + RED = 0; + GREEN = 1; + BLUE = 2; + } + + // Default-valued fields of all basic types. + // Same as GoTest, but copied here to make testing easier. + optional bool F_Bool = 1 [default=true]; + optional int32 F_Int32 = 2 [default=32]; + optional int64 F_Int64 = 3 [default=64]; + optional fixed32 F_Fixed32 = 4 [default=320]; + optional fixed64 F_Fixed64 = 5 [default=640]; + optional uint32 F_Uint32 = 6 [default=3200]; + optional uint64 F_Uint64 = 7 [default=6400]; + optional float F_Float = 8 [default=314159.]; + optional double F_Double = 9 [default=271828.]; + optional string F_String = 10 [default="hello, \"world!\"\n"]; + optional bytes F_Bytes = 11 [default="Bignose"]; + optional sint32 F_Sint32 = 12 [default=-32]; + optional sint64 F_Sint64 = 13 [default=-64]; + optional Color F_Enum = 14 [default=GREEN]; + + // More fields with crazy defaults. + optional float F_Pinf = 15 [default=inf]; + optional float F_Ninf = 16 [default=-inf]; + optional float F_Nan = 17 [default=nan]; + + // Sub-message. + optional SubDefaults sub = 18; +} + +message SubDefaults { + optional int64 n = 1 [default=7]; +} + +message RepeatedEnum { + enum Color { + RED = 1; + } + repeated Color color = 1; +} + +message MoreRepeated { + repeated bool bools = 1; + repeated bool bools_packed = 2 [packed=true]; + repeated int32 ints = 3; + repeated int32 ints_packed = 4 [packed=true]; + repeated int64 int64s_packed = 7 [packed=true]; + repeated string strings = 5; + repeated fixed32 fixeds = 6; +} + +// GroupOld and GroupNew have the same wire format. +// GroupNew has a new field inside a group. + +message GroupOld { + optional group G = 101 { + optional int32 x = 2; + } +} + +message GroupNew { + optional group G = 101 { + optional int32 x = 2; + optional int32 y = 3; + } +} + +message FloatingPoint { + required double f = 1; +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text.go new file mode 100644 index 00000000000..e88badd72c6 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text.go @@ -0,0 +1,736 @@ +// Extensions for Protocol Buffers to create more go like structures. +// +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +// Functions for writing the text protocol buffer format. + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "math" + "os" + "reflect" + "sort" + "strings" +) + +var ( + newline = []byte("\n") + spaces = []byte(" ") + gtNewline = []byte(">\n") + endBraceNewline = []byte("}\n") + backslashN = []byte{'\\', 'n'} + backslashR = []byte{'\\', 'r'} + backslashT = []byte{'\\', 't'} + backslashDQ = []byte{'\\', '"'} + backslashBS = []byte{'\\', '\\'} + posInf = []byte("inf") + negInf = []byte("-inf") + nan = []byte("nan") +) + +type writer interface { + io.Writer + WriteByte(byte) error +} + +// textWriter is an io.Writer that tracks its indentation level. +type textWriter struct { + ind int + complete bool // if the current position is a complete line + compact bool // whether to write out as a one-liner + w writer +} + +// textMarshaler is implemented by Messages that can marshal themsleves. +// It is identical to encoding.TextMarshaler, introduced in go 1.2, +// which will eventually replace it. +type textMarshaler interface { + MarshalText() (text []byte, err error) +} + +func (w *textWriter) WriteString(s string) (n int, err error) { + if !strings.Contains(s, "\n") { + if !w.compact && w.complete { + w.writeIndent() + } + w.complete = false + return io.WriteString(w.w, s) + } + // WriteString is typically called without newlines, so this + // codepath and its copy are rare. We copy to avoid + // duplicating all of Write's logic here. + return w.Write([]byte(s)) +} + +func (w *textWriter) Write(p []byte) (n int, err error) { + newlines := bytes.Count(p, newline) + if newlines == 0 { + if !w.compact && w.complete { + w.writeIndent() + } + n, err = w.w.Write(p) + w.complete = false + return n, err + } + + frags := bytes.SplitN(p, newline, newlines+1) + if w.compact { + for i, frag := range frags { + if i > 0 { + if err := w.w.WriteByte(' '); err != nil { + return n, err + } + n++ + } + nn, err := w.w.Write(frag) + n += nn + if err != nil { + return n, err + } + } + return n, nil + } + + for i, frag := range frags { + if w.complete { + w.writeIndent() + } + nn, err := w.w.Write(frag) + n += nn + if err != nil { + return n, err + } + if i+1 < len(frags) { + if err := w.w.WriteByte('\n'); err != nil { + return n, err + } + n++ + } + } + w.complete = len(frags[len(frags)-1]) == 0 + return n, nil +} + +func (w *textWriter) WriteByte(c byte) error { + if w.compact && c == '\n' { + c = ' ' + } + if !w.compact && w.complete { + w.writeIndent() + } + err := w.w.WriteByte(c) + w.complete = c == '\n' + return err +} + +func (w *textWriter) indent() { w.ind++ } + +func (w *textWriter) unindent() { + if w.ind == 0 { + log.Printf("proto: textWriter unindented too far") + return + } + w.ind-- +} + +func writeName(w *textWriter, props *Properties) error { + if _, err := w.WriteString(props.OrigName); err != nil { + return err + } + if props.Wire != "group" { + return w.WriteByte(':') + } + return nil +} + +var ( + messageSetType = reflect.TypeOf((*MessageSet)(nil)).Elem() +) + +// raw is the interface satisfied by RawMessage. +type raw interface { + Bytes() []byte +} + +func writeStruct(w *textWriter, sv reflect.Value) error { + if sv.Type() == messageSetType { + return writeMessageSet(w, sv.Addr().Interface().(*MessageSet)) + } + + st := sv.Type() + sprops := GetProperties(st) + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + props := sprops.Prop[i] + name := st.Field(i).Name + + if strings.HasPrefix(name, "XXX_") { + // There are two XXX_ fields: + // XXX_unrecognized []byte + // XXX_extensions map[int32]proto.Extension + // The first is handled here; + // the second is handled at the bottom of this function. + if name == "XXX_unrecognized" && !fv.IsNil() { + if err := writeUnknownStruct(w, fv.Interface().([]byte)); err != nil { + return err + } + } + continue + } + if fv.Kind() == reflect.Ptr && fv.IsNil() { + // Field not filled in. This could be an optional field or + // a required field that wasn't filled in. Either way, there + // isn't anything we can show for it. + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + // Repeated field that is empty, or a bytes field that is unused. + continue + } + + if props.Repeated && fv.Kind() == reflect.Slice { + // Repeated field. + for j := 0; j < fv.Len(); j++ { + if err := writeName(w, props); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + v := fv.Index(j) + if v.Kind() == reflect.Ptr && v.IsNil() { + // A nil message in a repeated field is not valid, + // but we can handle that more gracefully than panicking. + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + continue + } + if len(props.Enum) > 0 { + if err := writeEnum(w, v, props); err != nil { + return err + } + } else if err := writeAny(w, v, props); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + } + continue + } + + if err := writeName(w, props); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + if b, ok := fv.Interface().(raw); ok { + if err := writeRaw(w, b.Bytes()); err != nil { + return err + } + continue + } + + if len(props.Enum) > 0 { + if err := writeEnum(w, fv, props); err != nil { + return err + } + } else if err := writeAny(w, fv, props); err != nil { + return err + } + + if err := w.WriteByte('\n'); err != nil { + return err + } + } + + // Extensions (the XXX_extensions field). + pv := sv.Addr() + if pv.Type().Implements(extendableProtoType) { + if err := writeExtensions(w, pv); err != nil { + return err + } + } + + return nil +} + +// writeRaw writes an uninterpreted raw message. +func writeRaw(w *textWriter, b []byte) error { + if err := w.WriteByte('<'); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte('\n'); err != nil { + return err + } + } + w.indent() + if err := writeUnknownStruct(w, b); err != nil { + return err + } + w.unindent() + if err := w.WriteByte('>'); err != nil { + return err + } + return nil +} + +// writeAny writes an arbitrary field. +func writeAny(w *textWriter, v reflect.Value, props *Properties) error { + v = reflect.Indirect(v) + + if props != nil && len(props.CustomType) > 0 { + var custom Marshaler = v.Interface().(Marshaler) + data, err := custom.Marshal() + if err != nil { + return err + } + if err := writeString(w, string(data)); err != nil { + return err + } + return nil + } + + // Floats have special cases. + if v.Kind() == reflect.Float32 || v.Kind() == reflect.Float64 { + x := v.Float() + var b []byte + switch { + case math.IsInf(x, 1): + b = posInf + case math.IsInf(x, -1): + b = negInf + case math.IsNaN(x): + b = nan + } + if b != nil { + _, err := w.Write(b) + return err + } + // Other values are handled below. + } + + // We don't attempt to serialise every possible value type; only those + // that can occur in protocol buffers. + switch v.Kind() { + case reflect.Slice: + // Should only be a []byte; repeated fields are handled in writeStruct. + if err := writeString(w, string(v.Interface().([]byte))); err != nil { + return err + } + case reflect.String: + if err := writeString(w, v.String()); err != nil { + return err + } + case reflect.Struct: + // Required/optional group/message. + var bra, ket byte = '<', '>' + if props != nil && props.Wire == "group" { + bra, ket = '{', '}' + } + if err := w.WriteByte(bra); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte('\n'); err != nil { + return err + } + } + w.indent() + if tm, ok := v.Interface().(textMarshaler); ok { + text, err := tm.MarshalText() + if err != nil { + return err + } + if _, err = w.Write(text); err != nil { + return err + } + } else if err := writeStruct(w, v); err != nil { + return err + } + w.unindent() + if err := w.WriteByte(ket); err != nil { + return err + } + default: + _, err := fmt.Fprint(w, v.Interface()) + return err + } + return nil +} + +// equivalent to C's isprint. +func isprint(c byte) bool { + return c >= 0x20 && c < 0x7f +} + +// writeString writes a string in the protocol buffer text format. +// It is similar to strconv.Quote except we don't use Go escape sequences, +// we treat the string as a byte sequence, and we use octal escapes. +// These differences are to maintain interoperability with the other +// languages' implementations of the text format. +func writeString(w *textWriter, s string) error { + // use WriteByte here to get any needed indent + if err := w.WriteByte('"'); err != nil { + return err + } + // Loop over the bytes, not the runes. + for i := 0; i < len(s); i++ { + var err error + // Divergence from C++: we don't escape apostrophes. + // There's no need to escape them, and the C++ parser + // copes with a naked apostrophe. + switch c := s[i]; c { + case '\n': + _, err = w.w.Write(backslashN) + case '\r': + _, err = w.w.Write(backslashR) + case '\t': + _, err = w.w.Write(backslashT) + case '"': + _, err = w.w.Write(backslashDQ) + case '\\': + _, err = w.w.Write(backslashBS) + default: + if isprint(c) { + err = w.w.WriteByte(c) + } else { + _, err = fmt.Fprintf(w.w, "\\%03o", c) + } + } + if err != nil { + return err + } + } + return w.WriteByte('"') +} + +func writeMessageSet(w *textWriter, ms *MessageSet) error { + for _, item := range ms.Item { + id := *item.TypeId + if msd, ok := messageSetMap[id]; ok { + // Known message set type. + if _, err := fmt.Fprintf(w, "[%s]: <\n", msd.name); err != nil { + return err + } + w.indent() + + pb := reflect.New(msd.t.Elem()) + if err := Unmarshal(item.Message, pb.Interface().(Message)); err != nil { + if _, err := fmt.Fprintf(w, "/* bad message: %v */\n", err); err != nil { + return err + } + } else { + if err := writeStruct(w, pb.Elem()); err != nil { + return err + } + } + } else { + // Unknown type. + if _, err := fmt.Fprintf(w, "[%d]: <\n", id); err != nil { + return err + } + w.indent() + if err := writeUnknownStruct(w, item.Message); err != nil { + return err + } + } + w.unindent() + if _, err := w.Write(gtNewline); err != nil { + return err + } + } + return nil +} + +func writeUnknownStruct(w *textWriter, data []byte) (err error) { + if !w.compact { + if _, err := fmt.Fprintf(w, "/* %d unknown bytes */\n", len(data)); err != nil { + return err + } + } + b := NewBuffer(data) + for b.index < len(b.buf) { + x, err := b.DecodeVarint() + if err != nil { + _, err := fmt.Fprintf(w, "/* %v */\n", err) + return err + } + wire, tag := x&7, x>>3 + if wire == WireEndGroup { + w.unindent() + if _, err := w.Write(endBraceNewline); err != nil { + return err + } + continue + } + if _, err := fmt.Fprint(w, tag); err != nil { + return err + } + if wire != WireStartGroup { + if err := w.WriteByte(':'); err != nil { + return err + } + } + if !w.compact || wire == WireStartGroup { + if err := w.WriteByte(' '); err != nil { + return err + } + } + switch wire { + case WireBytes: + buf, e := b.DecodeRawBytes(false) + if e == nil { + _, err = fmt.Fprintf(w, "%q", buf) + } else { + _, err = fmt.Fprintf(w, "/* %v */", e) + } + case WireFixed32: + x, err = b.DecodeFixed32() + err = writeUnknownInt(w, x, err) + case WireFixed64: + x, err = b.DecodeFixed64() + err = writeUnknownInt(w, x, err) + case WireStartGroup: + err = w.WriteByte('{') + w.indent() + case WireVarint: + x, err = b.DecodeVarint() + err = writeUnknownInt(w, x, err) + default: + _, err = fmt.Fprintf(w, "/* unknown wire type %d */", wire) + } + if err != nil { + return err + } + if err = w.WriteByte('\n'); err != nil { + return err + } + } + return nil +} + +func writeUnknownInt(w *textWriter, x uint64, err error) error { + if err == nil { + _, err = fmt.Fprint(w, x) + } else { + _, err = fmt.Fprintf(w, "/* %v */", err) + } + return err +} + +type int32Slice []int32 + +func (s int32Slice) Len() int { return len(s) } +func (s int32Slice) Less(i, j int) bool { return s[i] < s[j] } +func (s int32Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// writeExtensions writes all the extensions in pv. +// pv is assumed to be a pointer to a protocol message struct that is extendable. +func writeExtensions(w *textWriter, pv reflect.Value) error { + emap := extensionMaps[pv.Type().Elem()] + ep := pv.Interface().(extendableProto) + + // Order the extensions by ID. + // This isn't strictly necessary, but it will give us + // canonical output, which will also make testing easier. + var m map[int32]Extension + if em, ok := ep.(extensionsMap); ok { + m = em.ExtensionMap() + } else if em, ok := ep.(extensionsBytes); ok { + eb := em.GetExtensions() + var err error + m, err = BytesToExtensionsMap(*eb) + if err != nil { + return err + } + } + + ids := make([]int32, 0, len(m)) + for id := range m { + ids = append(ids, id) + } + sort.Sort(int32Slice(ids)) + + for _, extNum := range ids { + ext := m[extNum] + var desc *ExtensionDesc + if emap != nil { + desc = emap[extNum] + } + if desc == nil { + // Unknown extension. + if err := writeUnknownStruct(w, ext.enc); err != nil { + return err + } + continue + } + + pb, err := GetExtension(ep, desc) + if err != nil { + if _, err := fmt.Fprintln(os.Stderr, "proto: failed getting extension: ", err); err != nil { + return err + } + continue + } + + // Repeated extensions will appear as a slice. + if !desc.repeated() { + if err := writeExtension(w, desc.Name, pb); err != nil { + return err + } + } else { + v := reflect.ValueOf(pb) + for i := 0; i < v.Len(); i++ { + if err := writeExtension(w, desc.Name, v.Index(i).Interface()); err != nil { + return err + } + } + } + } + return nil +} + +func writeExtension(w *textWriter, name string, pb interface{}) error { + if _, err := fmt.Fprintf(w, "[%s]:", name); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + if err := writeAny(w, reflect.ValueOf(pb), nil); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + return nil +} + +func (w *textWriter) writeIndent() { + if !w.complete { + return + } + remain := w.ind * 2 + for remain > 0 { + n := remain + if n > len(spaces) { + n = len(spaces) + } + w.w.Write(spaces[:n]) + remain -= n + } + w.complete = false +} + +func marshalText(w io.Writer, pb Message, compact bool) error { + val := reflect.ValueOf(pb) + if pb == nil || val.IsNil() { + w.Write([]byte("")) + return nil + } + var bw *bufio.Writer + ww, ok := w.(writer) + if !ok { + bw = bufio.NewWriter(w) + ww = bw + } + aw := &textWriter{ + w: ww, + complete: true, + compact: compact, + } + + if tm, ok := pb.(textMarshaler); ok { + text, err := tm.MarshalText() + if err != nil { + return err + } + if _, err = aw.Write(text); err != nil { + return err + } + if bw != nil { + return bw.Flush() + } + return nil + } + // Dereference the received pointer so we don't have outer < and >. + v := reflect.Indirect(val) + if err := writeStruct(aw, v); err != nil { + return err + } + if bw != nil { + return bw.Flush() + } + return nil +} + +// MarshalText writes a given protocol buffer in text format. +// The only errors returned are from w. +func MarshalText(w io.Writer, pb Message) error { + return marshalText(w, pb, false) +} + +// MarshalTextString is the same as MarshalText, but returns the string directly. +func MarshalTextString(pb Message) string { + var buf bytes.Buffer + marshalText(&buf, pb, false) + return buf.String() +} + +// CompactText writes a given protocol buffer in compact text format (one line). +func CompactText(w io.Writer, pb Message) error { return marshalText(w, pb, true) } + +// CompactTextString is the same as CompactText, but returns the string directly. +func CompactTextString(pb Message) string { + var buf bytes.Buffer + marshalText(&buf, pb, true) + return buf.String() +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_gogo.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_gogo.go new file mode 100644 index 00000000000..3c4e469b276 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_gogo.go @@ -0,0 +1,55 @@ +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "fmt" + "reflect" +) + +func writeEnum(w *textWriter, v reflect.Value, props *Properties) error { + m, ok := enumStringMaps[props.Enum] + if !ok { + if err := writeAny(w, v, props); err != nil { + return err + } + } + key := int32(0) + if v.Kind() == reflect.Ptr { + key = int32(v.Elem().Int()) + } else { + key = int32(v.Int()) + } + s, ok := m[key] + if !ok { + if err := writeAny(w, v, props); err != nil { + return err + } + } + _, err := fmt.Fprint(w, s) + return err +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser.go new file mode 100644 index 00000000000..37be7c9a61c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser.go @@ -0,0 +1,727 @@ +// Extensions for Protocol Buffers to create more go like structures. +// +// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved. +// http://code.google.com/p/gogoprotobuf/gogoproto +// +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +// Functions for parsing the Text protocol buffer format. +// TODO: message sets. + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "unicode/utf8" +) + +// textUnmarshaler is implemented by Messages that can unmarshal themsleves. +// It is identical to encoding.TextUnmarshaler, introduced in go 1.2, +// which will eventually replace it. +type textUnmarshaler interface { + UnmarshalText(text []byte) error +} + +type ParseError struct { + Message string + Line int // 1-based line number + Offset int // 0-based byte offset from start of input +} + +func (p *ParseError) Error() string { + if p.Line == 1 { + // show offset only for first line + return fmt.Sprintf("line 1.%d: %v", p.Offset, p.Message) + } + return fmt.Sprintf("line %d: %v", p.Line, p.Message) +} + +type token struct { + value string + err *ParseError + line int // line number + offset int // byte number from start of input, not start of line + unquoted string // the unquoted version of value, if it was a quoted string +} + +func (t *token) String() string { + if t.err == nil { + return fmt.Sprintf("%q (line=%d, offset=%d)", t.value, t.line, t.offset) + } + return fmt.Sprintf("parse error: %v", t.err) +} + +type textParser struct { + s string // remaining input + done bool // whether the parsing is finished (success or error) + backed bool // whether back() was called + offset, line int + cur token +} + +func newTextParser(s string) *textParser { + p := new(textParser) + p.s = s + p.line = 1 + p.cur.line = 1 + return p +} + +func (p *textParser) errorf(format string, a ...interface{}) *ParseError { + pe := &ParseError{fmt.Sprintf(format, a...), p.cur.line, p.cur.offset} + p.cur.err = pe + p.done = true + return pe +} + +// Numbers and identifiers are matched by [-+._A-Za-z0-9] +func isIdentOrNumberChar(c byte) bool { + switch { + case 'A' <= c && c <= 'Z', 'a' <= c && c <= 'z': + return true + case '0' <= c && c <= '9': + return true + } + switch c { + case '-', '+', '.', '_': + return true + } + return false +} + +func isWhitespace(c byte) bool { + switch c { + case ' ', '\t', '\n', '\r': + return true + } + return false +} + +func (p *textParser) skipWhitespace() { + i := 0 + for i < len(p.s) && (isWhitespace(p.s[i]) || p.s[i] == '#') { + if p.s[i] == '#' { + // comment; skip to end of line or input + for i < len(p.s) && p.s[i] != '\n' { + i++ + } + if i == len(p.s) { + break + } + } + if p.s[i] == '\n' { + p.line++ + } + i++ + } + p.offset += i + p.s = p.s[i:len(p.s)] + if len(p.s) == 0 { + p.done = true + } +} + +func (p *textParser) advance() { + // Skip whitespace + p.skipWhitespace() + if p.done { + return + } + + // Start of non-whitespace + p.cur.err = nil + p.cur.offset, p.cur.line = p.offset, p.line + p.cur.unquoted = "" + switch p.s[0] { + case '<', '>', '{', '}', ':', '[', ']', ';', ',': + // Single symbol + p.cur.value, p.s = p.s[0:1], p.s[1:len(p.s)] + case '"', '\'': + // Quoted string + i := 1 + for i < len(p.s) && p.s[i] != p.s[0] && p.s[i] != '\n' { + if p.s[i] == '\\' && i+1 < len(p.s) { + // skip escaped char + i++ + } + i++ + } + if i >= len(p.s) || p.s[i] != p.s[0] { + p.errorf("unmatched quote") + return + } + unq, err := unquoteC(p.s[1:i], rune(p.s[0])) + if err != nil { + p.errorf("invalid quoted string %v", p.s[0:i+1]) + return + } + p.cur.value, p.s = p.s[0:i+1], p.s[i+1:len(p.s)] + p.cur.unquoted = unq + default: + i := 0 + for i < len(p.s) && isIdentOrNumberChar(p.s[i]) { + i++ + } + if i == 0 { + p.errorf("unexpected byte %#x", p.s[0]) + return + } + p.cur.value, p.s = p.s[0:i], p.s[i:len(p.s)] + } + p.offset += len(p.cur.value) +} + +var ( + errBadUTF8 = errors.New("proto: bad UTF-8") + errBadHex = errors.New("proto: bad hexadecimal") +) + +func unquoteC(s string, quote rune) (string, error) { + // This is based on C++'s tokenizer.cc. + // Despite its name, this is *not* parsing C syntax. + // For instance, "\0" is an invalid quoted string. + + // Avoid allocation in trivial cases. + simple := true + for _, r := range s { + if r == '\\' || r == quote { + simple = false + break + } + } + if simple { + return s, nil + } + + buf := make([]byte, 0, 3*len(s)/2) + for len(s) > 0 { + r, n := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && n == 1 { + return "", errBadUTF8 + } + s = s[n:] + if r != '\\' { + if r < utf8.RuneSelf { + buf = append(buf, byte(r)) + } else { + buf = append(buf, string(r)...) + } + continue + } + + ch, tail, err := unescape(s) + if err != nil { + return "", err + } + buf = append(buf, ch...) + s = tail + } + return string(buf), nil +} + +func unescape(s string) (ch string, tail string, err error) { + r, n := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && n == 1 { + return "", "", errBadUTF8 + } + s = s[n:] + switch r { + case 'a': + return "\a", s, nil + case 'b': + return "\b", s, nil + case 'f': + return "\f", s, nil + case 'n': + return "\n", s, nil + case 'r': + return "\r", s, nil + case 't': + return "\t", s, nil + case 'v': + return "\v", s, nil + case '?': + return "?", s, nil // trigraph workaround + case '\'', '"', '\\': + return string(r), s, nil + case '0', '1', '2', '3', '4', '5', '6', '7', 'x', 'X': + if len(s) < 2 { + return "", "", fmt.Errorf(`\%c requires 2 following digits`, r) + } + base := 8 + ss := s[:2] + s = s[2:] + if r == 'x' || r == 'X' { + base = 16 + } else { + ss = string(r) + ss + } + i, err := strconv.ParseUint(ss, base, 8) + if err != nil { + return "", "", err + } + return string([]byte{byte(i)}), s, nil + case 'u', 'U': + n := 4 + if r == 'U' { + n = 8 + } + if len(s) < n { + return "", "", fmt.Errorf(`\%c requires %d digits`, r, n) + } + + bs := make([]byte, n/2) + for i := 0; i < n; i += 2 { + a, ok1 := unhex(s[i]) + b, ok2 := unhex(s[i+1]) + if !ok1 || !ok2 { + return "", "", errBadHex + } + bs[i/2] = a<<4 | b + } + s = s[n:] + return string(bs), s, nil + } + return "", "", fmt.Errorf(`unknown escape \%c`, r) +} + +// Adapted from src/pkg/strconv/quote.go. +func unhex(b byte) (v byte, ok bool) { + switch { + case '0' <= b && b <= '9': + return b - '0', true + case 'a' <= b && b <= 'f': + return b - 'a' + 10, true + case 'A' <= b && b <= 'F': + return b - 'A' + 10, true + } + return 0, false +} + +// Back off the parser by one token. Can only be done between calls to next(). +// It makes the next advance() a no-op. +func (p *textParser) back() { p.backed = true } + +// Advances the parser and returns the new current token. +func (p *textParser) next() *token { + if p.backed || p.done { + p.backed = false + return &p.cur + } + p.advance() + if p.done { + p.cur.value = "" + } else if len(p.cur.value) > 0 && p.cur.value[0] == '"' { + // Look for multiple quoted strings separated by whitespace, + // and concatenate them. + cat := p.cur + for { + p.skipWhitespace() + if p.done || p.s[0] != '"' { + break + } + p.advance() + if p.cur.err != nil { + return &p.cur + } + cat.value += " " + p.cur.value + cat.unquoted += p.cur.unquoted + } + p.done = false // parser may have seen EOF, but we want to return cat + p.cur = cat + } + return &p.cur +} + +// Return an error indicating which required field was not set. +func (p *textParser) missingRequiredFieldError(sv reflect.Value) *ParseError { + st := sv.Type() + sprops := GetProperties(st) + for i := 0; i < st.NumField(); i++ { + if !isNil(sv.Field(i)) { + continue + } + + props := sprops.Prop[i] + if props.Required { + return p.errorf("message %v missing required field %q", st, props.OrigName) + } + } + return p.errorf("message %v missing required field", st) // should not happen +} + +// Returns the index in the struct for the named field, as well as the parsed tag properties. +func structFieldByName(st reflect.Type, name string) (int, *Properties, bool) { + sprops := GetProperties(st) + i, ok := sprops.decoderOrigNames[name] + if ok { + return i, sprops.Prop[i], true + } + return -1, nil, false +} + +// Consume a ':' from the input stream (if the next token is a colon), +// returning an error if a colon is needed but not present. +func (p *textParser) checkForColon(props *Properties, typ reflect.Type) *ParseError { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value != ":" { + // Colon is optional when the field is a group or message. + needColon := true + switch props.Wire { + case "group": + needColon = false + case "bytes": + // A "bytes" field is either a message, a string, or a repeated field; + // those three become *T, *string and []T respectively, so we can check for + // this field being a pointer to a non-string. + if typ.Kind() == reflect.Ptr { + // *T or *string + if typ.Elem().Kind() == reflect.String { + break + } + } else if typ.Kind() == reflect.Slice { + // []T or []*T + if typ.Elem().Kind() != reflect.Ptr { + break + } + } + needColon = false + } + if needColon { + return p.errorf("expected ':', found %q", tok.value) + } + p.back() + } + return nil +} + +func (p *textParser) readStruct(sv reflect.Value, terminator string) *ParseError { + st := sv.Type() + reqCount := GetProperties(st).reqCount + // A struct is a sequence of "name: value", terminated by one of + // '>' or '}', or the end of the input. A name may also be + // "[extension]". + for { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == terminator { + break + } + if tok.value == "[" { + // Looks like an extension. + // + // TODO: Check whether we need to handle + // namespace rooted names (e.g. ".something.Foo"). + tok = p.next() + if tok.err != nil { + return tok.err + } + var desc *ExtensionDesc + // This could be faster, but it's functional. + // TODO: Do something smarter than a linear scan. + for _, d := range RegisteredExtensions(reflect.New(st).Interface().(Message)) { + if d.Name == tok.value { + desc = d + break + } + } + if desc == nil { + return p.errorf("unrecognized extension %q", tok.value) + } + // Check the extension terminator. + tok = p.next() + if tok.err != nil { + return tok.err + } + if tok.value != "]" { + return p.errorf("unrecognized extension terminator %q", tok.value) + } + + props := &Properties{} + props.Parse(desc.Tag) + + typ := reflect.TypeOf(desc.ExtensionType) + if err := p.checkForColon(props, typ); err != nil { + return err + } + + rep := desc.repeated() + + // Read the extension structure, and set it in + // the value we're constructing. + var ext reflect.Value + if !rep { + ext = reflect.New(typ).Elem() + } else { + ext = reflect.New(typ.Elem()).Elem() + } + if err := p.readAny(ext, props); err != nil { + return err + } + ep := sv.Addr().Interface().(extendableProto) + if !rep { + SetExtension(ep, desc, ext.Interface()) + } else { + old, err := GetExtension(ep, desc) + var sl reflect.Value + if err == nil { + sl = reflect.ValueOf(old) // existing slice + } else { + sl = reflect.MakeSlice(typ, 0, 1) + } + sl = reflect.Append(sl, ext) + SetExtension(ep, desc, sl.Interface()) + } + } else { + // This is a normal, non-extension field. + fi, props, ok := structFieldByName(st, tok.value) + if !ok { + return p.errorf("unknown field name %q in %v", tok.value, st) + } + + dst := sv.Field(fi) + isDstNil := isNil(dst) + + // Check that it's not already set if it's not a repeated field. + if !props.Repeated && !isDstNil && dst.Kind() == reflect.Ptr { + return p.errorf("non-repeated field %q was repeated", tok.value) + } + + if err := p.checkForColon(props, st.Field(fi).Type); err != nil { + return err + } + + // Parse into the field. + if err := p.readAny(dst, props); err != nil { + return err + } + + if props.Required { + reqCount-- + } + } + + // For backward compatibility, permit a semicolon or comma after a field. + tok = p.next() + if tok.err != nil { + return tok.err + } + if tok.value != ";" && tok.value != "," { + p.back() + } + } + + if reqCount > 0 { + return p.missingRequiredFieldError(sv) + } + return nil +} + +func (p *textParser) readAny(v reflect.Value, props *Properties) *ParseError { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == "" { + return p.errorf("unexpected EOF") + } + if len(props.CustomType) > 0 { + if props.Repeated { + t := reflect.TypeOf(v.Interface()) + if t.Kind() == reflect.Slice { + tc := reflect.TypeOf(new(Marshaler)) + ok := t.Elem().Implements(tc.Elem()) + if ok { + fv := v + flen := fv.Len() + if flen == fv.Cap() { + nav := reflect.MakeSlice(v.Type(), flen, 2*flen+1) + reflect.Copy(nav, fv) + fv.Set(nav) + } + fv.SetLen(flen + 1) + + // Read one. + p.back() + return p.readAny(fv.Index(flen), props) + } + } + } + if reflect.TypeOf(v.Interface()).Kind() == reflect.Ptr { + custom := reflect.New(props.ctype.Elem()).Interface().(Unmarshaler) + err := custom.Unmarshal([]byte(tok.unquoted)) + if err != nil { + return p.errorf("%v %v: %v", err, v.Type(), tok.value) + } + v.Set(reflect.ValueOf(custom)) + } else { + custom := reflect.New(reflect.TypeOf(v.Interface())).Interface().(Unmarshaler) + err := custom.Unmarshal([]byte(tok.unquoted)) + if err != nil { + return p.errorf("%v %v: %v", err, v.Type(), tok.value) + } + v.Set(reflect.Indirect(reflect.ValueOf(custom))) + } + return nil + } + switch fv := v; fv.Kind() { + case reflect.Slice: + at := v.Type() + if at.Elem().Kind() == reflect.Uint8 { + // Special case for []byte + if tok.value[0] != '"' && tok.value[0] != '\'' { + // Deliberately written out here, as the error after + // this switch statement would write "invalid []byte: ...", + // which is not as user-friendly. + return p.errorf("invalid string: %v", tok.value) + } + bytes := []byte(tok.unquoted) + fv.Set(reflect.ValueOf(bytes)) + return nil + } + // Repeated field. May already exist. + flen := fv.Len() + if flen == fv.Cap() { + nav := reflect.MakeSlice(at, flen, 2*flen+1) + reflect.Copy(nav, fv) + fv.Set(nav) + } + fv.SetLen(flen + 1) + + // Read one. + p.back() + return p.readAny(fv.Index(flen), props) + case reflect.Bool: + // Either "true", "false", 1 or 0. + switch tok.value { + case "true", "1": + fv.SetBool(true) + return nil + case "false", "0": + fv.SetBool(false) + return nil + } + case reflect.Float32, reflect.Float64: + v := tok.value + // Ignore 'f' for compatibility with output generated by C++, but don't + // remove 'f' when the value is "-inf" or "inf". + if strings.HasSuffix(v, "f") && tok.value != "-inf" && tok.value != "inf" { + v = v[:len(v)-1] + } + if f, err := strconv.ParseFloat(v, fv.Type().Bits()); err == nil { + fv.SetFloat(f) + return nil + } + case reflect.Int32: + if x, err := strconv.ParseInt(tok.value, 0, 32); err == nil { + fv.SetInt(x) + return nil + } + + if len(props.Enum) == 0 { + break + } + m, ok := enumValueMaps[props.Enum] + if !ok { + break + } + x, ok := m[tok.value] + if !ok { + break + } + fv.SetInt(int64(x)) + return nil + case reflect.Int64: + if x, err := strconv.ParseInt(tok.value, 0, 64); err == nil { + fv.SetInt(x) + return nil + } + + case reflect.Ptr: + // A basic field (indirected through pointer), or a repeated message/group + p.back() + fv.Set(reflect.New(fv.Type().Elem())) + return p.readAny(fv.Elem(), props) + case reflect.String: + if tok.value[0] == '"' || tok.value[0] == '\'' { + fv.SetString(tok.unquoted) + return nil + } + case reflect.Struct: + var terminator string + switch tok.value { + case "{": + terminator = "}" + case "<": + terminator = ">" + default: + return p.errorf("expected '{' or '<', found %q", tok.value) + } + // TODO: Handle nested messages which implement textUnmarshaler. + return p.readStruct(fv, terminator) + case reflect.Uint32: + if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil { + fv.SetUint(uint64(x)) + return nil + } + case reflect.Uint64: + if x, err := strconv.ParseUint(tok.value, 0, 64); err == nil { + fv.SetUint(x) + return nil + } + } + return p.errorf("invalid %v: %v", v.Type(), tok.value) +} + +// UnmarshalText reads a protocol buffer in Text format. UnmarshalText resets pb +// before starting to unmarshal, so any existing data in pb is always removed. +func UnmarshalText(s string, pb Message) error { + if um, ok := pb.(textUnmarshaler); ok { + err := um.UnmarshalText([]byte(s)) + return err + } + pb.Reset() + v := reflect.ValueOf(pb) + if pe := newTextParser(s).readStruct(v.Elem(), ""); pe != nil { + return pe + } + return nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser_test.go new file mode 100644 index 00000000000..cc1e5301aa3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_parser_test.go @@ -0,0 +1,462 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "math" + "reflect" + "testing" + + . "./testdata" + . "code.google.com/p/gogoprotobuf/proto" +) + +type UnmarshalTextTest struct { + in string + err string // if "", no error expected + out *MyMessage +} + +func buildExtStructTest(text string) UnmarshalTextTest { + msg := &MyMessage{ + Count: Int32(42), + } + SetExtension(msg, E_Ext_More, &Ext{ + Data: String("Hello, world!"), + }) + return UnmarshalTextTest{in: text, out: msg} +} + +func buildExtDataTest(text string) UnmarshalTextTest { + msg := &MyMessage{ + Count: Int32(42), + } + SetExtension(msg, E_Ext_Text, String("Hello, world!")) + SetExtension(msg, E_Ext_Number, Int32(1729)) + return UnmarshalTextTest{in: text, out: msg} +} + +func buildExtRepStringTest(text string) UnmarshalTextTest { + msg := &MyMessage{ + Count: Int32(42), + } + if err := SetExtension(msg, E_Greeting, []string{"bula", "hola"}); err != nil { + panic(err) + } + return UnmarshalTextTest{in: text, out: msg} +} + +var unMarshalTextTests = []UnmarshalTextTest{ + // Basic + { + in: " count:42\n name:\"Dave\" ", + out: &MyMessage{ + Count: Int32(42), + Name: String("Dave"), + }, + }, + + // Empty quoted string + { + in: `count:42 name:""`, + out: &MyMessage{ + Count: Int32(42), + Name: String(""), + }, + }, + + // Quoted string concatenation + { + in: `count:42 name: "My name is "` + "\n" + `"elsewhere"`, + out: &MyMessage{ + Count: Int32(42), + Name: String("My name is elsewhere"), + }, + }, + + // Quoted string with escaped apostrophe + { + in: `count:42 name: "HOLIDAY - New Year\'s Day"`, + out: &MyMessage{ + Count: Int32(42), + Name: String("HOLIDAY - New Year's Day"), + }, + }, + + // Quoted string with single quote + { + in: `count:42 name: 'Roger "The Ramster" Ramjet'`, + out: &MyMessage{ + Count: Int32(42), + Name: String(`Roger "The Ramster" Ramjet`), + }, + }, + + // Quoted string with all the accepted special characters from the C++ test + { + in: `count:42 name: ` + "\"\\\"A string with \\' characters \\n and \\r newlines and \\t tabs and \\001 slashes \\\\ and multiple spaces\"", + out: &MyMessage{ + Count: Int32(42), + Name: String("\"A string with ' characters \n and \r newlines and \t tabs and \001 slashes \\ and multiple spaces"), + }, + }, + + // Quoted string with quoted backslash + { + in: `count:42 name: "\\'xyz"`, + out: &MyMessage{ + Count: Int32(42), + Name: String(`\'xyz`), + }, + }, + + // Quoted string with UTF-8 bytes. + { + in: "count:42 name: '\303\277\302\201\xAB'", + out: &MyMessage{ + Count: Int32(42), + Name: String("\303\277\302\201\xAB"), + }, + }, + + // Bad quoted string + { + in: `inner: < host: "\0" >` + "\n", + err: `line 1.15: invalid quoted string "\0"`, + }, + + // Number too large for int64 + { + in: "count: 1 others { key: 123456789012345678901 }", + err: "line 1.23: invalid int64: 123456789012345678901", + }, + + // Number too large for int32 + { + in: "count: 1234567890123", + err: "line 1.7: invalid int32: 1234567890123", + }, + + // Number in hexadecimal + { + in: "count: 0x2beef", + out: &MyMessage{ + Count: Int32(0x2beef), + }, + }, + + // Number in octal + { + in: "count: 024601", + out: &MyMessage{ + Count: Int32(024601), + }, + }, + + // Floating point number with "f" suffix + { + in: "count: 4 others:< weight: 17.0f >", + out: &MyMessage{ + Count: Int32(4), + Others: []*OtherMessage{ + { + Weight: Float32(17), + }, + }, + }, + }, + + // Floating point positive infinity + { + in: "count: 4 bigfloat: inf", + out: &MyMessage{ + Count: Int32(4), + Bigfloat: Float64(math.Inf(1)), + }, + }, + + // Floating point negative infinity + { + in: "count: 4 bigfloat: -inf", + out: &MyMessage{ + Count: Int32(4), + Bigfloat: Float64(math.Inf(-1)), + }, + }, + + // Number too large for float32 + { + in: "others:< weight: 12345678901234567890123456789012345678901234567890 >", + err: "line 1.17: invalid float32: 12345678901234567890123456789012345678901234567890", + }, + + // Number posing as a quoted string + { + in: `inner: < host: 12 >` + "\n", + err: `line 1.15: invalid string: 12`, + }, + + // Quoted string posing as int32 + { + in: `count: "12"`, + err: `line 1.7: invalid int32: "12"`, + }, + + // Quoted string posing a float32 + { + in: `others:< weight: "17.4" >`, + err: `line 1.17: invalid float32: "17.4"`, + }, + + // Enum + { + in: `count:42 bikeshed: BLUE`, + out: &MyMessage{ + Count: Int32(42), + Bikeshed: MyMessage_BLUE.Enum(), + }, + }, + + // Repeated field + { + in: `count:42 pet: "horsey" pet:"bunny"`, + out: &MyMessage{ + Count: Int32(42), + Pet: []string{"horsey", "bunny"}, + }, + }, + + // Repeated message with/without colon and <>/{} + { + in: `count:42 others:{} others{} others:<> others:{}`, + out: &MyMessage{ + Count: Int32(42), + Others: []*OtherMessage{ + {}, + {}, + {}, + {}, + }, + }, + }, + + // Missing colon for inner message + { + in: `count:42 inner < host: "cauchy.syd" >`, + out: &MyMessage{ + Count: Int32(42), + Inner: &InnerMessage{ + Host: String("cauchy.syd"), + }, + }, + }, + + // Missing colon for string field + { + in: `name "Dave"`, + err: `line 1.5: expected ':', found "\"Dave\""`, + }, + + // Missing colon for int32 field + { + in: `count 42`, + err: `line 1.6: expected ':', found "42"`, + }, + + // Missing required field + { + in: ``, + err: `line 1.0: message testdata.MyMessage missing required field "count"`, + }, + + // Repeated non-repeated field + { + in: `name: "Rob" name: "Russ"`, + err: `line 1.12: non-repeated field "name" was repeated`, + }, + + // Group + { + in: `count: 17 SomeGroup { group_field: 12 }`, + out: &MyMessage{ + Count: Int32(17), + Somegroup: &MyMessage_SomeGroup{ + GroupField: Int32(12), + }, + }, + }, + + // Semicolon between fields + { + in: `count:3;name:"Calvin"`, + out: &MyMessage{ + Count: Int32(3), + Name: String("Calvin"), + }, + }, + // Comma between fields + { + in: `count:4,name:"Ezekiel"`, + out: &MyMessage{ + Count: Int32(4), + Name: String("Ezekiel"), + }, + }, + + // Extension + buildExtStructTest(`count: 42 [testdata.Ext.more]:`), + buildExtStructTest(`count: 42 [testdata.Ext.more] {data:"Hello, world!"}`), + buildExtDataTest(`count: 42 [testdata.Ext.text]:"Hello, world!" [testdata.Ext.number]:1729`), + buildExtRepStringTest(`count: 42 [testdata.greeting]:"bula" [testdata.greeting]:"hola"`), + + // Big all-in-one + { + in: "count:42 # Meaning\n" + + `name:"Dave" ` + + `quote:"\"I didn't want to go.\"" ` + + `pet:"bunny" ` + + `pet:"kitty" ` + + `pet:"horsey" ` + + `inner:<` + + ` host:"footrest.syd" ` + + ` port:7001 ` + + ` connected:true ` + + `> ` + + `others:<` + + ` key:3735928559 ` + + ` value:"\x01A\a\f" ` + + `> ` + + `others:<` + + " weight:58.9 # Atomic weight of Co\n" + + ` inner:<` + + ` host:"lesha.mtv" ` + + ` port:8002 ` + + ` >` + + `>`, + out: &MyMessage{ + Count: Int32(42), + Name: String("Dave"), + Quote: String(`"I didn't want to go."`), + Pet: []string{"bunny", "kitty", "horsey"}, + Inner: &InnerMessage{ + Host: String("footrest.syd"), + Port: Int32(7001), + Connected: Bool(true), + }, + Others: []*OtherMessage{ + { + Key: Int64(3735928559), + Value: []byte{0x1, 'A', '\a', '\f'}, + }, + { + Weight: Float32(58.9), + Inner: &InnerMessage{ + Host: String("lesha.mtv"), + Port: Int32(8002), + }, + }, + }, + }, + }, +} + +func TestUnmarshalText(t *testing.T) { + for i, test := range unMarshalTextTests { + pb := new(MyMessage) + err := UnmarshalText(test.in, pb) + if test.err == "" { + // We don't expect failure. + if err != nil { + t.Errorf("Test %d: Unexpected error: %v", i, err) + } else if !reflect.DeepEqual(pb, test.out) { + t.Errorf("Test %d: Incorrect populated \nHave: %v\nWant: %v", + i, pb, test.out) + } + } else { + // We do expect failure. + if err == nil { + t.Errorf("Test %d: Didn't get expected error: %v", i, test.err) + } else if err.Error() != test.err { + t.Errorf("Test %d: Incorrect error.\nHave: %v\nWant: %v", + i, err.Error(), test.err) + } + } + } +} + +func TestUnmarshalTextCustomMessage(t *testing.T) { + msg := &textMessage{} + if err := UnmarshalText("custom", msg); err != nil { + t.Errorf("Unexpected error from custom unmarshal: %v", err) + } + if UnmarshalText("not custom", msg) == nil { + t.Errorf("Didn't get expected error from custom unmarshal") + } +} + +// Regression test; this caused a panic. +func TestRepeatedEnum(t *testing.T) { + pb := new(RepeatedEnum) + if err := UnmarshalText("color: RED", pb); err != nil { + t.Fatal(err) + } + exp := &RepeatedEnum{ + Color: []RepeatedEnum_Color{RepeatedEnum_RED}, + } + if !Equal(pb, exp) { + t.Errorf("Incorrect populated \nHave: %v\nWant: %v", pb, exp) + } +} + +var benchInput string + +func init() { + benchInput = "count: 4\n" + for i := 0; i < 1000; i++ { + benchInput += "pet: \"fido\"\n" + } + + // Check it is valid input. + pb := new(MyMessage) + err := UnmarshalText(benchInput, pb) + if err != nil { + panic("Bad benchmark input: " + err.Error()) + } +} + +func BenchmarkUnmarshalText(b *testing.B) { + pb := new(MyMessage) + for i := 0; i < b.N; i++ { + UnmarshalText(benchInput, pb) + } + b.SetBytes(int64(len(benchInput))) +} diff --git a/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_test.go b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_test.go new file mode 100644 index 00000000000..253d4851d74 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/text_test.go @@ -0,0 +1,408 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// http://code.google.com/p/goprotobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto_test + +import ( + "bytes" + "errors" + "io/ioutil" + "math" + "strings" + "testing" + + "code.google.com/p/gogoprotobuf/proto" + + pb "./testdata" +) + +// textMessage implements the methods that allow it to marshal and unmarshal +// itself as text. +type textMessage struct { +} + +func (*textMessage) MarshalText() ([]byte, error) { + return []byte("custom"), nil +} + +func (*textMessage) UnmarshalText(bytes []byte) error { + if string(bytes) != "custom" { + return errors.New("expected 'custom'") + } + return nil +} + +func (*textMessage) Reset() {} +func (*textMessage) String() string { return "" } +func (*textMessage) ProtoMessage() {} + +func newTestMessage() *pb.MyMessage { + msg := &pb.MyMessage{ + Count: proto.Int32(42), + Name: proto.String("Dave"), + Quote: proto.String(`"I didn't want to go."`), + Pet: []string{"bunny", "kitty", "horsey"}, + Inner: &pb.InnerMessage{ + Host: proto.String("footrest.syd"), + Port: proto.Int32(7001), + Connected: proto.Bool(true), + }, + Others: []*pb.OtherMessage{ + { + Key: proto.Int64(0xdeadbeef), + Value: []byte{1, 65, 7, 12}, + }, + { + Weight: proto.Float32(6.022), + Inner: &pb.InnerMessage{ + Host: proto.String("lesha.mtv"), + Port: proto.Int32(8002), + }, + }, + }, + Bikeshed: pb.MyMessage_BLUE.Enum(), + Somegroup: &pb.MyMessage_SomeGroup{ + GroupField: proto.Int32(8), + }, + // One normally wouldn't do this. + // This is an undeclared tag 13, as a varint (wire type 0) with value 4. + XXX_unrecognized: []byte{13<<3 | 0, 4}, + } + ext := &pb.Ext{ + Data: proto.String("Big gobs for big rats"), + } + if err := proto.SetExtension(msg, pb.E_Ext_More, ext); err != nil { + panic(err) + } + greetings := []string{"adg", "easy", "cow"} + if err := proto.SetExtension(msg, pb.E_Greeting, greetings); err != nil { + panic(err) + } + + // Add an unknown extension. We marshal a pb.Ext, and fake the ID. + b, err := proto.Marshal(&pb.Ext{Data: proto.String("3G skiing")}) + if err != nil { + panic(err) + } + b = append(proto.EncodeVarint(201<<3|proto.WireBytes), b...) + proto.SetRawExtension(msg, 201, b) + + // Extensions can be plain fields, too, so let's test that. + b = append(proto.EncodeVarint(202<<3|proto.WireVarint), 19) + proto.SetRawExtension(msg, 202, b) + + return msg +} + +const text = `count: 42 +name: "Dave" +quote: "\"I didn't want to go.\"" +pet: "bunny" +pet: "kitty" +pet: "horsey" +inner: < + host: "footrest.syd" + port: 7001 + connected: true +> +others: < + key: 3735928559 + value: "\001A\007\014" +> +others: < + weight: 6.022 + inner: < + host: "lesha.mtv" + port: 8002 + > +> +bikeshed: BLUE +SomeGroup { + group_field: 8 +} +/* 2 unknown bytes */ +13: 4 +[testdata.Ext.more]: < + data: "Big gobs for big rats" +> +[testdata.greeting]: "adg" +[testdata.greeting]: "easy" +[testdata.greeting]: "cow" +/* 13 unknown bytes */ +201: "\t3G skiing" +/* 3 unknown bytes */ +202: 19 +` + +func TestMarshalText(t *testing.T) { + buf := new(bytes.Buffer) + if err := proto.MarshalText(buf, newTestMessage()); err != nil { + t.Fatalf("proto.MarshalText: %v", err) + } + s := buf.String() + if s != text { + t.Errorf("Got:\n===\n%v===\nExpected:\n===\n%v===\n", s, text) + } +} + +func TestMarshalTextCustomMessage(t *testing.T) { + buf := new(bytes.Buffer) + if err := proto.MarshalText(buf, &textMessage{}); err != nil { + t.Fatalf("proto.MarshalText: %v", err) + } + s := buf.String() + if s != "custom" { + t.Errorf("Got %q, expected %q", s, "custom") + } +} +func TestMarshalTextNil(t *testing.T) { + want := "" + tests := []proto.Message{nil, (*pb.MyMessage)(nil)} + for i, test := range tests { + buf := new(bytes.Buffer) + if err := proto.MarshalText(buf, test); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != want { + t.Errorf("%d: got %q want %q", i, got, want) + } + } +} + +func TestMarshalTextUnknownEnum(t *testing.T) { + // The Color enum only specifies values 0-2. + m := &pb.MyMessage{Bikeshed: pb.MyMessage_Color(3).Enum()} + got := m.String() + const want = `bikeshed:3 ` + if got != want { + t.Errorf("\n got %q\nwant %q", got, want) + } +} + +func BenchmarkMarshalTextBuffered(b *testing.B) { + buf := new(bytes.Buffer) + m := newTestMessage() + for i := 0; i < b.N; i++ { + buf.Reset() + proto.MarshalText(buf, m) + } +} + +func BenchmarkMarshalTextUnbuffered(b *testing.B) { + w := ioutil.Discard + m := newTestMessage() + for i := 0; i < b.N; i++ { + proto.MarshalText(w, m) + } +} + +func compact(src string) string { + // s/[ \n]+/ /g; s/ $//; + dst := make([]byte, len(src)) + space, comment := false, false + j := 0 + for i := 0; i < len(src); i++ { + if strings.HasPrefix(src[i:], "/*") { + comment = true + i++ + continue + } + if comment && strings.HasPrefix(src[i:], "*/") { + comment = false + i++ + continue + } + if comment { + continue + } + c := src[i] + if c == ' ' || c == '\n' { + space = true + continue + } + if j > 0 && (dst[j-1] == ':' || dst[j-1] == '<' || dst[j-1] == '{') { + space = false + } + if c == '{' { + space = false + } + if space { + dst[j] = ' ' + j++ + space = false + } + dst[j] = c + j++ + } + if space { + dst[j] = ' ' + j++ + } + return string(dst[0:j]) +} + +var compactText = compact(text) + +func TestCompactText(t *testing.T) { + s := proto.CompactTextString(newTestMessage()) + if s != compactText { + t.Errorf("Got:\n===\n%v===\nExpected:\n===\n%v\n===\n", s, compactText) + } +} + +func TestStringEscaping(t *testing.T) { + testCases := []struct { + in *pb.Strings + out string + }{ + { + // Test data from C++ test (TextFormatTest.StringEscape). + // Single divergence: we don't escape apostrophes. + &pb.Strings{StringField: proto.String("\"A string with ' characters \n and \r newlines and \t tabs and \001 slashes \\ and multiple spaces")}, + "string_field: \"\\\"A string with ' characters \\n and \\r newlines and \\t tabs and \\001 slashes \\\\ and multiple spaces\"\n", + }, + { + // Test data from the same C++ test. + &pb.Strings{StringField: proto.String("\350\260\267\346\255\214")}, + "string_field: \"\\350\\260\\267\\346\\255\\214\"\n", + }, + { + // Some UTF-8. + &pb.Strings{StringField: proto.String("\x00\x01\xff\x81")}, + `string_field: "\000\001\377\201"` + "\n", + }, + } + + for i, tc := range testCases { + var buf bytes.Buffer + if err := proto.MarshalText(&buf, tc.in); err != nil { + t.Errorf("proto.MarsalText: %v", err) + continue + } + s := buf.String() + if s != tc.out { + t.Errorf("#%d: Got:\n%s\nExpected:\n%s\n", i, s, tc.out) + continue + } + + // Check round-trip. + pb := new(pb.Strings) + if err := proto.UnmarshalText(s, pb); err != nil { + t.Errorf("#%d: UnmarshalText: %v", i, err) + continue + } + if !proto.Equal(pb, tc.in) { + t.Errorf("#%d: Round-trip failed:\nstart: %v\n end: %v", i, tc.in, pb) + } + } +} + +// A limitedWriter accepts some output before it fails. +// This is a proxy for something like a nearly-full or imminently-failing disk, +// or a network connection that is about to die. +type limitedWriter struct { + b bytes.Buffer + limit int +} + +var outOfSpace = errors.New("proto: insufficient space") + +func (w *limitedWriter) Write(p []byte) (n int, err error) { + var avail = w.limit - w.b.Len() + if avail <= 0 { + return 0, outOfSpace + } + if len(p) <= avail { + return w.b.Write(p) + } + n, _ = w.b.Write(p[:avail]) + return n, outOfSpace +} + +func TestMarshalTextFailing(t *testing.T) { + // Try lots of different sizes to exercise more error code-paths. + for lim := 0; lim < len(text); lim++ { + buf := new(limitedWriter) + buf.limit = lim + err := proto.MarshalText(buf, newTestMessage()) + // We expect a certain error, but also some partial results in the buffer. + if err != outOfSpace { + t.Errorf("Got:\n===\n%v===\nExpected:\n===\n%v===\n", err, outOfSpace) + } + s := buf.b.String() + x := text[:buf.limit] + if s != x { + t.Errorf("Got:\n===\n%v===\nExpected:\n===\n%v===\n", s, x) + } + } +} + +func TestFloats(t *testing.T) { + tests := []struct { + f float64 + want string + }{ + {0, "0"}, + {4.7, "4.7"}, + {math.Inf(1), "inf"}, + {math.Inf(-1), "-inf"}, + {math.NaN(), "nan"}, + } + for _, test := range tests { + msg := &pb.FloatingPoint{F: &test.f} + got := strings.TrimSpace(msg.String()) + want := `f:` + test.want + if got != want { + t.Errorf("f=%f: got %q, want %q", test.f, got, want) + } + } +} + +func TestRepeatedNilText(t *testing.T) { + m := &pb.MessageList{ + Message: []*pb.MessageList_Message{ + nil, + { + Name: proto.String("Horse"), + }, + nil, + }, + } + want := `Message +Message { + name: "Horse" +} +Message +` + if s := proto.MarshalTextString(m); s != want { + t.Errorf(" got: %s\nwant: %s", s, want) + } +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.gitignore b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.gitignore new file mode 100644 index 00000000000..782377890fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.gitignore @@ -0,0 +1 @@ +*.coverprofile diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.travis.yml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.travis.yml new file mode 100644 index 00000000000..a589b87e58a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.3 + +install: + - go get -t -v ./... + - go install github.com/onsi/ginkgo/ginkgo + +script: + - export PATH=$HOME/gopath/bin:$PATH + - ginkgo -r -failOnPending -randomizeAllSpecs -race diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/LICENSE b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/LICENSE new file mode 100644 index 00000000000..f4f87bd4ed6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/README.md b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/README.md new file mode 100644 index 00000000000..4ef0e2e113e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/README.md @@ -0,0 +1,53 @@ +[![Build Status](https://travis-ci.org/cloudfoundry-incubator/candiedyaml.svg)](https://travis-ci.org/cloudfoundry-incubator/candiedyaml) + +candiedyaml +=========== + +YAML for Go + +Usage +----- + +```go +package myApp + +import ( + "github.com/cloudfoundry-incubator/candiedyaml" + "fmt" + "os" +) + +func main() { + file, err := os.Open("path/to/some/file.yml") + if err != nil { + println("File does not exist:", err.Error()) + os.Exit(1) + } + + document := new(interface{}) + decoder := candiedyaml.NewDecoder(file) + err = decoder.Decode(document) + + if err != nil { + println("Failed to decode document:", err.Error()) + } + + println("parsed yml into interface:", fmt.Sprintf("%#v", document)) + + fileToWrite, err := os.Create("path/to/some/new/file.yml") + if err != nil { + println("Failed to open file for writing:", err.Error()) + os.Exit(1) + } + + encoder := candiedyaml.NewEncoder(fileToWrite) + err = encoder.Encode(document) + + if err != nil { + println("Failed to encode document:", err.Error()) + os.Exit(1) + } + + return +} +``` diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/api.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/api.go new file mode 100644 index 00000000000..87c1043ea88 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/api.go @@ -0,0 +1,834 @@ +/* +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 candiedyaml + +import ( + "io" +) + +/* + * Create a new parser object. + */ + +func yaml_parser_initialize(parser *yaml_parser_t) bool { + *parser = yaml_parser_t{ + raw_buffer: make([]byte, 0, INPUT_RAW_BUFFER_SIZE), + buffer: make([]byte, 0, INPUT_BUFFER_SIZE), + } + + return true +} + +/* + * Destroy a parser object. + */ +func yaml_parser_delete(parser *yaml_parser_t) { + *parser = yaml_parser_t{} +} + +/* + * String read handler. + */ + +func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (int, error) { + if parser.input_pos == len(parser.input) { + return 0, io.EOF + } + + n := copy(buffer, parser.input[parser.input_pos:]) + parser.input_pos += n + return n, nil +} + +/* + * File read handler. + */ + +func yaml_file_read_handler(parser *yaml_parser_t, buffer []byte) (int, error) { + return parser.input_reader.Read(buffer) +} + +/* + * Set a string input. + */ + +func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) { + if parser.read_handler != nil { + panic("input already set") + } + + parser.read_handler = yaml_string_read_handler + + parser.input = input + parser.input_pos = 0 +} + +/* + * Set a reader input + */ +func yaml_parser_set_input_reader(parser *yaml_parser_t, reader io.Reader) { + if parser.read_handler != nil { + panic("input already set") + } + + parser.read_handler = yaml_file_read_handler + parser.input_reader = reader +} + +/* + * Set a generic input. + */ + +func yaml_parser_set_input(parser *yaml_parser_t, handler yaml_read_handler_t) { + if parser.read_handler != nil { + panic("input already set") + } + + parser.read_handler = handler +} + +/* + * Set the source encoding. + */ + +func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) { + if parser.encoding != yaml_ANY_ENCODING { + panic("encoding already set") + } + + parser.encoding = encoding +} + +/* + * Create a new emitter object. + */ + +func yaml_emitter_initialize(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{ + buffer: make([]byte, OUTPUT_BUFFER_SIZE), + raw_buffer: make([]byte, 0, OUTPUT_RAW_BUFFER_SIZE), + states: make([]yaml_emitter_state_t, 0, INITIAL_STACK_SIZE), + events: make([]yaml_event_t, 0, INITIAL_QUEUE_SIZE), + } +} + +func yaml_emitter_delete(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{} +} + +/* + * String write handler. + */ + +func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + *emitter.output_buffer = append(*emitter.output_buffer, buffer...) + return nil +} + +/* + * File write handler. + */ + +func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + _, err := emitter.output_writer.Write(buffer) + return err +} + +/* + * Set a string output. + */ + +func yaml_emitter_set_output_string(emitter *yaml_emitter_t, buffer *[]byte) { + if emitter.write_handler != nil { + panic("output already set") + } + + emitter.write_handler = yaml_string_write_handler + emitter.output_buffer = buffer +} + +/* + * Set a file output. + */ + +func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) { + if emitter.write_handler != nil { + panic("output already set") + } + + emitter.write_handler = yaml_writer_write_handler + emitter.output_writer = w +} + +/* + * Set a generic output handler. + */ + +func yaml_emitter_set_output(emitter *yaml_emitter_t, handler yaml_write_handler_t) { + if emitter.write_handler != nil { + panic("output already set") + } + + emitter.write_handler = handler +} + +/* + * Set the output encoding. + */ + +func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) { + if emitter.encoding != yaml_ANY_ENCODING { + panic("encoding already set") + } + + emitter.encoding = encoding +} + +/* + * Set the canonical output style. + */ + +func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) { + emitter.canonical = canonical +} + +/* + * Set the indentation increment. + */ + +func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) { + if indent < 2 || indent > 9 { + indent = 2 + } + emitter.best_indent = indent +} + +/* + * Set the preferred line width. + */ + +func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) { + if width < 0 { + width = -1 + } + emitter.best_width = width +} + +/* + * Set if unescaped non-ASCII characters are allowed. + */ + +func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) { + emitter.unicode = unicode +} + +/* + * Set the preferred line break character. + */ + +func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) { + emitter.line_break = line_break +} + +/* + * Destroy a token object. + */ + +// yaml_DECLARE(void) +// yaml_token_delete(yaml_token_t *token) +// { +// assert(token); /* Non-NULL token object expected. */ +// +// switch (token.type) +// { +// case yaml_TAG_DIRECTIVE_TOKEN: +// yaml_free(token.data.tag_directive.handle); +// yaml_free(token.data.tag_directive.prefix); +// break; +// +// case yaml_ALIAS_TOKEN: +// yaml_free(token.data.alias.value); +// break; +// +// case yaml_ANCHOR_TOKEN: +// yaml_free(token.data.anchor.value); +// break; +// +// case yaml_TAG_TOKEN: +// yaml_free(token.data.tag.handle); +// yaml_free(token.data.tag.suffix); +// break; +// +// case yaml_SCALAR_TOKEN: +// yaml_free(token.data.scalar.value); +// break; +// +// default: +// break; +// } +// +// memset(token, 0, sizeof(yaml_token_t)); +// } + +/* + * Check if a string is a valid UTF-8 sequence. + * + * Check 'reader.c' for more details on UTF-8 encoding. + */ + +// static int +// yaml_check_utf8(yaml_char_t *start, size_t length) +// { +// yaml_char_t *end = start+length; +// yaml_char_t *pointer = start; +// +// while (pointer < end) { +// unsigned char octet; +// unsigned int width; +// unsigned int value; +// size_t k; +// +// octet = pointer[0]; +// width = (octet & 0x80) == 0x00 ? 1 : +// (octet & 0xE0) == 0xC0 ? 2 : +// (octet & 0xF0) == 0xE0 ? 3 : +// (octet & 0xF8) == 0xF0 ? 4 : 0; +// value = (octet & 0x80) == 0x00 ? octet & 0x7F : +// (octet & 0xE0) == 0xC0 ? octet & 0x1F : +// (octet & 0xF0) == 0xE0 ? octet & 0x0F : +// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0; +// if (!width) return 0; +// if (pointer+width > end) return 0; +// for (k = 1; k < width; k ++) { +// octet = pointer[k]; +// if ((octet & 0xC0) != 0x80) return 0; +// value = (value << 6) + (octet & 0x3F); +// } +// if (!((width == 1) || +// (width == 2 && value >= 0x80) || +// (width == 3 && value >= 0x800) || +// (width == 4 && value >= 0x10000))) return 0; +// +// pointer += width; +// } +// +// return 1; +// } + +/* + * Create STREAM-START. + */ + +func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) { + *event = yaml_event_t{ + event_type: yaml_STREAM_START_EVENT, + encoding: encoding, + } +} + +/* + * Create STREAM-END. + */ + +func yaml_stream_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + event_type: yaml_STREAM_END_EVENT, + } +} + +/* + * Create DOCUMENT-START. + */ + +func yaml_document_start_event_initialize(event *yaml_event_t, + version_directive *yaml_version_directive_t, + tag_directives []yaml_tag_directive_t, + implicit bool) { + *event = yaml_event_t{ + event_type: yaml_DOCUMENT_START_EVENT, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: implicit, + } +} + +/* + * Create DOCUMENT-END. + */ + +func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) { + *event = yaml_event_t{ + event_type: yaml_DOCUMENT_END_EVENT, + implicit: implicit, + } +} + +/* + * Create ALIAS. + */ + +func yaml_alias_event_initialize(event *yaml_event_t, anchor []byte) { + *event = yaml_event_t{ + event_type: yaml_ALIAS_EVENT, + anchor: anchor, + } +} + +/* + * Create SCALAR. + */ + +func yaml_scalar_event_initialize(event *yaml_event_t, + anchor []byte, tag []byte, + value []byte, + plain_implicit bool, quoted_implicit bool, + style yaml_scalar_style_t) { + + *event = yaml_event_t{ + event_type: yaml_SCALAR_EVENT, + anchor: anchor, + tag: tag, + value: value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(style), + } +} + +/* + * Create SEQUENCE-START. + */ + +func yaml_sequence_start_event_initialize(event *yaml_event_t, + anchor []byte, tag []byte, implicit bool, style yaml_sequence_style_t) { + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } +} + +/* + * Create SEQUENCE-END. + */ + +func yaml_sequence_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_END_EVENT, + } +} + +/* + * Create MAPPING-START. + */ + +func yaml_mapping_start_event_initialize(event *yaml_event_t, + anchor []byte, tag []byte, implicit bool, style yaml_mapping_style_t) { + *event = yaml_event_t{ + event_type: yaml_MAPPING_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } +} + +/* + * Create MAPPING-END. + */ + +func yaml_mapping_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + event_type: yaml_MAPPING_END_EVENT, + } +} + +/* + * Destroy an event object. + */ + +func yaml_event_delete(event *yaml_event_t) { + *event = yaml_event_t{} +} + +// /* +// * Create a document object. +// */ +// +// func yaml_document_initialize(document *yaml_document_t, +// version_directive *yaml_version_directive_t, +// tag_directives []yaml_tag_directive_t, +// start_implicit, end_implicit bool) bool { +// +// +// { +// struct { +// YAML_error_type_t error; +// } context; +// struct { +// yaml_node_t *start; +// yaml_node_t *end; +// yaml_node_t *top; +// } nodes = { NULL, NULL, NULL }; +// yaml_version_directive_t *version_directive_copy = NULL; +// struct { +// yaml_tag_directive_t *start; +// yaml_tag_directive_t *end; +// yaml_tag_directive_t *top; +// } tag_directives_copy = { NULL, NULL, NULL }; +// yaml_tag_directive_t value = { NULL, NULL }; +// YAML_mark_t mark = { 0, 0, 0 }; +// +// assert(document); /* Non-NULL document object is expected. */ +// assert((tag_directives_start && tag_directives_end) || +// (tag_directives_start == tag_directives_end)); +// /* Valid tag directives are expected. */ +// +// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error; +// +// if (version_directive) { +// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t)); +// if (!version_directive_copy) goto error; +// version_directive_copy.major = version_directive.major; +// version_directive_copy.minor = version_directive.minor; +// } +// +// if (tag_directives_start != tag_directives_end) { +// yaml_tag_directive_t *tag_directive; +// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE)) +// goto error; +// for (tag_directive = tag_directives_start; +// tag_directive != tag_directives_end; tag_directive ++) { +// assert(tag_directive.handle); +// assert(tag_directive.prefix); +// if (!yaml_check_utf8(tag_directive.handle, +// strlen((char *)tag_directive.handle))) +// goto error; +// if (!yaml_check_utf8(tag_directive.prefix, +// strlen((char *)tag_directive.prefix))) +// goto error; +// value.handle = yaml_strdup(tag_directive.handle); +// value.prefix = yaml_strdup(tag_directive.prefix); +// if (!value.handle || !value.prefix) goto error; +// if (!PUSH(&context, tag_directives_copy, value)) +// goto error; +// value.handle = NULL; +// value.prefix = NULL; +// } +// } +// +// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy, +// tag_directives_copy.start, tag_directives_copy.top, +// start_implicit, end_implicit, mark, mark); +// +// return 1; +// +// error: +// STACK_DEL(&context, nodes); +// yaml_free(version_directive_copy); +// while (!STACK_EMPTY(&context, tag_directives_copy)) { +// yaml_tag_directive_t value = POP(&context, tag_directives_copy); +// yaml_free(value.handle); +// yaml_free(value.prefix); +// } +// STACK_DEL(&context, tag_directives_copy); +// yaml_free(value.handle); +// yaml_free(value.prefix); +// +// return 0; +// } +// +// /* +// * Destroy a document object. +// */ +// +// yaml_DECLARE(void) +// yaml_document_delete(document *yaml_document_t) +// { +// struct { +// YAML_error_type_t error; +// } context; +// yaml_tag_directive_t *tag_directive; +// +// context.error = yaml_NO_ERROR; /* Eliminate a compliler warning. */ +// +// assert(document); /* Non-NULL document object is expected. */ +// +// while (!STACK_EMPTY(&context, document.nodes)) { +// yaml_node_t node = POP(&context, document.nodes); +// yaml_free(node.tag); +// switch (node.type) { +// case yaml_SCALAR_NODE: +// yaml_free(node.data.scalar.value); +// break; +// case yaml_SEQUENCE_NODE: +// STACK_DEL(&context, node.data.sequence.items); +// break; +// case yaml_MAPPING_NODE: +// STACK_DEL(&context, node.data.mapping.pairs); +// break; +// default: +// assert(0); /* Should not happen. */ +// } +// } +// STACK_DEL(&context, document.nodes); +// +// yaml_free(document.version_directive); +// for (tag_directive = document.tag_directives.start; +// tag_directive != document.tag_directives.end; +// tag_directive++) { +// yaml_free(tag_directive.handle); +// yaml_free(tag_directive.prefix); +// } +// yaml_free(document.tag_directives.start); +// +// memset(document, 0, sizeof(yaml_document_t)); +// } +// +// /** +// * Get a document node. +// */ +// +// yaml_DECLARE(yaml_node_t *) +// yaml_document_get_node(document *yaml_document_t, int index) +// { +// assert(document); /* Non-NULL document object is expected. */ +// +// if (index > 0 && document.nodes.start + index <= document.nodes.top) { +// return document.nodes.start + index - 1; +// } +// return NULL; +// } +// +// /** +// * Get the root object. +// */ +// +// yaml_DECLARE(yaml_node_t *) +// yaml_document_get_root_node(document *yaml_document_t) +// { +// assert(document); /* Non-NULL document object is expected. */ +// +// if (document.nodes.top != document.nodes.start) { +// return document.nodes.start; +// } +// return NULL; +// } +// +// /* +// * Add a scalar node to a document. +// */ +// +// yaml_DECLARE(int) +// yaml_document_add_scalar(document *yaml_document_t, +// yaml_char_t *tag, yaml_char_t *value, int length, +// yaml_scalar_style_t style) +// { +// struct { +// YAML_error_type_t error; +// } context; +// YAML_mark_t mark = { 0, 0, 0 }; +// yaml_char_t *tag_copy = NULL; +// yaml_char_t *value_copy = NULL; +// yaml_node_t node; +// +// assert(document); /* Non-NULL document object is expected. */ +// assert(value); /* Non-NULL value is expected. */ +// +// if (!tag) { +// tag = (yaml_char_t *)yaml_DEFAULT_SCALAR_TAG; +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error; +// tag_copy = yaml_strdup(tag); +// if (!tag_copy) goto error; +// +// if (length < 0) { +// length = strlen((char *)value); +// } +// +// if (!yaml_check_utf8(value, length)) goto error; +// value_copy = yaml_malloc(length+1); +// if (!value_copy) goto error; +// memcpy(value_copy, value, length); +// value_copy[length] = '\0'; +// +// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark); +// if (!PUSH(&context, document.nodes, node)) goto error; +// +// return document.nodes.top - document.nodes.start; +// +// error: +// yaml_free(tag_copy); +// yaml_free(value_copy); +// +// return 0; +// } +// +// /* +// * Add a sequence node to a document. +// */ +// +// yaml_DECLARE(int) +// yaml_document_add_sequence(document *yaml_document_t, +// yaml_char_t *tag, yaml_sequence_style_t style) +// { +// struct { +// YAML_error_type_t error; +// } context; +// YAML_mark_t mark = { 0, 0, 0 }; +// yaml_char_t *tag_copy = NULL; +// struct { +// yaml_node_item_t *start; +// yaml_node_item_t *end; +// yaml_node_item_t *top; +// } items = { NULL, NULL, NULL }; +// yaml_node_t node; +// +// assert(document); /* Non-NULL document object is expected. */ +// +// if (!tag) { +// tag = (yaml_char_t *)yaml_DEFAULT_SEQUENCE_TAG; +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error; +// tag_copy = yaml_strdup(tag); +// if (!tag_copy) goto error; +// +// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error; +// +// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end, +// style, mark, mark); +// if (!PUSH(&context, document.nodes, node)) goto error; +// +// return document.nodes.top - document.nodes.start; +// +// error: +// STACK_DEL(&context, items); +// yaml_free(tag_copy); +// +// return 0; +// } +// +// /* +// * Add a mapping node to a document. +// */ +// +// yaml_DECLARE(int) +// yaml_document_add_mapping(document *yaml_document_t, +// yaml_char_t *tag, yaml_mapping_style_t style) +// { +// struct { +// YAML_error_type_t error; +// } context; +// YAML_mark_t mark = { 0, 0, 0 }; +// yaml_char_t *tag_copy = NULL; +// struct { +// yaml_node_pair_t *start; +// yaml_node_pair_t *end; +// yaml_node_pair_t *top; +// } pairs = { NULL, NULL, NULL }; +// yaml_node_t node; +// +// assert(document); /* Non-NULL document object is expected. */ +// +// if (!tag) { +// tag = (yaml_char_t *)yaml_DEFAULT_MAPPING_TAG; +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error; +// tag_copy = yaml_strdup(tag); +// if (!tag_copy) goto error; +// +// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error; +// +// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end, +// style, mark, mark); +// if (!PUSH(&context, document.nodes, node)) goto error; +// +// return document.nodes.top - document.nodes.start; +// +// error: +// STACK_DEL(&context, pairs); +// yaml_free(tag_copy); +// +// return 0; +// } +// +// /* +// * Append an item to a sequence node. +// */ +// +// yaml_DECLARE(int) +// yaml_document_append_sequence_item(document *yaml_document_t, +// int sequence, int item) +// { +// struct { +// YAML_error_type_t error; +// } context; +// +// assert(document); /* Non-NULL document is required. */ +// assert(sequence > 0 +// && document.nodes.start + sequence <= document.nodes.top); +// /* Valid sequence id is required. */ +// assert(document.nodes.start[sequence-1].type == yaml_SEQUENCE_NODE); +// /* A sequence node is required. */ +// assert(item > 0 && document.nodes.start + item <= document.nodes.top); +// /* Valid item id is required. */ +// +// if (!PUSH(&context, +// document.nodes.start[sequence-1].data.sequence.items, item)) +// return 0; +// +// return 1; +// } +// +// /* +// * Append a pair of a key and a value to a mapping node. +// */ +// +// yaml_DECLARE(int) +// yaml_document_append_mapping_pair(document *yaml_document_t, +// int mapping, int key, int value) +// { +// struct { +// YAML_error_type_t error; +// } context; +// +// yaml_node_pair_t pair; +// +// assert(document); /* Non-NULL document is required. */ +// assert(mapping > 0 +// && document.nodes.start + mapping <= document.nodes.top); +// /* Valid mapping id is required. */ +// assert(document.nodes.start[mapping-1].type == yaml_MAPPING_NODE); +// /* A mapping node is required. */ +// assert(key > 0 && document.nodes.start + key <= document.nodes.top); +// /* Valid key id is required. */ +// assert(value > 0 && document.nodes.start + value <= document.nodes.top); +// /* Valid value id is required. */ +// +// pair.key = key; +// pair.value = value; +// +// if (!PUSH(&context, +// document.nodes.start[mapping-1].data.mapping.pairs, pair)) +// return 0; +// +// return 1; +// } +// diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/candiedyaml_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/candiedyaml_suite_test.go new file mode 100644 index 00000000000..0b97fe94839 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/candiedyaml_suite_test.go @@ -0,0 +1,27 @@ +/* +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 candiedyaml + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCandiedyaml(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Candiedyaml Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode.go new file mode 100644 index 00000000000..94752c5990a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode.go @@ -0,0 +1,564 @@ +/* +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 candiedyaml + +import ( + "bytes" + "errors" + "fmt" + "io" + "reflect" + "runtime" + "runtime/debug" + "strconv" + "strings" +) + +type Unmarshaler interface { + UnmarshalYAML(tag string, value interface{}) error +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +type Decoder struct { + parser yaml_parser_t + event yaml_event_t + useNumber bool + + anchors map[string]interface{} +} + +type ParserError struct { + ErrorType YAML_error_type_t + Context string + ContextMark YAML_mark_t + Problem string + ProblemMark YAML_mark_t +} + +func (e *ParserError) Error() string { + return fmt.Sprintf("yaml: [%s] %s at line %d, column %d", e.Context, e.Problem, e.ProblemMark.line+1, e.ProblemMark.column+1) +} + +type UnexpectedEventError struct { + Value string + EventType yaml_event_type_t + At YAML_mark_t +} + +func (e *UnexpectedEventError) Error() string { + return fmt.Sprintf("yaml: Unexpect event [%d]: '%s' at line %d, column %d", e.EventType, e.Value, e.At.line+1, e.At.column+1) +} + +func recovery(err *error) { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + + var tmpError error + switch r := r.(type) { + case error: + tmpError = r + case string: + tmpError = errors.New(r) + default: + tmpError = errors.New("Unknown panic: " + reflect.TypeOf(r).String()) + } + + stackTrace := debug.Stack() + *err = fmt.Errorf("%s\n%s", tmpError.Error(), string(stackTrace)) + } +} + +func Unmarshal(data []byte, v interface{}) error { + d := NewDecoder(bytes.NewBuffer(data)) + return d.Decode(v) +} + +func NewDecoder(r io.Reader) *Decoder { + d := &Decoder{ + anchors: make(map[string]interface{}), + } + yaml_parser_initialize(&d.parser) + yaml_parser_set_input_reader(&d.parser, r) + return d +} + +func (d *Decoder) Decode(v interface{}) (err error) { + defer recovery(&err) + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + rType := reflect.TypeOf(v) + msg := "nil" + if rType != nil { + msg = rType.String() + } + return errors.New("Invalid type: " + msg) + } + + if d.event.event_type == yaml_NO_EVENT { + d.nextEvent() + + if d.event.event_type != yaml_STREAM_START_EVENT { + return errors.New("Invalid stream") + } + + d.nextEvent() + } + + d.document(rv) + return nil +} + +func (d *Decoder) UseNumber() { d.useNumber = true } + +func (d *Decoder) error(err error) { + panic(err) +} + +func (d *Decoder) nextEvent() { + if d.event.event_type == yaml_STREAM_END_EVENT { + d.error(errors.New("The stream is closed")) + } + + if !yaml_parser_parse(&d.parser, &d.event) { + yaml_event_delete(&d.event) + + d.error(&ParserError{ + ErrorType: d.parser.error, + Context: d.parser.context, + ContextMark: d.parser.context_mark, + Problem: d.parser.problem, + ProblemMark: d.parser.problem_mark, + }) + } +} + +func (d *Decoder) document(rv reflect.Value) { + if d.event.event_type != yaml_DOCUMENT_START_EVENT { + d.error(fmt.Errorf("Expected document start - found %d", d.event.event_type)) + } + + d.nextEvent() + d.parse(rv) + + if d.event.event_type != yaml_DOCUMENT_END_EVENT { + d.error(fmt.Errorf("Expected document end - found %d", d.event.event_type)) + } + + d.nextEvent() +} + +func (d *Decoder) parse(rv reflect.Value) { + if !rv.IsValid() { + // skip ahead since we cannot store + d.valueInterface() + return + } + + anchor := string(d.event.anchor) + switch d.event.event_type { + case yaml_SEQUENCE_START_EVENT: + d.sequence(rv) + d.anchor(anchor, rv) + case yaml_MAPPING_START_EVENT: + d.mapping(rv) + d.anchor(anchor, rv) + case yaml_SCALAR_EVENT: + d.scalar(rv) + d.anchor(anchor, rv) + case yaml_ALIAS_EVENT: + d.alias(rv) + case yaml_DOCUMENT_END_EVENT: + default: + d.error(&UnexpectedEventError{ + Value: string(d.event.value), + EventType: d.event.event_type, + At: d.event.start_mark, + }) + } +} + +func (d *Decoder) anchor(anchor string, rv reflect.Value) { + if anchor != "" { + d.anchors[anchor] = rv.Interface() + } +} + +func (d *Decoder) indirect(v reflect.Value) (Unmarshaler, reflect.Value) { + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() { + v = e + continue + } + } + + if v.Kind() != reflect.Ptr { + break + } + + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + var temp interface{} + return u, reflect.ValueOf(&temp) + } + } + + v = v.Elem() + } + + return nil, v +} + +func (d *Decoder) sequence(v reflect.Value) { + if d.event.event_type != yaml_SEQUENCE_START_EVENT { + d.error(fmt.Errorf("Expected sequence start - found %d", d.event.event_type)) + } + + u, pv := d.indirect(v) + if u != nil { + defer func() { + if err := u.UnmarshalYAML("!!seq", pv.Interface()); err != nil { + d.error(err) + } + }() + _, pv = d.indirect(pv) + } + + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + v.Set(reflect.ValueOf(d.sequenceInterface())) + return + } + // Otherwise it's invalid. + fallthrough + default: + d.error(errors.New("sequence: invalid type: " + v.Type().String())) + case reflect.Array: + case reflect.Slice: + break + } + + d.nextEvent() + + i := 0 + for { + if d.event.event_type == yaml_SEQUENCE_END_EVENT { + break + } + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + d.parse(v.Index(i)) + } else { + // Ran out of fixed array: skip. + d.parse(reflect.Value{}) + } + i++ + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + + d.nextEvent() +} + +func (d *Decoder) mapping(v reflect.Value) { + u, pv := d.indirect(v) + if u != nil { + defer func() { + if err := u.UnmarshalYAML("!!map", pv.Interface()); err != nil { + d.error(err) + } + }() + _, pv = d.indirect(pv) + } + v = pv + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + v.Set(reflect.ValueOf(d.mappingInterface())) + return + } + + // Check type of target: struct or map[X]Y + switch v.Kind() { + case reflect.Struct: + d.mappingStruct(v) + return + case reflect.Map: + default: + d.error(errors.New("mapping: invalid type: " + v.Type().String())) + } + + mapt := v.Type() + if v.IsNil() { + v.Set(reflect.MakeMap(mapt)) + } + + d.nextEvent() + + keyt := mapt.Key() + mapElemt := mapt.Elem() + + var mapElem reflect.Value + for { + if d.event.event_type == yaml_MAPPING_END_EVENT { + break + } + key := reflect.New(keyt) + d.parse(key.Elem()) + + if !mapElem.IsValid() { + mapElem = reflect.New(mapElemt).Elem() + } else { + mapElem.Set(reflect.Zero(mapElemt)) + } + + d.parse(mapElem) + + v.SetMapIndex(key.Elem(), mapElem) + } + + d.nextEvent() +} + +func (d *Decoder) mappingStruct(v reflect.Value) { + + structt := v.Type() + fields := cachedTypeFields(structt) + + d.nextEvent() + + for { + if d.event.event_type == yaml_MAPPING_END_EVENT { + break + } + key := "" + d.parse(reflect.ValueOf(&key)) + + // Figure out field corresponding to key. + var subv reflect.Value + + var f *field + for i := range fields { + ff := &fields[i] + if ff.name == key { + f = ff + break + } + + if f == nil && strings.EqualFold(ff.name, key) { + f = ff + } + } + + if f != nil { + subv = v + for _, i := range f.index { + if subv.Kind() == reflect.Ptr { + if subv.IsNil() { + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + } + d.parse(subv) + } + + d.nextEvent() +} + +func (d *Decoder) scalar(v reflect.Value) { + u, pv := d.indirect(v) + + var tag string + if u != nil { + defer func() { + if err := u.UnmarshalYAML(tag, pv.Interface()); err != nil { + d.error(err) + } + }() + + _, pv = d.indirect(pv) + } + v = pv + + var err error + tag, err = resolve(d.event, v, d.useNumber) + if err != nil { + d.error(err) + } + + d.nextEvent() +} + +func (d *Decoder) alias(rv reflect.Value) { + if val, ok := d.anchors[string(d.event.anchor)]; ok { + rv.Set(reflect.ValueOf(val)) + } + + d.nextEvent() +} + +func (d *Decoder) valueInterface() interface{} { + var v interface{} + + anchor := string(d.event.anchor) + switch d.event.event_type { + case yaml_SEQUENCE_START_EVENT: + v = d.sequenceInterface() + case yaml_MAPPING_START_EVENT: + v = d.mappingInterface() + case yaml_SCALAR_EVENT: + v = d.scalarInterface() + case yaml_ALIAS_EVENT: + return d.aliasInterface() + case yaml_DOCUMENT_END_EVENT: + d.error(&UnexpectedEventError{ + Value: string(d.event.value), + EventType: d.event.event_type, + At: d.event.start_mark, + }) + + } + + d.anchorInterface(anchor, v) + return v +} + +func (d *Decoder) scalarInterface() interface{} { + _, v := resolveInterface(d.event, d.useNumber) + + d.nextEvent() + return v +} + +func (d *Decoder) anchorInterface(anchor string, i interface{}) { + if anchor != "" { + d.anchors[anchor] = i + } +} + +func (d *Decoder) aliasInterface() interface{} { + v := d.anchors[string(d.event.anchor)] + + d.nextEvent() + return v +} + +// arrayInterface is like array but returns []interface{}. +func (d *Decoder) sequenceInterface() []interface{} { + var v = make([]interface{}, 0) + + d.nextEvent() + for { + if d.event.event_type == yaml_SEQUENCE_END_EVENT { + break + } + + v = append(v, d.valueInterface()) + } + + d.nextEvent() + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *Decoder) mappingInterface() map[interface{}]interface{} { + m := make(map[interface{}]interface{}) + + d.nextEvent() + + for { + if d.event.event_type == yaml_MAPPING_END_EVENT { + break + } + + key := d.valueInterface() + + // Read value. + m[key] = d.valueInterface() + } + + d.nextEvent() + return m +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode_test.go new file mode 100644 index 00000000000..a0a92a1d294 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/decode_test.go @@ -0,0 +1,679 @@ +/* +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 candiedyaml + +import ( + "math" + "os" + "strconv" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Decode", func() { + It("Decodes a file", func() { + f, _ := os.Open("fixtures/specification/example2_1.yaml") + d := NewDecoder(f) + var v interface{} + err := d.Decode(&v) + + Ω(err).ShouldNot(HaveOccurred()) + }) + + Context("strings", func() { + It("Decodes an empty string", func() { + d := NewDecoder(strings.NewReader(`"" +`)) + var v string + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal("")) + }) + + It("Decodes an empty string to an interface", func() { + d := NewDecoder(strings.NewReader(`"" +`)) + var v interface{} + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal("")) + }) + + It("Decodes a map containing empty strings to an interface", func() { + d := NewDecoder(strings.NewReader(`"" : "" +`)) + var v interface{} + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[interface{}]interface{}{"": ""})) + }) + }) + + Context("Sequence", func() { + It("Decodes to interface{}s", func() { + f, _ := os.Open("fixtures/specification/example2_1.yaml") + d := NewDecoder(f) + var v interface{} + err := d.Decode(&v) + + Ω(err).ShouldNot(HaveOccurred()) + Ω((v).([]interface{})).To(Equal([]interface{}{"Mark McGwire", "Sammy Sosa", "Ken Griffey"})) + }) + + It("Decodes to []string", func() { + f, _ := os.Open("fixtures/specification/example2_1.yaml") + d := NewDecoder(f) + v := make([]string, 0, 3) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([]string{"Mark McGwire", "Sammy Sosa", "Ken Griffey"})) + }) + + It("Decodes a sequence of maps", func() { + f, _ := os.Open("fixtures/specification/example2_12.yaml") + d := NewDecoder(f) + v := make([]map[string]interface{}, 1) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([]map[string]interface{}{ + {"item": "Super Hoop", "quantity": int64(1)}, + {"item": "Basketball", "quantity": int64(4)}, + {"item": "Big Shoes", "quantity": int64(1)}, + })) + }) + + Describe("As structs", func() { + It("Simple struct", func() { + f, _ := os.Open("fixtures/specification/example2_4.yaml") + d := NewDecoder(f) + + type batter struct { + Name string + HR int64 + AVG float64 + } + v := make([]batter, 0, 1) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([]batter{ + batter{Name: "Mark McGwire", HR: 65, AVG: 0.278}, + batter{Name: "Sammy Sosa", HR: 63, AVG: 0.288}, + })) + }) + + It("Tagged struct", func() { + f, _ := os.Open("fixtures/specification/example2_4.yaml") + d := NewDecoder(f) + + type batter struct { + N string `yaml:"name"` + H int64 `yaml:"hr"` + A float64 `yaml:"avg"` + } + v := make([]batter, 0, 1) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([]batter{ + batter{N: "Mark McGwire", H: 65, A: 0.278}, + batter{N: "Sammy Sosa", H: 63, A: 0.288}, + })) + }) + + It("ignores missing tags", func() { + f, _ := os.Open("fixtures/specification/example2_4.yaml") + d := NewDecoder(f) + + type batter struct { + N string `yaml:"name"` + HR int64 + A float64 + } + v := make([]batter, 0, 1) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([]batter{ + batter{N: "Mark McGwire", HR: 65}, + batter{N: "Sammy Sosa", HR: 63}, + })) + }) + }) + + It("Decodes a sequence of sequences", func() { + f, _ := os.Open("fixtures/specification/example2_5.yaml") + d := NewDecoder(f) + v := make([][]interface{}, 1) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal([][]interface{}{ + {"name", "hr", "avg"}, + {"Mark McGwire", int64(65), float64(0.278)}, + {"Sammy Sosa", int64(63), float64(0.288)}, + })) + }) + }) + + Context("Maps", func() { + It("Decodes to interface{}s", func() { + f, _ := os.Open("fixtures/specification/example2_2.yaml") + d := NewDecoder(f) + var v interface{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω((v).(map[interface{}]interface{})).To(Equal(map[interface{}]interface{}{ + "hr": int64(65), + "avg": float64(0.278), + "rbi": int64(147), + })) + }) + + It("Decodes to a struct", func() { + f, _ := os.Open("fixtures/specification/example2_2.yaml") + d := NewDecoder(f) + + type batter struct { + HR int64 + AVG float64 + RBI int64 + } + v := batter{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal(batter{HR: 65, AVG: 0.278, RBI: 147})) + }) + + It("Decodes to a map of string arrays", func() { + f, _ := os.Open("fixtures/specification/example2_9.yaml") + d := NewDecoder(f) + v := make(map[string][]string) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).To(Equal(map[string][]string{"hr": []string{"Mark McGwire", "Sammy Sosa"}, "rbi": []string{"Sammy Sosa", "Ken Griffey"}})) + }) + }) + + Context("Sequence of Maps", func() { + It("Decodes to interface{}s", func() { + f, _ := os.Open("fixtures/specification/example2_4.yaml") + d := NewDecoder(f) + var v interface{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω((v).([]interface{})).To(Equal([]interface{}{ + map[interface{}]interface{}{"name": "Mark McGwire", "hr": int64(65), "avg": float64(0.278)}, + map[interface{}]interface{}{"name": "Sammy Sosa", "hr": int64(63), "avg": float64(0.288)}, + })) + }) + }) + + It("Decodes ascii art", func() { + f, _ := os.Open("fixtures/specification/example2_13.yaml") + d := NewDecoder(f) + v := "" + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(`\//||\/|| +// || ||__ +`)) + }) + + It("Decodes folded strings", func() { + f, _ := os.Open("fixtures/specification/example2_15.yaml") + d := NewDecoder(f) + v := "" + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal("Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n")) + }) + + It("Decodes literal and folded strings with indents", func() { + f, _ := os.Open("fixtures/specification/example2_16.yaml") + d := NewDecoder(f) + v := make(map[string]string) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]string{ + "name": "Mark McGwire", + "accomplishment": `Mark set a major league home run record in 1998. +`, + "stats": `65 Home Runs +0.278 Batting Average +`, + })) + }) + + It("Decodes single quoted", func() { + f, _ := os.Open("fixtures/specification/example2_17_quoted.yaml") + d := NewDecoder(f) + v := make(map[string]string) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]string{ + "quoted": ` # not a 'comment'.`, + })) + }) + + Context("ints", func() { + It("Decodes into an interface{}", func() { + f, _ := os.Open("fixtures/specification/example2_19.yaml") + d := NewDecoder(f) + v := make(map[string]interface{}) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]interface{}{ + "canonical": int64(12345), + "decimal": int64(12345), + "sexagesimal": int64(12345), + "octal": int64(12), + "hexadecimal": int64(12), + })) + }) + + It("Decodes into int64", func() { + f, _ := os.Open("fixtures/specification/example2_19.yaml") + d := NewDecoder(f) + v := make(map[string]int64) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]int64{ + "canonical": int64(12345), + "decimal": int64(12345), + "sexagesimal": int64(12345), + "octal": int64(12), + "hexadecimal": int64(12), + })) + }) + + Context("boundary values", func() { + intoInt64 := func(val int64) { + It("Decodes into an int64 value", func() { + var v int64 + + d := NewDecoder(strings.NewReader(strconv.FormatInt(val, 10))) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(val)) + + }) + } + + intoInt := func(val int) { + It("Decodes into an int value", func() { + var v int + + d := NewDecoder(strings.NewReader(strconv.FormatInt(int64(val), 10))) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(val)) + + }) + } + + intoInterface := func(val int64) { + It("Decodes into an interface{}", func() { + var v interface{} + + d := NewDecoder(strings.NewReader(strconv.FormatInt(val, 10))) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(val)) + }) + } + + intoInt64(math.MaxInt64) + intoInterface(math.MaxInt64) + + intoInt64(math.MinInt64) + intoInterface(math.MinInt64) + + intoInt(math.MaxInt32) + intoInt(math.MinInt32) + }) + }) + + It("Decodes a variety of floats", func() { + f, _ := os.Open("fixtures/specification/example2_20.yaml") + d := NewDecoder(f) + v := make(map[string]float64) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(math.IsNaN(v["not a number"])).Should(BeTrue()) + delete(v, "not a number") + + Ω(v).Should(Equal(map[string]float64{ + "canonical": float64(1230.15), + "exponential": float64(1230.15), + "sexagesimal": float64(1230.15), + "fixed": float64(1230.15), + "negative infinity": math.Inf(-1), + })) + }) + + It("Decodes booleans, nil and strings", func() { + f, _ := os.Open("fixtures/specification/example2_21.yaml") + d := NewDecoder(f) + v := make(map[string]interface{}) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]interface{}{ + "": interface{}(nil), + "true": true, + "false": false, + "string": "12345", + })) + }) + + It("Decodes dates/time", func() { + f, _ := os.Open("fixtures/specification/example2_22.yaml") + d := NewDecoder(f) + v := make(map[string]time.Time) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]time.Time{ + "canonical": time.Date(2001, time.December, 15, 2, 59, 43, int(1*time.Millisecond), time.UTC), + "iso8601": time.Date(2001, time.December, 14, 21, 59, 43, int(10*time.Millisecond), time.FixedZone("", -5*3600)), + "spaced": time.Date(2001, time.December, 14, 21, 59, 43, int(10*time.Millisecond), time.FixedZone("", -5*3600)), + "date": time.Date(2002, time.December, 14, 0, 0, 0, 0, time.UTC), + })) + }) + + It("Respects tags", func() { + f, _ := os.Open("fixtures/specification/example2_23_non_date.yaml") + d := NewDecoder(f) + v := make(map[string]string) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]string{ + "not-date": "2002-04-28", + })) + }) + + Context("Decodes binary/base64", func() { + It("to []byte", func() { + f, _ := os.Open("fixtures/specification/example2_23_picture.yaml") + d := NewDecoder(f) + v := make(map[string][]byte) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string][]byte{ + "picture": []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x0c, 0x00, + 0x0c, 0x00, 0x84, 0x00, 0x00, 0xff, 0xff, 0xf7, 0xf5, 0xf5, 0xee, + 0xe9, 0xe9, 0xe5, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0xe7, 0xe7, + 0xe7, 0x5e, 0x5e, 0x5e, 0xf3, 0xf3, 0xed, 0x8e, 0x8e, 0x8e, 0xe0, + 0xe0, 0xe0, 0x9f, 0x9f, 0x9f, 0x93, 0x93, 0x93, 0xa7, 0xa7, 0xa7, + 0x9e, 0x9e, 0x9e, 0x69, 0x5e, 0x10, 0x27, 0x20, 0x82, 0x0a, 0x01, + 0x00, 0x3b}, + })) + }) + + It("to string", func() { + d := NewDecoder(strings.NewReader("!binary YWJjZGVmZw==")) + var v string + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal("abcdefg")) + }) + + It("to string via alternate form", func() { + d := NewDecoder(strings.NewReader("!!binary YWJjZGVmZw==")) + var v string + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal("abcdefg")) + }) + + }) + + Context("Aliases", func() { + Context("to known types", func() { + It("aliases scalars", func() { + f, _ := os.Open("fixtures/specification/example2_10.yaml") + d := NewDecoder(f) + v := make(map[string][]string) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string][]string{ + "hr": {"Mark McGwire", "Sammy Sosa"}, + "rbi": {"Sammy Sosa", "Ken Griffey"}, + })) + }) + + It("aliases sequences", func() { + d := NewDecoder(strings.NewReader(` +--- +hr: &ss + - MG + - SS +rbi: *ss +`)) + v := make(map[string][]string) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string][]string{ + "hr": {"MG", "SS"}, + "rbi": {"MG", "SS"}, + })) + }) + + It("aliases maps", func() { + d := NewDecoder(strings.NewReader(` +--- +hr: &ss + MG : SS +rbi: *ss +`)) + v := make(map[string]map[string]string) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]map[string]string{ + "hr": {"MG": "SS"}, + "rbi": {"MG": "SS"}, + })) + }) + }) + + Context("to Interface", func() { + It("aliases scalars", func() { + f, _ := os.Open("fixtures/specification/example2_10.yaml") + d := NewDecoder(f) + v := make(map[string]interface{}) + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]interface{}{ + "hr": []interface{}{"Mark McGwire", "Sammy Sosa"}, + "rbi": []interface{}{"Sammy Sosa", "Ken Griffey"}, + })) + }) + + It("aliases sequences", func() { + d := NewDecoder(strings.NewReader(` +--- +hr: &ss + - MG + - SS +rbi: *ss +`)) + v := make(map[string]interface{}) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]interface{}{ + "hr": []interface{}{"MG", "SS"}, + "rbi": []interface{}{"MG", "SS"}, + })) + }) + + It("aliases maps", func() { + d := NewDecoder(strings.NewReader(` +--- +hr: &ss + MG : SS +rbi: *ss +`)) + v := make(map[string]interface{}) + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal(map[string]interface{}{ + "hr": map[interface{}]interface{}{"MG": "SS"}, + "rbi": map[interface{}]interface{}{"MG": "SS"}, + })) + }) + + It("supports binary", func() { + d := NewDecoder(strings.NewReader("!binary YWJjZGVmZw==")) + var v interface{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(Equal([]byte("abcdefg"))) + }) + }) + }) + + Context("When decoding fails", func() { + It("returns an error", func() { + f, _ := os.Open("fixtures/specification/example_empty.yaml") + d := NewDecoder(f) + var v interface{} + + err := d.Decode(&v) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("Unmarshaler support", func() { + Context("Receiver is a value", func() { + It("the Marshaler interface is not used", func() { + d := NewDecoder(strings.NewReader("abc\n")) + v := hasMarshaler{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.Value).Should(BeNil()) + }) + }) + + Context("Receiver is a pointer", func() { + It("uses the Marshaler interface when a pointer", func() { + d := NewDecoder(strings.NewReader("abc\n")) + v := hasPtrMarshaler{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("marshals a scalar", func() { + d := NewDecoder(strings.NewReader("abc\n")) + v := hasPtrMarshaler{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.Tag).Should(Equal("!!str")) + Ω(v.Value).Should(Equal("abc")) + }) + + It("marshals a sequence", func() { + d := NewDecoder(strings.NewReader("[abc, def]\n")) + v := hasPtrMarshaler{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.Tag).Should(Equal("!!seq")) + Ω(v.Value).Should(Equal([]interface{}{"abc", "def"})) + }) + + It("marshals a map", func() { + d := NewDecoder(strings.NewReader("{ a: bc}\n")) + v := hasPtrMarshaler{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.Tag).Should(Equal("!!map")) + Ω(v.Value).Should(Equal(map[interface{}]interface{}{"a": "bc"})) + }) + }) + }) + + Context("Marshals into a Number", func() { + It("when the number is an int", func() { + d := NewDecoder(strings.NewReader("123\n")) + d.UseNumber() + var v Number + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.String()).Should(Equal("123")) + }) + + It("when the number is an float", func() { + d := NewDecoder(strings.NewReader("1.23\n")) + d.UseNumber() + var v Number + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v.String()).Should(Equal("1.23")) + }) + + It("it fails when its a non-Number", func() { + d := NewDecoder(strings.NewReader("on\n")) + d.UseNumber() + var v Number + + err := d.Decode(&v) + Ω(err).Should(HaveOccurred()) + }) + + It("returns a Number", func() { + d := NewDecoder(strings.NewReader("123\n")) + d.UseNumber() + var v interface{} + + err := d.Decode(&v) + Ω(err).ShouldNot(HaveOccurred()) + Ω(v).Should(BeAssignableToTypeOf(Number(""))) + + n := v.(Number) + Ω(n.String()).Should(Equal("123")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/emitter.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/emitter.go new file mode 100644 index 00000000000..4f07eb64fe9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/emitter.go @@ -0,0 +1,2072 @@ +/* +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 candiedyaml + +import ( + "bytes" +) + +var default_tag_directives = []yaml_tag_directive_t{ + {[]byte("!"), []byte("!")}, + {[]byte("!!"), []byte("tag:yaml.org,2002:")}, +} + +/* + * Flush the buffer if needed. + */ + +func flush(emitter *yaml_emitter_t) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + return yaml_emitter_flush(emitter) + } + return true +} + +/* + * Put a character to the output buffer. + */ +func put(emitter *yaml_emitter_t, value byte) bool { + if !flush(emitter) { + return false + } + + emitter.buffer[emitter.buffer_pos] = value + emitter.buffer_pos++ + emitter.column++ + return true +} + +/* + * Put a line break to the output buffer. + */ + +func put_break(emitter *yaml_emitter_t) bool { + if !flush(emitter) { + return false + } + switch emitter.line_break { + case yaml_CR_BREAK: + emitter.buffer[emitter.buffer_pos] = '\r' + emitter.buffer_pos++ + case yaml_LN_BREAK: + emitter.buffer[emitter.buffer_pos] = '\n' + emitter.buffer_pos++ + case yaml_CRLN_BREAK: + emitter.buffer[emitter.buffer_pos] = '\r' + emitter.buffer[emitter.buffer_pos] = '\n' + emitter.buffer_pos += 2 + default: + return false + } + emitter.column = 0 + emitter.line++ + return true +} + +/* + * Copy a character from a string into buffer. + */ +func write(emitter *yaml_emitter_t, src []byte, src_pos *int) bool { + if !flush(emitter) { + return false + } + copy_bytes(emitter.buffer, &emitter.buffer_pos, src, src_pos) + emitter.column++ + return true +} + +/* + * Copy a line break character from a string into buffer. + */ + +func write_break(emitter *yaml_emitter_t, src []byte, src_pos *int) bool { + if src[*src_pos] == '\n' { + if !put_break(emitter) { + return false + } + *src_pos++ + } else { + if !write(emitter, src, src_pos) { + return false + } + emitter.column = 0 + emitter.line++ + } + + return true +} + +/* + * Set an emitter error and return 0. + */ + +func yaml_emitter_set_emitter_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_EMITTER_ERROR + emitter.problem = problem + return false +} + +/* + * Emit an event. + */ + +func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.events = append(emitter.events, *event) + for !yaml_emitter_need_more_events(emitter) { + event := &emitter.events[emitter.events_head] + if !yaml_emitter_analyze_event(emitter, event) { + return false + } + if !yaml_emitter_state_machine(emitter, event) { + return false + } + yaml_event_delete(event) + emitter.events_head++ + } + return true +} + +/* + * Check if we need to accumulate more events before emitting. + * + * We accumulate extra + * - 1 event for DOCUMENT-START + * - 2 events for SEQUENCE-START + * - 3 events for MAPPING-START + */ + +func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool { + if emitter.events_head == len(emitter.events) { + return true + } + + accumulate := 0 + switch emitter.events[emitter.events_head].event_type { + case yaml_DOCUMENT_START_EVENT: + accumulate = 1 + case yaml_SEQUENCE_START_EVENT: + accumulate = 2 + case yaml_MAPPING_START_EVENT: + accumulate = 3 + default: + return false + } + + if len(emitter.events)-emitter.events_head > accumulate { + return false + } + + level := 0 + for i := emitter.events_head; i < len(emitter.events); i++ { + switch emitter.events[i].event_type { + case yaml_STREAM_START_EVENT, yaml_DOCUMENT_START_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT: + level++ + case yaml_STREAM_END_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_END_EVENT, yaml_MAPPING_END_EVENT: + level-- + } + + if level == 0 { + return false + } + } + return true +} + +/* + * Append a directive to the directives stack. + */ + +func yaml_emitter_append_tag_directive(emitter *yaml_emitter_t, + value *yaml_tag_directive_t, allow_duplicates bool) bool { + + for i := range emitter.tag_directives { + + if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { + if allow_duplicates { + return true + } + return yaml_emitter_set_emitter_error(emitter, "duplicat %TAG directive") + } + } + + tag_copy := yaml_tag_directive_t{ + handle: value.handle, + prefix: value.prefix, + } + + emitter.tag_directives = append(emitter.tag_directives, tag_copy) + + return true +} + +/* + * Increase the indentation level. + */ + +func yaml_emitter_increase_indent(emitter *yaml_emitter_t, flow bool, indentless bool) bool { + + emitter.indents = append(emitter.indents, emitter.indent) + + if emitter.indent < 0 { + if flow { + emitter.indent = emitter.best_indent + } else { + emitter.indent = 0 + } + } else if !indentless { + emitter.indent += emitter.best_indent + } + + return true +} + +/* + * State dispatcher. + */ + +func yaml_emitter_state_machine(emitter *yaml_emitter_t, event *yaml_event_t) bool { + switch emitter.state { + case yaml_EMIT_STREAM_START_STATE: + return yaml_emitter_emit_stream_start(emitter, event) + + case yaml_EMIT_FIRST_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, true) + + case yaml_EMIT_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, false) + + case yaml_EMIT_DOCUMENT_CONTENT_STATE: + return yaml_emitter_emit_document_content(emitter, event) + + case yaml_EMIT_DOCUMENT_END_STATE: + return yaml_emitter_emit_document_end(emitter, event) + + case yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, true) + + case yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, false) + + case yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, true) + + case yaml_EMIT_FLOW_MAPPING_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, false) + + case yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, true) + + case yaml_EMIT_FLOW_MAPPING_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, false) + + case yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, true) + + case yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, false) + + case yaml_EMIT_END_STATE: + return yaml_emitter_set_emitter_error(emitter, + "expected nothing after STREAM-END") + + } + + panic("invalid state") +} + +/* + * Expect STREAM-START. + */ + +func yaml_emitter_emit_stream_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + if event.event_type != yaml_STREAM_START_EVENT { + return yaml_emitter_set_emitter_error(emitter, + "expected STREAM-START") + } + + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = event.encoding + + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = yaml_UTF8_ENCODING + } + } + + if emitter.best_indent < 2 || emitter.best_indent > 9 { + emitter.best_indent = 2 + } + + if emitter.best_width >= 0 && emitter.best_width <= emitter.best_indent*2 { + emitter.best_width = 80 + } + + if emitter.best_width < 0 { + emitter.best_width = 1<<31 - 1 + } + + if emitter.line_break == yaml_ANY_BREAK { + emitter.line_break = yaml_LN_BREAK + } + + emitter.indent = -1 + + emitter.line = 0 + emitter.column = 0 + emitter.whitespace = true + emitter.indention = true + + if emitter.encoding != yaml_UTF8_ENCODING { + if !yaml_emitter_write_bom(emitter) { + return false + } + } + + emitter.state = yaml_EMIT_FIRST_DOCUMENT_START_STATE + + return true +} + +/* + * Expect DOCUMENT-START or STREAM-END. + */ + +func yaml_emitter_emit_document_start(emitter *yaml_emitter_t, + event *yaml_event_t, first bool) bool { + + if event.event_type == yaml_DOCUMENT_START_EVENT { + if event.version_directive != nil { + if !yaml_emitter_analyze_version_directive(emitter, + *event.version_directive) { + return false + } + } + + for i := range event.tag_directives { + tag_directive := &event.tag_directives[i] + + if !yaml_emitter_analyze_tag_directive(emitter, tag_directive) { + return false + } + if !yaml_emitter_append_tag_directive(emitter, tag_directive, false) { + return false + } + } + + for i := range default_tag_directives { + if !yaml_emitter_append_tag_directive(emitter, &default_tag_directives[i], true) { + return false + } + } + + implicit := event.implicit + if !first || emitter.canonical { + implicit = false + } + + if (event.version_directive != nil || len(event.tag_directives) > 0) && + emitter.open_ended { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if event.version_directive != nil { + implicit = false + if !yaml_emitter_write_indicator(emitter, []byte("%YAML"), true, false, false) { + return false + } + + if !yaml_emitter_write_indicator(emitter, []byte("1.1"), true, false, false) { + return false + } + + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if len(event.tag_directives) > 0 { + implicit = false + for i := range event.tag_directives { + tag_directive := &event.tag_directives[i] + + if !yaml_emitter_write_indicator(emitter, []byte("%TAG"), true, false, false) { + return false + } + if !yaml_emitter_write_tag_handle(emitter, tag_directive.handle) { + return false + } + if !yaml_emitter_write_tag_content(emitter, tag_directive.prefix, true) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + if yaml_emitter_check_empty_document(emitter) { + implicit = false + } + + if !implicit { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("---"), true, false, false) { + return false + } + + if emitter.canonical { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + emitter.state = yaml_EMIT_DOCUMENT_CONTENT_STATE + + return true + } else if event.event_type == yaml_STREAM_END_EVENT { + if emitter.open_ended { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if !yaml_emitter_flush(emitter) { + return false + } + + emitter.state = yaml_EMIT_END_STATE + + return true + } + + return yaml_emitter_set_emitter_error(emitter, + "expected DOCUMENT-START or STREAM-END") +} + +/* + * Expect the root node. + */ + +func yaml_emitter_emit_document_content(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.states = append(emitter.states, yaml_EMIT_DOCUMENT_END_STATE) + + return yaml_emitter_emit_node(emitter, event, true, false, false, false) +} + +/* + * Expect DOCUMENT-END. + */ + +func yaml_emitter_emit_document_end(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + if event.event_type != yaml_DOCUMENT_END_EVENT { + return yaml_emitter_set_emitter_error(emitter, + "expected DOCUMENT-END") + } + + if !yaml_emitter_write_indent(emitter) { + return false + } + if !event.implicit { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_flush(emitter) { + return false + } + + emitter.state = yaml_EMIT_DOCUMENT_START_STATE + emitter.tag_directives = emitter.tag_directives[:0] + return true +} + +/* + * + * Expect a flow item node. + */ + +func yaml_emitter_emit_flow_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + if first { + if !yaml_emitter_write_indicator(emitter, []byte("["), true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.event_type == yaml_SEQUENCE_END_EVENT { + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + if emitter.canonical && !first { + if !yaml_emitter_write_indicator(emitter, []byte(","), false, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte("]"), false, false, false) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !first { + if !yaml_emitter_write_indicator(emitter, []byte(","), false, false, false) { + return false + } + } + + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE) + return yaml_emitter_emit_node(emitter, event, false, true, false, false) +} + +/* + * Expect a flow key node. + */ + +func yaml_emitter_emit_flow_mapping_key(emitter *yaml_emitter_t, + event *yaml_event_t, first bool) bool { + + if first { + + if !yaml_emitter_write_indicator(emitter, []byte("{"), true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.event_type == yaml_MAPPING_END_EVENT { + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + + if emitter.canonical && !first { + if !yaml_emitter_write_indicator(emitter, []byte(","), false, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte("}"), false, false, false) { + return false + } + + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !first { + if !yaml_emitter_write_indicator(emitter, []byte(","), false, false, false) { + return false + } + } + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if !emitter.canonical && yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } else { + if !yaml_emitter_write_indicator(emitter, []byte("?"), true, false, false) { + return false + } + + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) + } +} + +/* + * Expect a flow value node. + */ + +func yaml_emitter_emit_flow_mapping_value(emitter *yaml_emitter_t, + event *yaml_event_t, simple bool) bool { + + if simple { + if !yaml_emitter_write_indicator(emitter, []byte(":"), false, false, false) { + return false + } + } else { + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte(":"), true, false, false) { + return false + } + } + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_KEY_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +/* + * Expect a block item node. + */ + +func yaml_emitter_emit_block_sequence_item(emitter *yaml_emitter_t, + event *yaml_event_t, first bool) bool { + + if first { + if !yaml_emitter_increase_indent(emitter, false, + (emitter.mapping_context && !emitter.indention)) { + return false + } + } + + if event.event_type == yaml_SEQUENCE_END_EVENT { + + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("-"), true, false, true) { + return false + } + + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE) + return yaml_emitter_emit_node(emitter, event, false, true, false, false) +} + +/* + * Expect a block key node. + */ + +func yaml_emitter_emit_block_mapping_key(emitter *yaml_emitter_t, + event *yaml_event_t, first bool) bool { + + if first { + if !yaml_emitter_increase_indent(emitter, false, false) { + return false + } + } + + if event.event_type == yaml_MAPPING_END_EVENT { + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !yaml_emitter_write_indent(emitter) { + return false + } + + if yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE) + + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } else { + if !yaml_emitter_write_indicator(emitter, []byte("?"), true, false, true) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_VALUE_STATE) + + return yaml_emitter_emit_node(emitter, event, false, false, true, false) + } +} + +/* + * Expect a block value node. + */ + +func yaml_emitter_emit_block_mapping_value(emitter *yaml_emitter_t, + event *yaml_event_t, simple bool) bool { + + if simple { + if !yaml_emitter_write_indicator(emitter, []byte(":"), false, false, false) { + return false + } + } else { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte(":"), true, false, true) { + return false + } + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_KEY_STATE) + + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +/* + * Expect a node. + */ + +func yaml_emitter_emit_node(emitter *yaml_emitter_t, event *yaml_event_t, + root bool, sequence bool, mapping bool, simple_key bool) bool { + emitter.root_context = root + emitter.sequence_context = sequence + emitter.mapping_context = mapping + emitter.simple_key_context = simple_key + + switch event.event_type { + case yaml_ALIAS_EVENT: + return yaml_emitter_emit_alias(emitter, event) + + case yaml_SCALAR_EVENT: + return yaml_emitter_emit_scalar(emitter, event) + + case yaml_SEQUENCE_START_EVENT: + return yaml_emitter_emit_sequence_start(emitter, event) + + case yaml_MAPPING_START_EVENT: + return yaml_emitter_emit_mapping_start(emitter, event) + + default: + return yaml_emitter_set_emitter_error(emitter, + "expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS") + } + + return false +} + +/* + * Expect ALIAS. + */ + +func yaml_emitter_emit_alias(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true +} + +/* + * Expect SCALAR. + */ + +func yaml_emitter_emit_scalar(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_select_scalar_style(emitter, event) { + return false + } + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + if !yaml_emitter_process_scalar(emitter) { + return false + } + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true +} + +/* + * Expect SEQUENCE-START. + */ + +func yaml_emitter_emit_sequence_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + + if emitter.flow_level > 0 || emitter.canonical || + event.style == yaml_style_t(yaml_FLOW_SEQUENCE_STYLE) || + yaml_emitter_check_empty_sequence(emitter) { + emitter.state = yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE + } + + return true +} + +/* + * Expect MAPPING-START. + */ + +func yaml_emitter_emit_mapping_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + + if emitter.flow_level > 0 || emitter.canonical || + event.style == yaml_style_t(yaml_FLOW_MAPPING_STYLE) || + yaml_emitter_check_empty_mapping(emitter) { + emitter.state = yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE + } + + return true +} + +/* + * Check if the document content is an empty scalar. + */ + +func yaml_emitter_check_empty_document(emitter *yaml_emitter_t) bool { + return false +} + +/* + * Check if the next events represent an empty sequence. + */ + +func yaml_emitter_check_empty_sequence(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + + return (emitter.events[emitter.events_head].event_type == yaml_SEQUENCE_START_EVENT && + emitter.events[emitter.events_head+1].event_type == yaml_SEQUENCE_END_EVENT) +} + +/* + * Check if the next events represent an empty mapping. + */ + +func yaml_emitter_check_empty_mapping(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + + return (emitter.events[emitter.events_head].event_type == yaml_MAPPING_START_EVENT && + emitter.events[emitter.events_head+1].event_type == yaml_MAPPING_END_EVENT) +} + +/* + * Check if the next node can be expressed as a simple key. + */ + +func yaml_emitter_check_simple_key(emitter *yaml_emitter_t) bool { + length := 0 + + switch emitter.events[emitter.events_head].event_type { + case yaml_ALIAS_EVENT: + length += len(emitter.anchor_data.anchor) + + case yaml_SCALAR_EVENT: + if emitter.scalar_data.multiline { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + + len(emitter.scalar_data.value) + + case yaml_SEQUENCE_START_EVENT: + if !yaml_emitter_check_empty_sequence(emitter) { + return false + } + + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + + case yaml_MAPPING_START_EVENT: + if !yaml_emitter_check_empty_mapping(emitter) { + return false + } + + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + + default: + return false + } + + if length > 128 { + return false + } + + return true +} + +/* + * Determine an acceptable scalar style. + */ + +func yaml_emitter_select_scalar_style(emitter *yaml_emitter_t, event *yaml_event_t) bool { + no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 + + if no_tag && !event.implicit && !event.quoted_implicit { + return yaml_emitter_set_emitter_error(emitter, + "neither tag nor implicit flags are specified") + } + + style := yaml_scalar_style_t(event.style) + + if style == yaml_ANY_SCALAR_STYLE { + style = yaml_PLAIN_SCALAR_STYLE + } + + if emitter.canonical { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + if emitter.simple_key_context && emitter.scalar_data.multiline { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + if style == yaml_PLAIN_SCALAR_STYLE { + if (emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed) || + (emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed) { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if len(emitter.scalar_data.value) == 0 && + (emitter.flow_level > 0 || emitter.simple_key_context) { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if no_tag && !event.implicit { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + } + + if style == yaml_SINGLE_QUOTED_SCALAR_STYLE { + if !emitter.scalar_data.single_quoted_allowed { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + + if style == yaml_LITERAL_SCALAR_STYLE || style == yaml_FOLDED_SCALAR_STYLE { + if !emitter.scalar_data.block_allowed || + emitter.flow_level > 0 || emitter.simple_key_context { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + + if no_tag && !event.quoted_implicit && + style != yaml_PLAIN_SCALAR_STYLE { + emitter.tag_data.handle = []byte("!") + } + + emitter.scalar_data.style = style + + return true +} + +/* + * Write an achor. + */ + +func yaml_emitter_process_anchor(emitter *yaml_emitter_t) bool { + if emitter.anchor_data.anchor == nil { + return true + } + + indicator := "*" + if !emitter.anchor_data.alias { + indicator = "&" + } + if !yaml_emitter_write_indicator(emitter, []byte(indicator), true, false, false) { + return false + } + + return yaml_emitter_write_anchor(emitter, emitter.anchor_data.anchor) +} + +/* + * Write a tag. + */ + +func yaml_emitter_process_tag(emitter *yaml_emitter_t) bool { + if len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 { + return true + } + + if len(emitter.tag_data.handle) > 0 { + if !yaml_emitter_write_tag_handle(emitter, emitter.tag_data.handle) { + return false + } + + if len(emitter.tag_data.suffix) > 0 { + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + + } + } else { + if !yaml_emitter_write_indicator(emitter, []byte("!<"), true, false, false) { + return false + } + + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + + if !yaml_emitter_write_indicator(emitter, []byte(">"), false, false, false) { + return false + } + + } + + return true +} + +/* + * Write a scalar. + */ + +func yaml_emitter_process_scalar(emitter *yaml_emitter_t) bool { + switch emitter.scalar_data.style { + case yaml_PLAIN_SCALAR_STYLE: + return yaml_emitter_write_plain_scalar(emitter, + emitter.scalar_data.value, + !emitter.simple_key_context) + + case yaml_SINGLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_single_quoted_scalar(emitter, + emitter.scalar_data.value, + !emitter.simple_key_context) + + case yaml_DOUBLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_double_quoted_scalar(emitter, + emitter.scalar_data.value, + !emitter.simple_key_context) + + case yaml_LITERAL_SCALAR_STYLE: + return yaml_emitter_write_literal_scalar(emitter, + emitter.scalar_data.value) + + case yaml_FOLDED_SCALAR_STYLE: + return yaml_emitter_write_folded_scalar(emitter, + emitter.scalar_data.value) + + default: + panic("unknown scalar") + } + + return false +} + +/* + * Check if a %YAML directive is valid. + */ + +func yaml_emitter_analyze_version_directive(emitter *yaml_emitter_t, + version_directive yaml_version_directive_t) bool { + if version_directive.major != 1 || version_directive.minor != 1 { + return yaml_emitter_set_emitter_error(emitter, + "incompatible %YAML directive") + } + + return true +} + +/* + * Check if a %TAG directive is valid. + */ + +func yaml_emitter_analyze_tag_directive(emitter *yaml_emitter_t, + tag_directive *yaml_tag_directive_t) bool { + handle := tag_directive.handle + prefix := tag_directive.prefix + + if len(handle) == 0 { + return yaml_emitter_set_emitter_error(emitter, + "tag handle must not be empty") + } + + if handle[0] != '!' { + return yaml_emitter_set_emitter_error(emitter, + "tag handle must start with '!'") + } + + if handle[len(handle)-1] != '!' { + return yaml_emitter_set_emitter_error(emitter, + "tag handle must end with '!'") + } + + for i := 1; i < len(handle)-1; width(handle[i]) { + if !is_alpha(handle[i]) { + return yaml_emitter_set_emitter_error(emitter, + "tag handle must contain alphanumerical characters only") + } + } + + if len(prefix) == 0 { + return yaml_emitter_set_emitter_error(emitter, + "tag prefix must not be empty") + } + + return true +} + +/* + * Check if an anchor is valid. + */ + +func yaml_emitter_analyze_anchor(emitter *yaml_emitter_t, + anchor []byte, alias bool) bool { + if len(anchor) == 0 { + errmsg := "alias value must not be empty" + if !alias { + errmsg = "anchor value must not be empty" + } + return yaml_emitter_set_emitter_error(emitter, errmsg) + } + + for i := 0; i < len(anchor); i += width(anchor[i]) { + if !is_alpha(anchor[i]) { + errmsg := "alias value must contain alphanumerical characters only" + if !alias { + errmsg = "anchor value must contain alphanumerical characters only" + } + return yaml_emitter_set_emitter_error(emitter, errmsg) + } + } + + emitter.anchor_data.anchor = anchor + emitter.anchor_data.alias = alias + + return true +} + +/* + * Check if a tag is valid. + */ + +func yaml_emitter_analyze_tag(emitter *yaml_emitter_t, tag []byte) bool { + if len(tag) == 0 { + return yaml_emitter_set_emitter_error(emitter, + "tag value must not be empty") + } + + for i := range emitter.tag_directives { + tag_directive := &emitter.tag_directives[i] + if bytes.HasPrefix(tag, tag_directive.prefix) { + emitter.tag_data.handle = tag_directive.handle + emitter.tag_data.suffix = tag[len(tag_directive.prefix):] + return true + } + } + + emitter.tag_data.suffix = tag + + return true +} + +/* + * Check if a scalar is valid. + */ + +func yaml_emitter_analyze_scalar(emitter *yaml_emitter_t, value []byte) bool { + block_indicators := false + flow_indicators := false + line_breaks := false + special_characters := false + + leading_space := false + leading_break := false + trailing_space := false + trailing_break := false + break_space := false + space_break := false + + preceeded_by_whitespace := false + followed_by_whitespace := false + previous_space := false + previous_break := false + + emitter.scalar_data.value = value + + if len(value) == 0 { + emitter.scalar_data.multiline = false + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = false + + return true + } + + if (value[0] == '-' && value[1] == '-' && value[2] == '-') || + (value[0] == '.' && value[1] == '.' && value[2] == '.') { + block_indicators = true + flow_indicators = true + } + + preceeded_by_whitespace = true + + for i, w := 0, 0; i < len(value); i += w { + w = width(value[i]) + followed_by_whitespace = i+w >= len(value) || is_blankz_at(value, w) + + if i == 0 { + switch value[i] { + case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`': + flow_indicators = true + block_indicators = true + case '?', ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '-': + if followed_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } else { + switch value[i] { + case ',', '?', '[', ']', '{', '}': + flow_indicators = true + case ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '#': + if preceeded_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } + + if !is_printable_at(value, i) || (!is_ascii(value[i]) && !emitter.unicode) { + special_characters = true + } + + if is_break_at(value, i) { + line_breaks = true + } + + if is_space(value[i]) { + if i == 0 { + leading_space = true + } + if i+w == len(value) { + trailing_space = true + } + if previous_break { + break_space = true + } + previous_space = true + previous_break = false + } else if is_break_at(value, i) { + if i == 0 { + leading_break = true + } + if i+width(value[i]) == len(value) { + trailing_break = true + } + if previous_space { + space_break = true + } + previous_space = false + previous_break = true + } else { + previous_space = false + previous_break = false + } + + preceeded_by_whitespace = is_blankz_at(value, i) + } + + emitter.scalar_data.multiline = line_breaks + + emitter.scalar_data.flow_plain_allowed = true + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = true + + if leading_space || leading_break || trailing_space || trailing_break { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + + if trailing_space { + emitter.scalar_data.block_allowed = false + } + + if break_space { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + } + + if space_break || special_characters { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + emitter.scalar_data.block_allowed = false + } + + if line_breaks { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + + if flow_indicators { + emitter.scalar_data.flow_plain_allowed = false + } + + if block_indicators { + emitter.scalar_data.block_plain_allowed = false + } + + return true +} + +/* + * Check if the event data is valid. + */ + +func yaml_emitter_analyze_event(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.anchor_data.anchor = nil + emitter.tag_data.handle = nil + emitter.tag_data.suffix = nil + emitter.scalar_data.value = nil + + switch event.event_type { + case yaml_ALIAS_EVENT: + if !yaml_emitter_analyze_anchor(emitter, + event.anchor, true) { + return false + } + + case yaml_SCALAR_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, + event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || + (!event.implicit && + !event.quoted_implicit)) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + if !yaml_emitter_analyze_scalar(emitter, event.value) { + return false + } + case yaml_SEQUENCE_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, + event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || + !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, + event.tag) { + return false + } + } + case yaml_MAPPING_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, + event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || + !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, + event.tag) { + return false + } + } + + } + return true +} + +/* + * Write the BOM character. + */ + +func yaml_emitter_write_bom(emitter *yaml_emitter_t) bool { + if !flush(emitter) { + return false + } + + pos := emitter.buffer_pos + emitter.buffer[pos] = '\xEF' + emitter.buffer[pos+1] = '\xBB' + emitter.buffer[pos+2] = '\xBF' + emitter.buffer_pos += 3 + return true +} + +func yaml_emitter_write_indent(emitter *yaml_emitter_t) bool { + indent := emitter.indent + if indent < 0 { + indent = 0 + } + + if !emitter.indention || emitter.column > indent || + (emitter.column == indent && !emitter.whitespace) { + if !put_break(emitter) { + return false + } + } + + for emitter.column < indent { + if !put(emitter, ' ') { + return false + } + } + + emitter.whitespace = true + emitter.indention = true + + return true +} + +func yaml_emitter_write_indicator(emitter *yaml_emitter_t, + indicator []byte, need_whitespace bool, + is_whitespace bool, is_indention bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + ind_pos := 0 + for ind_pos < len(indicator) { + if !write(emitter, indicator, &ind_pos) { + return false + } + } + + emitter.whitespace = is_whitespace + emitter.indention = (emitter.indention && is_indention) + emitter.open_ended = false + + return true +} + +func yaml_emitter_write_anchor(emitter *yaml_emitter_t, value []byte) bool { + pos := 0 + for pos < len(value) { + if !write(emitter, value, &pos) { + return false + } + } + + emitter.whitespace = false + emitter.indention = false + + return true +} + +func yaml_emitter_write_tag_handle(emitter *yaml_emitter_t, value []byte) bool { + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + pos := 0 + for pos < len(value) { + if !write(emitter, value, &pos) { + return false + } + } + + emitter.whitespace = false + emitter.indention = false + + return true +} + +func yaml_emitter_write_tag_content(emitter *yaml_emitter_t, value []byte, + need_whitespace bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + for i := 0; i < len(value); { + write_it := false + switch value[i] { + case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', + '.', '!', '~', '*', '\'', '(', ')', '[', ']': + write_it = true + default: + write_it = is_alpha(value[i]) + } + if write_it { + if !write(emitter, value, &i) { + return false + } + } else { + w := width(value[i]) + for j := 0; j < w; j++ { + val := value[i] + i++ + + if !put(emitter, '%') { + return false + } + c := val >> 4 + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + + c = val & 0x0f + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + + } + } + } + + emitter.whitespace = false + emitter.indention = false + + return true +} + +func yaml_emitter_write_plain_scalar(emitter *yaml_emitter_t, value []byte, + allow_breaks bool) bool { + spaces := false + breaks := false + + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + for i := 0; i < len(value); { + if is_space(value[i]) { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + !is_space(value[i+1]) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break_at(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + + emitter.whitespace = false + emitter.indention = false + if emitter.root_context { + emitter.open_ended = true + } + + return true +} + +func yaml_emitter_write_single_quoted_scalar(emitter *yaml_emitter_t, value []byte, + allow_breaks bool) bool { + spaces := false + breaks := false + + if !yaml_emitter_write_indicator(emitter, []byte("'"), true, false, false) { + return false + } + + for i := 0; i < len(value); { + if is_space(value[i]) { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + i > 0 && i < len(value)-1 && + !is_space(value[i+1]) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break_at(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if value[i] == '\'' { + if !put(emitter, '\'') { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + + if !yaml_emitter_write_indicator(emitter, []byte("'"), false, false, false) { + return false + } + + emitter.whitespace = false + emitter.indention = false + + return true +} + +func yaml_emitter_write_double_quoted_scalar(emitter *yaml_emitter_t, value []byte, + allow_breaks bool) bool { + + spaces := false + + if !yaml_emitter_write_indicator(emitter, []byte("\""), true, false, false) { + return false + } + + for i := 0; i < len(value); { + if !is_printable_at(value, i) || (!emitter.unicode && !is_ascii(value[i])) || + is_bom_at(value, i) || is_break_at(value, i) || + value[i] == '"' || value[i] == '\\' { + octet := value[i] + + var w int + var v rune + switch { + case octet&0x80 == 0x00: + w, v = 1, rune(octet&0x7F) + case octet&0xE0 == 0xC0: + w, v = 2, rune(octet&0x1F) + case octet&0xF0 == 0xE0: + w, v = 3, rune(octet&0x0F) + case octet&0xF8 == 0xF0: + w, v = 4, rune(octet&0x07) + } + + for k := 1; k < w; k++ { + octet = value[i+k] + v = (v << 6) + (rune(octet) & 0x3F) + } + i += w + + if !put(emitter, '\\') { + return false + } + + switch v { + case 0x00: + if !put(emitter, '0') { + return false + } + case 0x07: + if !put(emitter, 'a') { + return false + } + case 0x08: + if !put(emitter, 'b') { + return false + } + case 0x09: + if !put(emitter, 't') { + return false + } + + case 0x0A: + if !put(emitter, 'n') { + return false + } + + case 0x0B: + if !put(emitter, 'v') { + return false + } + + case 0x0C: + if !put(emitter, 'f') { + return false + } + + case 0x0D: + if !put(emitter, 'r') { + return false + } + + case 0x1B: + if !put(emitter, 'e') { + return false + } + case 0x22: + if !put(emitter, '"') { + return false + } + case 0x5C: + if !put(emitter, '\\') { + return false + } + case 0x85: + if !put(emitter, 'N') { + return false + } + + case 0xA0: + if !put(emitter, '_') { + return false + } + + case 0x2028: + if !put(emitter, 'L') { + return false + } + + case 0x2029: + if !put(emitter, 'P') { + return false + } + default: + if v <= 0xFF { + if !put(emitter, 'x') { + return false + } + w = 2 + } else if v <= 0xFFFF { + if !put(emitter, 'u') { + return false + } + w = 4 + } else { + if !put(emitter, 'U') { + return false + } + w = 8 + } + for k := (w - 1) * 4; k >= 0; k -= 4 { + digit := byte((v >> uint(k)) & 0x0F) + c := digit + '0' + if c > 9 { + c = digit + 'A' - 10 + } + if !put(emitter, c) { + return false + } + } + } + spaces = false + } else if is_space(value[i]) { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + i > 0 && i < len(value)-1 { + if !yaml_emitter_write_indent(emitter) { + return false + } + if is_space(value[i+1]) { + if !put(emitter, '\\') { + return false + } + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else { + if !write(emitter, value, &i) { + return false + } + spaces = false + } + } + + if !yaml_emitter_write_indicator(emitter, []byte("\""), false, false, false) { + return false + } + + emitter.whitespace = false + emitter.indention = false + + return true +} + +func yaml_emitter_write_block_scalar_hints(emitter *yaml_emitter_t, value []byte) bool { + + if is_space(value[0]) || is_break_at(value, 0) { + indent_hint := []byte{'0' + byte(emitter.best_indent)} + if !yaml_emitter_write_indicator(emitter, indent_hint, false, false, false) { + return false + } + } + + emitter.open_ended = false + + var chomp_hint [1]byte + if len(value) == 0 { + chomp_hint[0] = '-' + } else { + i := len(value) - 1 + for value[i]&0xC0 == 0x80 { + i-- + } + + if !is_break_at(value, i) { + chomp_hint[0] = '-' + } else if i == 0 { + chomp_hint[0] = '+' + emitter.open_ended = true + } else { + for value[i]&0xC0 == 0x80 { + i-- + } + + if is_break_at(value, i) { + chomp_hint[0] = '+' + emitter.open_ended = true + } + } + } + + if chomp_hint[0] != 0 { + if !yaml_emitter_write_indicator(emitter, chomp_hint[:], false, false, false) { + return false + } + } + + return true +} + +func yaml_emitter_write_literal_scalar(emitter *yaml_emitter_t, value []byte) bool { + + breaks := true + + if !yaml_emitter_write_indicator(emitter, []byte("|"), true, false, false) { + return false + } + + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + + if !put_break(emitter) { + return false + } + + emitter.indention = true + emitter.whitespace = true + + for i := 0; i < len(value); { + if is_break_at(value, i) { + if !write_break(emitter, value, &i) { + return false + } + emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + breaks = false + } + } + + return true +} + +func yaml_emitter_write_folded_scalar(emitter *yaml_emitter_t, value []byte) bool { + breaks := true + leading_spaces := true + + if !yaml_emitter_write_indicator(emitter, []byte(">"), true, false, false) { + return false + } + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + if !put_break(emitter) { + return false + } + emitter.indention = true + emitter.whitespace = true + + for i := 0; i < len(value); { + if is_break_at(value, i) { + if !breaks && !leading_spaces && value[i] == '\n' { + k := i + for is_break_at(value, k) { + k += width(value[k]) + } + if !is_blankz_at(value, k) { + if !put_break(emitter) { + return false + } + } + } + if !write_break(emitter, value, &i) { + return false + } + emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + leading_spaces = is_blank(value[i]) + } + if !breaks && is_space(value[i]) && !is_space(value[i+1]) && + emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + emitter.indention = false + breaks = false + } + } + + return true +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode.go new file mode 100644 index 00000000000..3ff02d63e2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode.go @@ -0,0 +1,358 @@ +/* +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 candiedyaml + +import ( + "bytes" + "encoding/base64" + "io" + "math" + "reflect" + "sort" + "strconv" + "time" +) + +var ( + timeTimeType = reflect.TypeOf(time.Time{}) + marshalerType = reflect.TypeOf(new(Marshaler)).Elem() + numberType = reflect.TypeOf(Number("")) +) + +type Marshaler interface { + MarshalYAML() (tag string, value interface{}) +} + +// An Encoder writes JSON objects to an output stream. +type Encoder struct { + w io.Writer + emitter yaml_emitter_t + event yaml_event_t + flow bool + err error +} + +func Marshal(v interface{}) ([]byte, error) { + b := bytes.Buffer{} + e := NewEncoder(&b) + err := e.Encode(v) + return b.Bytes(), err +} + +// NewEncoder returns a new encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + e := &Encoder{w: w} + yaml_emitter_initialize(&e.emitter) + yaml_emitter_set_output_writer(&e.emitter, e.w) + yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING) + e.emit() + yaml_document_start_event_initialize(&e.event, nil, nil, true) + e.emit() + + return e +} + +func (e *Encoder) Encode(v interface{}) (err error) { + defer recovery(&err) + + if e.err != nil { + return e.err + } + + e.marshal("", reflect.ValueOf(v), true) + + yaml_document_end_event_initialize(&e.event, true) + e.emit() + e.emitter.open_ended = false + yaml_stream_end_event_initialize(&e.event) + e.emit() + + return nil +} + +func (e *Encoder) emit() { + if !yaml_emitter_emit(&e.emitter, &e.event) { + panic("bad emit") + } +} + +func (e *Encoder) marshal(tag string, v reflect.Value, allowAddr bool) { + vt := v.Type() + + if vt.Implements(marshalerType) { + e.emitMarshaler(tag, v) + return + } + + if vt.Kind() != reflect.Ptr && allowAddr { + if reflect.PtrTo(vt).Implements(marshalerType) { + e.emitAddrMarshaler(tag, v) + return + } + } + + switch v.Kind() { + case reflect.Interface: + if v.IsNil() { + e.emitNil() + } else { + e.marshal(tag, v.Elem(), allowAddr) + } + case reflect.Map: + e.emitMap(tag, v) + case reflect.Ptr: + if v.IsNil() { + e.emitNil() + } else { + e.marshal(tag, v.Elem(), true) + } + case reflect.Struct: + e.emitStruct(tag, v) + case reflect.Slice: + e.emitSlice(tag, v) + case reflect.String: + e.emitString(tag, v) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + e.emitInt(tag, v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + e.emitUint(tag, v) + case reflect.Float32, reflect.Float64: + e.emitFloat(tag, v) + case reflect.Bool: + e.emitBool(tag, v) + default: + panic("Can't marshal type yet: " + v.Type().String()) + } +} + +func (e *Encoder) emitMap(tag string, v reflect.Value) { + e.mapping(tag, func() { + var keys stringValues = v.MapKeys() + sort.Sort(keys) + for _, k := range keys { + e.marshal("", k, true) + e.marshal("", v.MapIndex(k), true) + } + }) +} + +func (e *Encoder) emitStruct(tag string, v reflect.Value) { + if v.Type() == timeTimeType { + e.emitTime(tag, v) + return + } + + fields := cachedTypeFields(v.Type()) + + e.mapping(tag, func() { + for _, f := range fields { + fv := fieldByIndex(v, f.index) + if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) { + continue + } + + e.marshal("", reflect.ValueOf(f.name), true) + e.flow = f.flow + e.marshal("", fv, true) + } + }) +} + +func (e *Encoder) emitTime(tag string, v reflect.Value) { + t := v.Interface().(time.Time) + bytes, _ := t.MarshalText() + e.emitScalar(string(bytes), "", tag, yaml_PLAIN_SCALAR_STYLE) +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func (e *Encoder) mapping(tag string, f func()) { + implicit := tag == "" + style := yaml_BLOCK_MAPPING_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_MAPPING_STYLE + } + yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style) + e.emit() + + f() + + yaml_mapping_end_event_initialize(&e.event) + e.emit() +} + +func (e *Encoder) emitSlice(tag string, v reflect.Value) { + if v.Type() == byteSliceType { + e.emitBase64(tag, v) + return + } + + implicit := tag == "" + style := yaml_BLOCK_SEQUENCE_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_SEQUENCE_STYLE + } + yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style) + e.emit() + + n := v.Len() + for i := 0; i < n; i++ { + e.marshal("", v.Index(i), true) + } + + yaml_sequence_end_event_initialize(&e.event) + e.emit() +} + +func (e *Encoder) emitBase64(tag string, v reflect.Value) { + if v.IsNil() { + e.emitNil() + return + } + + s := v.Bytes() + + dst := make([]byte, base64.StdEncoding.EncodedLen(len(s))) + + base64.StdEncoding.Encode(dst, s) + e.emitScalar(string(dst), "", "!!binary", yaml_DOUBLE_QUOTED_SCALAR_STYLE) +} + +func (e *Encoder) emitString(tag string, v reflect.Value) { + var style yaml_scalar_style_t + s := v.String() + + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + + if v.Type() == numberType { + style = yaml_PLAIN_SCALAR_STYLE + } else { + event := yaml_event_t{ + implicit: true, + value: []byte(s), + } + if tag, _ := resolveInterface(event, false); tag == "!!str" { + style = yaml_PLAIN_SCALAR_STYLE + } + } + + e.emitScalar(s, "", tag, style) +} + +func (e *Encoder) emitBool(tag string, v reflect.Value) { + s := strconv.FormatBool(v.Bool()) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE) +} + +func (e *Encoder) emitInt(tag string, v reflect.Value) { + s := strconv.FormatInt(v.Int(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE) +} + +func (e *Encoder) emitUint(tag string, v reflect.Value) { + s := strconv.FormatUint(v.Uint(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE) +} + +func (e *Encoder) emitFloat(tag string, v reflect.Value) { + f := v.Float() + + var s string + switch { + case math.IsNaN(f): + s = ".nan" + case math.IsInf(f, 1): + s = "+.inf" + case math.IsInf(f, -1): + s = "-.inf" + default: + s = strconv.FormatFloat(f, 'g', -1, v.Type().Bits()) + } + + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE) +} + +func (e *Encoder) emitNil() { + e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE) +} + +func (e *Encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t) { + implicit := tag == "" + if !implicit { + style = yaml_PLAIN_SCALAR_STYLE + } + yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style) + e.emit() +} + +func (e *Encoder) emitMarshaler(tag string, v reflect.Value) { + if v.Kind() == reflect.Ptr && v.IsNil() { + e.emitNil() + return + } + + m := v.Interface().(Marshaler) + if m == nil { + e.emitNil() + return + } + t, val := m.MarshalYAML() + if val == nil { + e.emitNil() + return + } + + e.marshal(t, reflect.ValueOf(val), false) +} + +func (e *Encoder) emitAddrMarshaler(tag string, v reflect.Value) { + if !v.CanAddr() { + e.marshal(tag, v, false) + return + } + + va := v.Addr() + if va.IsNil() { + e.emitNil() + return + } + + m := v.Interface().(Marshaler) + t, val := m.MarshalYAML() + if val == nil { + e.emitNil() + return + } + + e.marshal(t, reflect.ValueOf(val), false) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode_test.go new file mode 100644 index 00000000000..a4856d3053b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/encode_test.go @@ -0,0 +1,549 @@ +/* +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 candiedyaml + +import ( + "bytes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "math" + "time" +) + +var _ = Describe("Encode", func() { + var buf *bytes.Buffer + var enc *Encoder + + BeforeEach(func() { + buf = &bytes.Buffer{} + enc = NewEncoder(buf) + }) + + Context("Scalars", func() { + It("handles strings", func() { + err := enc.Encode("abc") + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`abc +`)) + }) + + Context("handles ints", func() { + It("handles ints", func() { + err := enc.Encode(13) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("13\n")) + }) + + It("handles uints", func() { + err := enc.Encode(uint64(1)) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("1\n")) + }) + }) + + Context("handles floats", func() { + It("handles float32", func() { + err := enc.Encode(float32(1.234)) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("1.234\n")) + + }) + + It("handles float64", func() { + err := enc.Encode(float64(1.2e23)) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("1.2e+23\n")) + }) + + It("handles NaN", func() { + err := enc.Encode(math.NaN()) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(".nan\n")) + }) + + It("handles infinity", func() { + err := enc.Encode(math.Inf(-1)) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("-.inf\n")) + }) + }) + + It("handles bools", func() { + err := enc.Encode(true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("true\n")) + }) + + It("handles time.Time", func() { + t := time.Now() + err := enc.Encode(t) + Ω(err).ShouldNot(HaveOccurred()) + bytes, _ := t.MarshalText() + Ω(buf.String()).Should(Equal(string(bytes) + "\n")) + }) + + Context("Null", func() { + It("fails on nil", func() { + err := enc.Encode(nil) + Ω(err).Should(HaveOccurred()) + }) + }) + + It("handles []byte", func() { + err := enc.Encode([]byte{'a', 'b', 'c'}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("!!binary YWJj\n")) + }) + + Context("Ptrs", func() { + It("handles ptr of a type", func() { + p := new(int) + *p = 10 + err := enc.Encode(p) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("10\n")) + }) + + It("handles nil ptr", func() { + var p *int + err := enc.Encode(p) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("null\n")) + }) + }) + + Context("Structs", func() { + It("handles simple structs", func() { + type batter struct { + Name string + HR int64 + AVG float64 + } + + batters := []batter{ + batter{Name: "Mark McGwire", HR: 65, AVG: 0.278}, + batter{Name: "Sammy Sosa", HR: 63, AVG: 0.288}, + } + err := enc.Encode(batters) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`- Name: Mark McGwire + HR: 65 + AVG: 0.278 +- Name: Sammy Sosa + HR: 63 + AVG: 0.288 +`)) + }) + + It("handles tagged structs", func() { + type batter struct { + Name string `yaml:"name"` + HR int64 + AVG float64 `yaml:"avg"` + } + + batters := []batter{ + batter{Name: "Mark McGwire", HR: 65, AVG: 0.278}, + batter{Name: "Sammy Sosa", HR: 63, AVG: 0.288}, + } + err := enc.Encode(batters) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`- name: Mark McGwire + HR: 65 + avg: 0.278 +- name: Sammy Sosa + HR: 63 + avg: 0.288 +`)) + }) + + It("handles nested structs", func() { + type nestedConfig struct { + AString string `yaml:"str"` + Integer int `yaml:"int"` + } + type config struct { + TopString string + Nested nestedConfig + } + + cfg := config{ + TopString: "def", + Nested: nestedConfig{ + AString: "abc", + Integer: 123, + }, + } + + err := enc.Encode(cfg) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`TopString: def +Nested: + str: abc + int: 123 +`)) + }) + + It("handles inline structs", func() { + type NestedConfig struct { + AString string `yaml:"str"` + Integer int `yaml:"int"` + } + type config struct { + TopString string + NestedConfig + } + + cfg := config{ + TopString: "def", + NestedConfig: NestedConfig{ + AString: "abc", + Integer: 123, + }, + } + + err := enc.Encode(cfg) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`TopString: def +str: abc +int: 123 +`)) + }) + + It("handles inline structs with conflicts", func() { + type NestedConfig struct { + AString string `yaml:"str"` + Integer int `yaml:"int"` + } + type config struct { + AString string `yaml:"str"` + NestedConfig + } + + cfg := config{ + AString: "def", + NestedConfig: NestedConfig{ + AString: "abc", + Integer: 123, + }, + } + + err := enc.Encode(cfg) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`str: def +int: 123 +`)) + }) + + }) + + }) + + Context("Sequence", func() { + It("handles slices", func() { + val := []string{"a", "b", "c"} + err := enc.Encode(val) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`- a +- b +- c +`)) + }) + }) + + Context("Maps", func() { + It("Decodes simple maps", func() { + err := enc.Encode(&map[string]string{ + "name": "Mark McGwire", + "hr": "65", + "avg": "0.278", + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`avg: "0.278" +hr: "65" +name: Mark McGwire +`)) + }) + + It("Decodes mix types", func() { + err := enc.Encode(&map[string]interface{}{ + "name": "Mark McGwire", + "hr": 65, + "avg": 0.278, + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`avg: 0.278 +hr: 65 +name: Mark McGwire +`)) + }) + }) + + Context("Sequence of Maps", func() { + It("decodes", func() { + err := enc.Encode([]map[string]interface{}{ + {"name": "Mark McGwire", + "hr": 65, + "avg": 0.278, + }, + {"name": "Sammy Sosa", + "hr": 63, + "avg": 0.288, + }, + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`- avg: 0.278 + hr: 65 + name: Mark McGwire +- avg: 0.288 + hr: 63 + name: Sammy Sosa +`)) + }) + }) + + Context("Maps of Sequence", func() { + It("decodes", func() { + err := enc.Encode(map[string][]interface{}{ + "name": []interface{}{"Mark McGwire", "Sammy Sosa"}, + "hr": []interface{}{65, 63}, + "avg": []interface{}{0.278, 0.288}, + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(buf.String()).Should(Equal(`avg: +- 0.278 +- 0.288 +hr: +- 65 +- 63 +name: +- Mark McGwire +- Sammy Sosa +`)) + }) + }) + + Context("Flow", func() { + It("flows structs", func() { + type i struct { + A string + } + type o struct { + I i `yaml:"i,flow"` + } + + err := enc.Encode(o{ + I: i{A: "abc"}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`i: {A: abc} +`)) + }) + + It("flows sequences", func() { + type i struct { + A string + } + type o struct { + I []i `yaml:"i,flow"` + } + + err := enc.Encode(o{ + I: []i{{A: "abc"}}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`i: [{A: abc}] +`)) + }) + }) + + Context("Omit empty", func() { + It("omits nil ptrs", func() { + type i struct { + A *string `yaml:"a,omitempty"` + } + type o struct { + I []i `yaml:"i,flow"` + } + + err := enc.Encode(o{ + I: []i{{A: nil}}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`i: [{}] +`)) + }) + + }) + + Context("Skip field", func() { + It("does not include the field", func() { + type a struct { + B string `yaml:"-"` + C string + } + + err := enc.Encode(a{B: "b", C: "c"}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`C: c +`)) + }) + }) + + Context("Marshaler support", func() { + Context("Receiver is a value", func() { + It("uses the Marshaler interface when a value", func() { + err := enc.Encode(hasMarshaler{Value: 123}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("123\n")) + }) + + It("uses the Marshaler interface when a pointer", func() { + err := enc.Encode(&hasMarshaler{Value: "abc"}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`abc +`)) + }) + }) + + Context("Receiver is a pointer", func() { + It("uses the Marshaler interface when a pointer", func() { + err := enc.Encode(&hasPtrMarshaler{Value: map[string]string{"a": "b"}}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`a: b +`)) + }) + + It("skips the Marshaler when its a value", func() { + err := enc.Encode(hasPtrMarshaler{Value: map[string]string{"a": "b"}}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`Tag: "" +Value: + a: b +`)) + }) + + Context("the receiver is nil", func() { + var ptr *hasPtrMarshaler + + It("returns a null", func() { + err := enc.Encode(ptr) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`null +`)) + }) + + It("returns a null value for ptr types", func() { + err := enc.Encode(map[string]*hasPtrMarshaler{"a": ptr}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`a: null +`)) + }) + + It("panics when used as a nil interface", func() { + Ω(func() { enc.Encode(map[string]Marshaler{"a": ptr}) }).Should(Panic()) + }) + }) + + Context("the receiver has a nil value", func() { + ptr := &hasPtrMarshaler{Value: nil} + + It("returns null", func() { + err := enc.Encode(ptr) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`null +`)) + }) + + Context("in a map", func() { + It("returns a null value for ptr types", func() { + err := enc.Encode(map[string]*hasPtrMarshaler{"a": ptr}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`a: null +`)) + }) + + It("returns a null value for interface types", func() { + err := enc.Encode(map[string]Marshaler{"a": ptr}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`a: null +`)) + }) + }) + + Context("in a slice", func() { + It("returns a null value for ptr types", func() { + err := enc.Encode([]*hasPtrMarshaler{ptr}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`- null +`)) + }) + + It("returns a null value for interface types", func() { + err := enc.Encode([]Marshaler{ptr}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal(`- null +`)) + }) + }) + }) + }) + }) + + Context("Number type", func() { + It("encodes as a number", func() { + n := Number("12345") + err := enc.Encode(n) + Ω(err).ShouldNot(HaveOccurred()) + Ω(buf.String()).Should(Equal("12345\n")) + }) + }) +}) + +type hasMarshaler struct { + Value interface{} +} + +func (m hasMarshaler) MarshalYAML() (tag string, value interface{}) { + return "", m.Value +} + +func (m hasMarshaler) UnmarshalYAML(tag string, value interface{}) error { + m.Value = value + return nil +} + +type hasPtrMarshaler struct { + Tag string + Value interface{} +} + +func (m *hasPtrMarshaler) MarshalYAML() (tag string, value interface{}) { + return "", m.Value +} + +func (m *hasPtrMarshaler) UnmarshalYAML(tag string, value interface{}) error { + m.Tag = tag + m.Value = value + return nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_1.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_1.yaml new file mode 100644 index 00000000000..d12e67111b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_1.yaml @@ -0,0 +1,3 @@ +- Mark McGwire +- Sammy Sosa +- Ken Griffey diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_10.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_10.yaml new file mode 100644 index 00000000000..61808f678e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_10.yaml @@ -0,0 +1,8 @@ +--- +hr: + - Mark McGwire + # Following node labeled SS + - &SS Sammy Sosa +rbi: + - *SS # Subsequent occurrence + - Ken Griffey diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_11.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_11.yaml new file mode 100644 index 00000000000..9123ce21348 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_11.yaml @@ -0,0 +1,9 @@ +? - Detroit Tigers + - Chicago cubs +: + - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_12.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_12.yaml new file mode 100644 index 00000000000..1fc33f9d772 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_12.yaml @@ -0,0 +1,8 @@ +--- +# products purchased +- item : Super Hoop + quantity: 1 +- item : Basketball + quantity: 4 +- item : Big Shoes + quantity: 1 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_13.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_13.yaml new file mode 100644 index 00000000000..13fb6560102 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_13.yaml @@ -0,0 +1,4 @@ +# ASCII Art +--- | + \//||\/|| + // || ||__ diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_14.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_14.yaml new file mode 100644 index 00000000000..59943def960 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_14.yaml @@ -0,0 +1,4 @@ +--- + Mark McGwire's + year was crippled + by a knee injury. diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15.yaml new file mode 100644 index 00000000000..80b89a6d9c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15.yaml @@ -0,0 +1,8 @@ +> + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15_dumped.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15_dumped.yaml new file mode 100644 index 00000000000..cc2d963e002 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_15_dumped.yaml @@ -0,0 +1,7 @@ +> + Sammy Sosa completed another fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_16.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_16.yaml new file mode 100644 index 00000000000..9f66d881c4b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_16.yaml @@ -0,0 +1,7 @@ +name: Mark McGwire +accomplishment: > + Mark set a major league + home run record in 1998. +stats: | + 65 Home Runs + 0.278 Batting Average diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17.yaml new file mode 100644 index 00000000000..3e899c08611 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17.yaml @@ -0,0 +1,7 @@ +unicode: "Sosa did fine.\u263A" +control: "\b1998\t1999\t2000\n" +hexesc: "\x0D\x0A is \r\n" + +single: '"Howdy!" he cried.' +quoted: ' # not a ''comment''.' +tie-fighter: '|\-*-/|' diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_control.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_control.yaml new file mode 100644 index 00000000000..59398a61bba --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_control.yaml @@ -0,0 +1,2 @@ +control: "\b1998\t1999\t2000\n" + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_hexesc.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_hexesc.yaml new file mode 100644 index 00000000000..7ddff26cbf2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_hexesc.yaml @@ -0,0 +1,2 @@ +hexesc: "\x0D\x0A is \r\n" + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_quoted.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_quoted.yaml new file mode 100644 index 00000000000..bedc4a5076a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_quoted.yaml @@ -0,0 +1,2 @@ +quoted: ' # not a ''comment''.' + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_single.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_single.yaml new file mode 100644 index 00000000000..c3fe6aad26e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_single.yaml @@ -0,0 +1 @@ +single: '"Howdy!" he cried.' diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_tie_fighter.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_tie_fighter.yaml new file mode 100644 index 00000000000..9d821731706 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_tie_fighter.yaml @@ -0,0 +1 @@ +tie-fighter: '|\-*-/|' diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_unicode.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_unicode.yaml new file mode 100644 index 00000000000..2b378bd4a55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_17_unicode.yaml @@ -0,0 +1,2 @@ +unicode: "Sosa did fine.\u263A" + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_18.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_18.yaml new file mode 100644 index 00000000000..e0a8bfa9922 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_18.yaml @@ -0,0 +1,6 @@ +plain: + This unquoted scalar + spans many lines. + +quoted: "So does this + quoted scalar.\n" diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_19.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_19.yaml new file mode 100644 index 00000000000..8aeb1a481c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_19.yaml @@ -0,0 +1,5 @@ +canonical: 12345 +decimal: +12_345 +sexagesimal: 3:25:45 +octal: 014 +hexadecimal: 0xC diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_2.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_2.yaml new file mode 100644 index 00000000000..7b7ec948db7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_2.yaml @@ -0,0 +1,3 @@ +hr: 65 # Home runs +avg: 0.278 # Batting average +rbi: 147 # Runs Batted In diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_20.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_20.yaml new file mode 100644 index 00000000000..60bfc06814a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_20.yaml @@ -0,0 +1,6 @@ +canonical: 1.23015e+3 +exponential: 12.3015e+02 +sexagesimal: 20:30.15 +fixed: 1_230.15 +negative infinity: -.inf +not a number: .NaN diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_21.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_21.yaml new file mode 100644 index 00000000000..c065b2ae98b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_21.yaml @@ -0,0 +1,4 @@ +null: ~ +true: yes +false: no +string: '12345' diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_22.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_22.yaml new file mode 100644 index 00000000000..aaac185a987 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_22.yaml @@ -0,0 +1,4 @@ +canonical: 2001-12-15T02:59:43.1Z +iso8601: 2001-12-14t21:59:43.10-05:00 +spaced: 2001-12-14 21:59:43.10 -5 +date: 2002-12-14 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23.yaml new file mode 100644 index 00000000000..adbe4e62b94 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23.yaml @@ -0,0 +1,14 @@ +--- +not-date: !!str 2002-04-28 + +picture: !!binary "\ + R0lGODlhDAAMAIQAAP//9/X\ + 17unp5WZmZgAAAOfn515eXv\ + Pz7Y6OjuDg4J+fn5OTk6enp\ + 56enmleECcgggoBADs=" + +application specific tag: !something | + The semantics of the tag + above may be different for + different documents. + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_application.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_application.yaml new file mode 100644 index 00000000000..03cc760303e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_application.yaml @@ -0,0 +1,5 @@ +--- +application specific tag: !something | + The semantics of the tag + above may be different for + different documents. diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_non_date.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_non_date.yaml new file mode 100644 index 00000000000..2e95415d941 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_non_date.yaml @@ -0,0 +1,3 @@ +--- +not-date: !!str 2002-04-28 + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_picture.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_picture.yaml new file mode 100644 index 00000000000..b87063e18ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_23_picture.yaml @@ -0,0 +1,9 @@ +--- +picture: !!binary "\ + R0lGODlhDAAMAIQAAP//9/X\ + 17unp5WZmZgAAAOfn515eXv\ + Pz7Y6OjuDg4J+fn5OTk6enp\ + 56enmleECcgggoBADs=" + + + \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24.yaml new file mode 100644 index 00000000000..1180757d81c --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24.yaml @@ -0,0 +1,14 @@ +%TAG ! tag:clarkevans.com,2002: +--- !shape + # Use the ! handle for presenting + # tag:clarkevans.com,2002:circle +- !circle + center: &ORIGIN {x: 73, y: 129} + radius: 7 +- !line + start: *ORIGIN + finish: { x: 89, y: 102 } +- !label + start: *ORIGIN + color: 0xFFEEBB + text: Pretty vector drawing. diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24_dumped.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24_dumped.yaml new file mode 100644 index 00000000000..1742cd21626 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_24_dumped.yaml @@ -0,0 +1,11 @@ +!shape +- !circle + center: &id001 {x: 73, y: 129} + radius: 7 +- !line + finish: {x: 89, y: 102} + start: *id001 +- !label + color: 0xFFEEBB + start: *id001 + text: Pretty vector drawing. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_25.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_25.yaml new file mode 100644 index 00000000000..769ac319168 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_25.yaml @@ -0,0 +1,7 @@ +# sets are represented as a +# mapping where each key is +# associated with the empty string +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griff diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_26.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_26.yaml new file mode 100644 index 00000000000..3143763dd09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_26.yaml @@ -0,0 +1,7 @@ +# ordered maps are represented as +# a sequence of mappings, with +# each mapping having one key +--- !!omap +- Mark McGwire: 65 +- Sammy Sosa: 63 +- Ken Griffy: 58 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27.yaml new file mode 100644 index 00000000000..395e79c440a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27.yaml @@ -0,0 +1,29 @@ +--- ! +invoice: 34843 +date : 2001-01-23 +billTo: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +shipTo: *id001 +product: + - sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 + - sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27_dumped.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27_dumped.yaml new file mode 100644 index 00000000000..51a89b889ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_27_dumped.yaml @@ -0,0 +1,20 @@ +!!org.yaml.snakeyaml.Invoice +billTo: &id001 + address: + city: Royal Oak + lines: | + 458 Walkman Dr. + Suite #292 + postal: '48046' + state: MI + family: Dumars + given: Chris +comments: Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338. +date: '2001-01-23' +invoice: 34843 +product: +- {description: Basketball, price: 450.0, quantity: 4, sku: BL394D} +- {description: Super Hoop, price: 2392.0, quantity: 1, sku: BL4438H} +shipTo: *id001 +tax: 251.42 +total: 4443.52 \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_28.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_28.yaml new file mode 100644 index 00000000000..eb5fb8afd7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_28.yaml @@ -0,0 +1,29 @@ +--- +Time: 2001-11-23 15:01:42 -5 +User: ed +Warning: + This is an error message + for the log file +--- +Time: 2001-11-23 15:02:31 -5 +User: ed +Warning: + A slightly different error + message. +--- +Date: 2001-11-23 15:03:17 -5 +User: ed +Fatal: + Unknown variable "bar" +Stack: + - file: TopClass.py + line: 23 + code: | + x = MoreObject("345\n") + - file: MoreClass.py + line: 58 + code: |- + foo = bar + + + diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_3.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_3.yaml new file mode 100644 index 00000000000..2c884b7a2ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_3.yaml @@ -0,0 +1,8 @@ +american: + - Boston Red Sox + - Detroit Tigers + - New York Yankees +national: + - New York Mets + - Chicago Cubs + - Atlanta Braves \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_4.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_4.yaml new file mode 100644 index 00000000000..430f6b3dbe1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_4.yaml @@ -0,0 +1,8 @@ +- + name: Mark McGwire + hr: 65 + avg: 0.278 +- + name: Sammy Sosa + hr: 63 + avg: 0.288 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_5.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_5.yaml new file mode 100644 index 00000000000..cdd7770628b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_5.yaml @@ -0,0 +1,3 @@ +- [name , hr, avg ] +- [Mark McGwire, 65, 0.278] +- [Sammy Sosa , 63, 0.288] diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_6.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_6.yaml new file mode 100644 index 00000000000..7a957b23a1b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_6.yaml @@ -0,0 +1,5 @@ +Mark McGwire: {hr: 65, avg: 0.278} +Sammy Sosa: { + hr: 63, + avg: 0.288 + } diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_7.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_7.yaml new file mode 100644 index 00000000000..bc711d547cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_7.yaml @@ -0,0 +1,10 @@ +# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_8.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_8.yaml new file mode 100644 index 00000000000..05e102d8ebd --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_8.yaml @@ -0,0 +1,10 @@ +--- +time: 20:03:20 +player: Sammy Sosa +action: strike (miss) +... +--- +time: 20:03:47 +player: Sammy Sosa +action: grand slam +... diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_9.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_9.yaml new file mode 100644 index 00000000000..e2641805396 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example2_9.yaml @@ -0,0 +1,8 @@ +--- +hr: # 1998 hr ranking + - Mark McGwire + - Sammy Sosa +rbi: + # 1998 rbi ranking + - Sammy Sosa + - Ken Griffey diff --git a/src/code.google.com/p/go.net/.hg/store/phaseroots b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example_empty.yaml similarity index 100% rename from src/code.google.com/p/go.net/.hg/store/phaseroots rename to Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/example_empty.yaml diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map.yaml new file mode 100644 index 00000000000..022446df466 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map.yaml @@ -0,0 +1,6 @@ +# Unordered set of key: value pairs. +Block style: !!map + Clark : Evans + Brian : Ingerson + Oren : Ben-Kiki +Flow style: !!map { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki } diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map_mixed_tags.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map_mixed_tags.yaml new file mode 100644 index 00000000000..a5d35b05dbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/map_mixed_tags.yaml @@ -0,0 +1,6 @@ +# Unordered set of key: value pairs. +Block style: ! + Clark : Evans + Brian : Ingerson + Oren : Ben-Kiki +Flow style: { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki } diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/merge.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/merge.yaml new file mode 100644 index 00000000000..ee4a48fe216 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/merge.yaml @@ -0,0 +1,27 @@ +--- +- &CENTER { x: 1, y: 2 } +- &LEFT { x: 0, y: 2 } +- &BIG { r: 10 } +- &SMALL { r: 1 } + +# All the following maps are equal: + +- # Explicit keys + x: 1 + y: 2 + r: 10 + label: center/big + +- # Merge one map + << : *CENTER + r: 10 + label: center/big + +- # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + +- # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: center/big diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/omap.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/omap.yaml new file mode 100644 index 00000000000..4fa0f45f26f --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/omap.yaml @@ -0,0 +1,8 @@ +# Explicitly typed ordered map (dictionary). +Bestiary: !!omap + - aardvark: African pig-like ant eater. Ugly. + - anteater: South-American ant eater. Two species. + - anaconda: South-American constrictor snake. Scaly. + # Etc. +# Flow style +Numbers: !!omap [ one: 1, two: 2, three : 3 ] diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/pairs.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/pairs.yaml new file mode 100644 index 00000000000..05f55b94260 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/pairs.yaml @@ -0,0 +1,7 @@ +# Explicitly typed pairs. +Block tasks: !!pairs + - meeting: with team. + - meeting: with boss. + - break: lunch. + - meeting: with client. +Flow tasks: !!pairs [ meeting: with team, meeting: with boss ] diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/seq.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/seq.yaml new file mode 100644 index 00000000000..5849115c942 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/seq.yaml @@ -0,0 +1,14 @@ +# Ordered sequence of nodes +Block style: !!seq +- Mercury # Rotates - no light/dark sides. +- Venus # Deadliest. Aptly named. +- Earth # Mostly dirt. +- Mars # Seems empty. +- Jupiter # The king. +- Saturn # Pretty. +- Uranus # Where the sun hardly shines. +- Neptune # Boring. No rings. +- Pluto # You call this a planet? +Flow style: !!seq [ Mercury, Venus, Earth, Mars, # Rocks + Jupiter, Saturn, Uranus, Neptune, # Gas + Pluto ] # Overrated diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/set.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/set.yaml new file mode 100644 index 00000000000..e05dc885796 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/set.yaml @@ -0,0 +1,7 @@ +# Explicitly typed set. +baseball players: !!set + ? Mark McGwire + ? Sammy Sosa + ? Ken Griffey +# Flow style +baseball teams: !!set { Boston Red Sox, Detroit Tigers, New York Yankees } diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/v.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/v.yaml new file mode 100644 index 00000000000..81c5d51f711 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/v.yaml @@ -0,0 +1,4 @@ +--- # New schema +link with: + - = : library1.dll + version: 1.2 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/value.yaml b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/value.yaml new file mode 100644 index 00000000000..3eb79198889 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/fixtures/specification/types/value.yaml @@ -0,0 +1,10 @@ +--- # Old schema +link with: + - library1.dll + - library2.dll +--- # New schema +link with: + - = : library1.dll + version: 1.2 + - = : library2.dll + version: 2.3 diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/libyaml-LICENSE b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/libyaml-LICENSE new file mode 100644 index 00000000000..050ced23f68 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/libyaml-LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser.go new file mode 100644 index 00000000000..8d38e306541 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser.go @@ -0,0 +1,1230 @@ +/* +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 candiedyaml + +import ( + "bytes" +) + +/* + * The parser implements the following grammar: + * + * stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + * implicit_document ::= block_node DOCUMENT-END* + * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + * block_node_or_indentless_sequence ::= + * ALIAS + * | properties (block_content | indentless_block_sequence)? + * | block_content + * | indentless_block_sequence + * block_node ::= ALIAS + * | properties block_content? + * | block_content + * flow_node ::= ALIAS + * | properties flow_content? + * | flow_content + * properties ::= TAG ANCHOR? | ANCHOR TAG? + * block_content ::= block_collection | flow_collection | SCALAR + * flow_content ::= flow_collection | SCALAR + * block_collection ::= block_sequence | block_mapping + * flow_collection ::= flow_sequence | flow_mapping + * block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + * indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + * block_mapping ::= BLOCK-MAPPING_START + * ((KEY block_node_or_indentless_sequence?)? + * (VALUE block_node_or_indentless_sequence?)?)* + * BLOCK-END + * flow_sequence ::= FLOW-SEQUENCE-START + * (flow_sequence_entry FLOW-ENTRY)* + * flow_sequence_entry? + * FLOW-SEQUENCE-END + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * flow_mapping ::= FLOW-MAPPING-START + * (flow_mapping_entry FLOW-ENTRY)* + * flow_mapping_entry? + * FLOW-MAPPING-END + * flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + */ + +/* + * Peek the next token in the token queue. + */ +func peek_token(parser *yaml_parser_t) *yaml_token_t { + if parser.token_available || yaml_parser_fetch_more_tokens(parser) { + return &parser.tokens[parser.tokens_head] + } + return nil +} + +/* + * Remove the next token from the queue (must be called after peek_token). + */ +func skip_token(parser *yaml_parser_t) { + parser.token_available = false + parser.tokens_parsed++ + parser.stream_end_produced = parser.tokens[parser.tokens_head].token_type == yaml_STREAM_END_TOKEN + parser.tokens_head++ +} + +/* + * Get the next event. + */ + +func yaml_parser_parse(parser *yaml_parser_t, event *yaml_event_t) bool { + /* Erase the event object. */ + *event = yaml_event_t{} + + /* No events after the end of the stream or error. */ + + if parser.stream_end_produced || parser.error != yaml_NO_ERROR || + parser.state == yaml_PARSE_END_STATE { + return true + } + + /* Generate the next event. */ + + return yaml_parser_state_machine(parser, event) +} + +/* + * Set parser error. + */ + +func yaml_parser_set_parser_error(parser *yaml_parser_t, + problem string, problem_mark YAML_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.problem = problem + parser.problem_mark = problem_mark + + return false +} + +func yaml_parser_set_parser_error_context(parser *yaml_parser_t, + context string, context_mark YAML_mark_t, + problem string, problem_mark YAML_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = problem_mark + + return false +} + +/* + * State dispatcher. + */ + +func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool { + switch parser.state { + case yaml_PARSE_STREAM_START_STATE: + return yaml_parser_parse_stream_start(parser, event) + + case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, true) + + case yaml_PARSE_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, false) + + case yaml_PARSE_DOCUMENT_CONTENT_STATE: + return yaml_parser_parse_document_content(parser, event) + + case yaml_PARSE_DOCUMENT_END_STATE: + return yaml_parser_parse_document_end(parser, event) + + case yaml_PARSE_BLOCK_NODE_STATE: + return yaml_parser_parse_node(parser, event, true, false) + + case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE: + return yaml_parser_parse_node(parser, event, true, true) + + case yaml_PARSE_FLOW_NODE_STATE: + return yaml_parser_parse_node(parser, event, false, false) + + case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, true) + + case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, false) + + case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_indentless_sequence_entry(parser, event) + + case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, true) + + case yaml_PARSE_BLOCK_MAPPING_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, false) + + case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE: + return yaml_parser_parse_block_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, true) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, false) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_key(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_end(parser, event) + + case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, true) + + case yaml_PARSE_FLOW_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, true) + } + + panic("invalid parser state") +} + +/* + * Parse the production: + * stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + * ************ + */ + +func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type != yaml_STREAM_START_TOKEN { + return yaml_parser_set_parser_error(parser, + "did not find expected ", token.start_mark) + } + + parser.state = yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE + *event = yaml_event_t{ + event_type: yaml_STREAM_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + encoding: token.encoding, + } + skip_token(parser) + + return true +} + +/* + * Parse the productions: + * implicit_document ::= block_node DOCUMENT-END* + * * + * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + * ************************* + */ + +func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, + implicit bool) bool { + + token := peek_token(parser) + if token == nil { + return false + } + + /* Parse extra document end indicators. */ + + if !implicit { + for token.token_type == yaml_DOCUMENT_END_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } + + /* Parse an implicit document. */ + + if implicit && token.token_type != yaml_VERSION_DIRECTIVE_TOKEN && + token.token_type != yaml_TAG_DIRECTIVE_TOKEN && + token.token_type != yaml_DOCUMENT_START_TOKEN && + token.token_type != yaml_STREAM_END_TOKEN { + if !yaml_parser_process_directives(parser, nil, nil) { + return false + } + + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_BLOCK_NODE_STATE + + *event = yaml_event_t{ + event_type: yaml_DOCUMENT_START_EVENT, + implicit: true, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + } else if token.token_type != yaml_STREAM_END_TOKEN { + /* Parse an explicit document. */ + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + + start_mark := token.start_mark + if !yaml_parser_process_directives(parser, &version_directive, + &tag_directives) { + return false + } + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_DOCUMENT_START_TOKEN { + yaml_parser_set_parser_error(parser, + "did not find expected ", token.start_mark) + return false + } + + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_DOCUMENT_CONTENT_STATE + + end_mark := token.end_mark + + *event = yaml_event_t{ + event_type: yaml_DOCUMENT_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: false, + } + skip_token(parser) + } else { + /* Parse the stream end. */ + parser.state = yaml_PARSE_END_STATE + + *event = yaml_event_t{ + event_type: yaml_STREAM_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + skip_token(parser) + } + return true +} + +/* + * Parse the productions: + * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + * *********** + */ + +func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_VERSION_DIRECTIVE_TOKEN || + token.token_type == yaml_TAG_DIRECTIVE_TOKEN || + token.token_type == yaml_DOCUMENT_START_TOKEN || + token.token_type == yaml_DOCUMENT_END_TOKEN || + token.token_type == yaml_STREAM_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + return yaml_parser_process_empty_scalar(parser, event, + token.start_mark) + } else { + return yaml_parser_parse_node(parser, event, true, false) + } +} + +/* + * Parse the productions: + * implicit_document ::= block_node DOCUMENT-END* + * ************* + * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + * ************* + */ + +func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool { + implicit := true + + token := peek_token(parser) + if token == nil { + return false + } + + start_mark, end_mark := token.start_mark, token.start_mark + + if token.token_type == yaml_DOCUMENT_END_TOKEN { + end_mark = token.end_mark + skip_token(parser) + implicit = false + } + + parser.tag_directives = parser.tag_directives[:0] + + parser.state = yaml_PARSE_DOCUMENT_START_STATE + *event = yaml_event_t{ + event_type: yaml_DOCUMENT_END_EVENT, + start_mark: start_mark, + end_mark: end_mark, + implicit: implicit, + } + + return true +} + +/* + * Parse the productions: + * block_node_or_indentless_sequence ::= + * ALIAS + * ***** + * | properties (block_content | indentless_block_sequence)? + * ********** * + * | block_content | indentless_block_sequence + * * + * block_node ::= ALIAS + * ***** + * | properties block_content? + * ********** * + * | block_content + * * + * flow_node ::= ALIAS + * ***** + * | properties flow_content? + * ********** * + * | flow_content + * * + * properties ::= TAG ANCHOR? | ANCHOR TAG? + * ************************* + * block_content ::= block_collection | flow_collection | SCALAR + * ****** + * flow_content ::= flow_collection | SCALAR + * ****** + */ + +func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, + block bool, indentless_sequence bool) bool { + + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_ALIAS_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + event_type: yaml_ALIAS_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + anchor: token.value, + } + skip_token(parser) + return true + } else { + start_mark, end_mark := token.start_mark, token.start_mark + + var tag_handle []byte + var tag_suffix, anchor []byte + var tag_mark YAML_mark_t + if token.token_type == yaml_ANCHOR_TOKEN { + anchor = token.value + start_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type == yaml_TAG_TOKEN { + tag_handle = token.value + tag_suffix = token.suffix + tag_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } else if token.token_type == yaml_TAG_TOKEN { + tag_handle = token.value + tag_suffix = token.suffix + start_mark, tag_mark = token.start_mark, token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type == yaml_ANCHOR_TOKEN { + anchor = token.value + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + + } + } + + var tag []byte + if tag_handle != nil { + if len(tag_handle) == 0 { + tag = tag_suffix + tag_handle = nil + tag_suffix = nil + } else { + for i := range parser.tag_directives { + tag_directive := &parser.tag_directives[i] + if bytes.Equal(tag_directive.handle, tag_handle) { + tag = append([]byte(nil), tag_directive.prefix...) + tag = append(tag, tag_suffix...) + tag_handle = nil + tag_suffix = nil + break + } + } + if len(tag) == 0 { + yaml_parser_set_parser_error_context(parser, + "while parsing a node", start_mark, + "found undefined tag handle", tag_mark) + return false + } + } + } + + implicit := len(tag) == 0 + if indentless_sequence && token.token_type == yaml_BLOCK_ENTRY_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + + return true + } else { + if token.token_type == yaml_SCALAR_TOKEN { + plain_implicit := false + quoted_implicit := false + end_mark = token.end_mark + if (token.style == yaml_PLAIN_SCALAR_STYLE && len(tag) == 0) || + (len(tag) == 1 && tag[0] == '!') { + plain_implicit = true + } else if len(tag) == 0 { + quoted_implicit = true + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + event_type: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + value: token.value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(token.style), + } + + skip_token(parser) + return true + } else if token.token_type == yaml_FLOW_SEQUENCE_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_SEQUENCE_STYLE), + } + + return true + } else if token.token_type == yaml_FLOW_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE + + *event = yaml_event_t{ + event_type: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + + return true + } else if block && token.token_type == yaml_BLOCK_SEQUENCE_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + + return true + } else if block && token.token_type == yaml_BLOCK_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE + + *event = yaml_event_t{ + event_type: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_MAPPING_STYLE), + } + return true + } else if len(anchor) > 0 || len(tag) > 0 { + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + event_type: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + quoted_implicit: false, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + return true + } else { + msg := "while parsing a block node" + if !block { + msg = "while parsing a flow node" + } + yaml_parser_set_parser_error_context(parser, msg, start_mark, + "did not find expected node content", token.start_mark) + return false + } + } + } + + return false +} + +/* + * Parse the productions: + * block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + * ******************** *********** * ********* + */ + +func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, + event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_BLOCK_ENTRY_TOKEN && + token.token_type != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } else { + parser.state = yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else if token.token_type == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + + skip_token(parser) + return true + } else { + mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + return yaml_parser_set_parser_error_context(parser, + "while parsing a block collection", mark, + "did not find expected '-' indicator", token.start_mark) + } +} + +/* + * Parse the productions: + * indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + * *********** * + */ + +func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, + event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_BLOCK_ENTRY_TOKEN && + token.token_type != yaml_KEY_TOKEN && + token.token_type != yaml_VALUE_TOKEN && + token.token_type != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } else { + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, + } + return true + } +} + +/* + * Parse the productions: + * block_mapping ::= BLOCK-MAPPING_START + * ******************* + * ((KEY block_node_or_indentless_sequence?)? + * *** * + * (VALUE block_node_or_indentless_sequence?)?)* + * + * BLOCK-END + * ********* + */ + +func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, + event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_KEY_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_KEY_TOKEN && + token.token_type != yaml_VALUE_TOKEN && + token.token_type != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } else { + parser.state = yaml_PARSE_BLOCK_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else if token.token_type == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + event_type: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + skip_token(parser) + return true + } else { + mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + return yaml_parser_set_parser_error_context(parser, + "while parsing a block mapping", mark, + "did not find expected key", token.start_mark) + } +} + +/* + * Parse the productions: + * block_mapping ::= BLOCK-MAPPING_START + * + * ((KEY block_node_or_indentless_sequence?)? + * + * (VALUE block_node_or_indentless_sequence?)?)* + * ***** * + * BLOCK-END + * + */ + +func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, + event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_VALUE_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_KEY_TOKEN && + token.token_type != yaml_VALUE_TOKEN && + token.token_type != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } else { + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else { + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) + } +} + +/* + * Parse the productions: + * flow_sequence ::= FLOW-SEQUENCE-START + * ******************* + * (flow_sequence_entry FLOW-ENTRY)* + * * ********** + * flow_sequence_entry? + * * + * FLOW-SEQUENCE-END + * ***************** + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * * + */ + +func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, + event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type != yaml_FLOW_SEQUENCE_END_TOKEN { + if !first { + if token.token_type == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow sequence", mark, + "did not find expected ',' or ']'", token.start_mark) + } + } + + if token.token_type == yaml_KEY_TOKEN { + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE + *event = yaml_event_t{ + event_type: yaml_MAPPING_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + implicit: true, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + + skip_token(parser) + return true + } else if token.token_type != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + event_type: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + + skip_token(parser) + return true +} + +/* + * Parse the productions: + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * *** * + */ + +func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, + event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type != yaml_VALUE_TOKEN && + token.token_type != yaml_FLOW_ENTRY_TOKEN && + token.token_type != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } else { + mark := token.end_mark + skip_token(parser) + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } +} + +/* + * Parse the productions: + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * ***** * + */ + +func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, + event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type == yaml_VALUE_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_FLOW_ENTRY_TOKEN && + token.token_type != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +/* + * Parse the productions: + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * * + */ + +func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, + event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE + *event = yaml_event_t{ + event_type: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, + } + + return true +} + +/* + * Parse the productions: + * flow_mapping ::= FLOW-MAPPING-START + * ****************** + * (flow_mapping_entry FLOW-ENTRY)* + * * ********** + * flow_mapping_entry? + * ****************** + * FLOW-MAPPING-END + * **************** + * flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * * *** * + */ + +func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, + event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.token_type != yaml_FLOW_MAPPING_END_TOKEN { + if !first { + if token.token_type == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow mapping", mark, + "did not find expected ',' or '}'", token.start_mark) + } + } + + if token.token_type == yaml_KEY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_VALUE_TOKEN && + token.token_type != yaml_FLOW_ENTRY_TOKEN && + token.token_type != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } else { + parser.state = yaml_PARSE_FLOW_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, + token.start_mark) + } + } else if token.token_type != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + event_type: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + + skip_token(parser) + return true +} + +/* + * Parse the productions: + * flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * * ***** * + */ + +func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, + event *yaml_event_t, empty bool) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if empty { + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, + token.start_mark) + } + + if token.token_type == yaml_VALUE_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.token_type != yaml_FLOW_ENTRY_TOKEN && + token.token_type != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +/* + * Generate an empty scalar event. + */ + +func yaml_parser_process_empty_scalar(parser *yaml_parser_t, event *yaml_event_t, + mark YAML_mark_t) bool { + *event = yaml_event_t{ + event_type: yaml_SCALAR_EVENT, + start_mark: mark, + end_mark: mark, + value: nil, + implicit: true, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + + return true +} + +/* + * Parse directives. + */ + +func yaml_parser_process_directives(parser *yaml_parser_t, + version_directive_ref **yaml_version_directive_t, + tag_directives_ref *[]yaml_tag_directive_t) bool { + + token := peek_token(parser) + if token == nil { + return false + } + + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + + for token.token_type == yaml_VERSION_DIRECTIVE_TOKEN || + token.token_type == yaml_TAG_DIRECTIVE_TOKEN { + if token.token_type == yaml_VERSION_DIRECTIVE_TOKEN { + if version_directive != nil { + yaml_parser_set_parser_error(parser, + "found duplicate %YAML directive", token.start_mark) + return false + } + if token.major != 1 || + token.minor != 1 { + yaml_parser_set_parser_error(parser, + "found incompatible YAML document", token.start_mark) + return false + } + version_directive = &yaml_version_directive_t{ + major: token.major, + minor: token.minor, + } + } else if token.token_type == yaml_TAG_DIRECTIVE_TOKEN { + value := yaml_tag_directive_t{ + handle: token.value, + prefix: token.prefix, + } + + if !yaml_parser_append_tag_directive(parser, value, false, + token.start_mark) { + return false + } + tag_directives = append(tag_directives, value) + } + + skip_token(parser) + token := peek_token(parser) + if token == nil { + return false + } + } + + for i := range default_tag_directives { + if !yaml_parser_append_tag_directive(parser, default_tag_directives[i], true, token.start_mark) { + return false + } + } + + if version_directive_ref != nil { + *version_directive_ref = version_directive + } + if tag_directives_ref != nil { + *tag_directives_ref = tag_directives + } + + return true +} + +/* + * Append a tag directive to the directives stack. + */ + +func yaml_parser_append_tag_directive(parser *yaml_parser_t, + value yaml_tag_directive_t, allow_duplicates bool, mark YAML_mark_t) bool { + for i := range parser.tag_directives { + tag := &parser.tag_directives[i] + if bytes.Equal(value.handle, tag.handle) { + if allow_duplicates { + return true + } + return yaml_parser_set_parser_error(parser, "found duplicate %TAG directive", mark) + } + } + + parser.tag_directives = append(parser.tag_directives, value) + return true +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser_test.go new file mode 100644 index 00000000000..fa19a78c092 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/parser_test.go @@ -0,0 +1,81 @@ +/* +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 candiedyaml + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var parses = func(filename string) { + It("parses "+filename, func() { + file, err := os.Open(filename) + Ω(err).To(BeNil()) + + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_reader(&parser, file) + + failed := false + event := yaml_event_t{} + + for { + if !yaml_parser_parse(&parser, &event) { + failed = true + println("---", parser.error, parser.problem, parser.context, "line", parser.problem_mark.line, "col", parser.problem_mark.column) + break + } + + if event.event_type == yaml_STREAM_END_EVENT { + break + } + } + + file.Close() + + // msg := "SUCCESS" + // if failed { + // msg = "FAILED" + // if parser.error != yaml_NO_ERROR { + // m := parser.problem_mark + // fmt.Printf("ERROR: (%s) %s @ line: %d col: %d\n", + // parser.context, parser.problem, m.line, m.column) + // } + // } + Ω(failed).To(BeFalse()) + }) +} + +var parseYamls = func(dirname string) { + fileInfos, err := ioutil.ReadDir(dirname) + if err != nil { + panic(err.Error()) + } + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + parses(filepath.Join(dirname, fileInfo.Name())) + } + } +} + +var _ = Describe("Parser", func() { + parseYamls("fixtures/specification") + parseYamls("fixtures/specification/types") +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader.go new file mode 100644 index 00000000000..5631da2dcc0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader.go @@ -0,0 +1,465 @@ +/* +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 candiedyaml + +import ( + "io" +) + +/* + * Set the reader error and return 0. + */ + +func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, + offset int, value int) bool { + parser.error = yaml_READER_ERROR + parser.problem = problem + parser.problem_offset = offset + parser.problem_value = value + + return false +} + +/* + * Byte order marks. + */ +const ( + BOM_UTF8 = "\xef\xbb\xbf" + BOM_UTF16LE = "\xff\xfe" + BOM_UTF16BE = "\xfe\xff" +) + +/* + * Determine the input stream encoding by checking the BOM symbol. If no BOM is + * found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure. + */ + +func yaml_parser_determine_encoding(parser *yaml_parser_t) bool { + /* Ensure that we had enough bytes in the raw buffer. */ + for !parser.eof && + len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { + if !yaml_parser_update_raw_buffer(parser) { + return false + } + } + + /* Determine the encoding. */ + raw := parser.raw_buffer + pos := parser.raw_buffer_pos + remaining := len(raw) - pos + if remaining >= 2 && + raw[pos] == BOM_UTF16LE[0] && raw[pos+1] == BOM_UTF16LE[1] { + parser.encoding = yaml_UTF16LE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if remaining >= 2 && + raw[pos] == BOM_UTF16BE[0] && raw[pos+1] == BOM_UTF16BE[1] { + parser.encoding = yaml_UTF16BE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if remaining >= 3 && + raw[pos] == BOM_UTF8[0] && raw[pos+1] == BOM_UTF8[1] && raw[pos+2] == BOM_UTF8[2] { + parser.encoding = yaml_UTF8_ENCODING + parser.raw_buffer_pos += 3 + parser.offset += 3 + } else { + parser.encoding = yaml_UTF8_ENCODING + } + + return true +} + +/* + * Update the raw buffer. + */ + +func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool { + size_read := 0 + + /* Return if the raw buffer is full. */ + if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { + return true + } + + /* Return on EOF. */ + + if parser.eof { + return true + } + + /* Move the remaining bytes in the raw buffer to the beginning. */ + if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { + copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) + } + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] + parser.raw_buffer_pos = 0 + + /* Call the read handler to fill the buffer. */ + size_read, err := parser.read_handler(parser, + parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] + + if err == io.EOF { + parser.eof = true + } else if err != nil { + return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), + parser.offset, -1) + } + + return true +} + +/* + * Ensure that the buffer contains at least `length` characters. + * Return 1 on success, 0 on failure. + * + * The length is supposed to be significantly less that the buffer size. + */ + +func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool { + /* Read handler must be set. */ + if parser.read_handler == nil { + panic("read handler must be set") + } + + /* If the EOF flag is set and the raw buffer is empty, do nothing. */ + + if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) { + return true + } + + /* Return if the buffer contains enough characters. */ + + if parser.unread >= length { + return true + } + + /* Determine the input encoding if it is not known yet. */ + + if parser.encoding == yaml_ANY_ENCODING { + if !yaml_parser_determine_encoding(parser) { + return false + } + } + + /* Move the unread characters to the beginning of the buffer. */ + buffer_end := len(parser.buffer) + if 0 < parser.buffer_pos && + parser.buffer_pos < buffer_end { + copy(parser.buffer, parser.buffer[parser.buffer_pos:]) + buffer_end -= parser.buffer_pos + parser.buffer_pos = 0 + } else if parser.buffer_pos == buffer_end { + buffer_end = 0 + parser.buffer_pos = 0 + } + + parser.buffer = parser.buffer[:cap(parser.buffer)] + + /* Fill the buffer until it has enough characters. */ + first := true + for parser.unread < length { + /* Fill the raw buffer if necessary. */ + + if !first || parser.raw_buffer_pos == len(parser.raw_buffer) { + if !yaml_parser_update_raw_buffer(parser) { + parser.buffer = parser.buffer[:buffer_end] + return false + } + } + first = false + + /* Decode the raw buffer. */ + for parser.raw_buffer_pos != len(parser.raw_buffer) { + var value rune + var w int + + raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos + incomplete := false + + /* Decode the next character. */ + + switch parser.encoding { + case yaml_UTF8_ENCODING: + + /* + * Decode a UTF-8 character. Check RFC 3629 + * (http://www.ietf.org/rfc/rfc3629.txt) for more details. + * + * The following table (taken from the RFC) is used for + * decoding. + * + * Char. number range | UTF-8 octet sequence + * (hexadecimal) | (binary) + * --------------------+------------------------------------ + * 0000 0000-0000 007F | 0xxxxxxx + * 0000 0080-0000 07FF | 110xxxxx 10xxxxxx + * 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx + * 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + * + * Additionally, the characters in the range 0xD800-0xDFFF + * are prohibited as they are reserved for use with UTF-16 + * surrogate pairs. + */ + + /* Determine the length of the UTF-8 sequence. */ + + octet := parser.raw_buffer[parser.raw_buffer_pos] + w = width(octet) + + /* Check if the leading octet is valid. */ + + if w == 0 { + return yaml_parser_set_reader_error(parser, + "invalid leading UTF-8 octet", + parser.offset, int(octet)) + } + + /* Check if the raw buffer contains an incomplete character. */ + + if w > raw_unread { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-8 octet sequence", + parser.offset, -1) + } + incomplete = true + break + } + + /* Decode the leading octet. */ + switch { + case octet&0x80 == 0x00: + value = rune(octet & 0x7F) + case octet&0xE0 == 0xC0: + value = rune(octet & 0x1F) + case octet&0xF0 == 0xE0: + value = rune(octet & 0x0F) + case octet&0xF8 == 0xF0: + value = rune(octet & 0x07) + default: + value = 0 + } + + /* Check and decode the trailing octets. */ + + for k := 1; k < w; k++ { + octet = parser.raw_buffer[parser.raw_buffer_pos+k] + + /* Check if the octet is valid. */ + + if (octet & 0xC0) != 0x80 { + return yaml_parser_set_reader_error(parser, + "invalid trailing UTF-8 octet", + parser.offset+k, int(octet)) + } + + /* Decode the octet. */ + + value = (value << 6) + rune(octet&0x3F) + } + + /* Check the length of the sequence against the value. */ + switch { + case w == 1: + case w == 2 && value >= 0x80: + case w == 3 && value >= 0x800: + case w == 4 && value >= 0x10000: + default: + return yaml_parser_set_reader_error(parser, + "invalid length of a UTF-8 sequence", + parser.offset, -1) + } + + /* Check the range of the value. */ + + if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF { + return yaml_parser_set_reader_error(parser, + "invalid Unicode character", + parser.offset, int(value)) + } + case yaml_UTF16LE_ENCODING, + yaml_UTF16BE_ENCODING: + + var low, high int + if parser.encoding == yaml_UTF16LE_ENCODING { + low, high = 0, 1 + } else { + high, low = 1, 0 + } + + /* + * The UTF-16 encoding is not as simple as one might + * naively think. Check RFC 2781 + * (http://www.ietf.org/rfc/rfc2781.txt). + * + * Normally, two subsequent bytes describe a Unicode + * character. However a special technique (called a + * surrogate pair) is used for specifying character + * values larger than 0xFFFF. + * + * A surrogate pair consists of two pseudo-characters: + * high surrogate area (0xD800-0xDBFF) + * low surrogate area (0xDC00-0xDFFF) + * + * The following formulas are used for decoding + * and encoding characters using surrogate pairs: + * + * U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF) + * U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF) + * W1 = 110110yyyyyyyyyy + * W2 = 110111xxxxxxxxxx + * + * where U is the character value, W1 is the high surrogate + * area, W2 is the low surrogate area. + */ + + /* Check for incomplete UTF-16 character. */ + + if raw_unread < 2 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 character", + parser.offset, -1) + } + incomplete = true + break + } + + /* Get the character. */ + value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8) + + /* Check for unexpected low surrogate area. */ + + if (value & 0xFC00) == 0xDC00 { + return yaml_parser_set_reader_error(parser, + "unexpected low surrogate area", + parser.offset, int(value)) + } + + /* Check for a high surrogate area. */ + + if (value & 0xFC00) == 0xD800 { + + w = 4 + + /* Check for incomplete surrogate pair. */ + + if raw_unread < 4 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 surrogate pair", + parser.offset, -1) + } + incomplete = true + break + } + + /* Get the next character. */ + + value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8) + + /* Check for a low surrogate area. */ + + if (value2 & 0xFC00) != 0xDC00 { + return yaml_parser_set_reader_error(parser, + "expected low surrogate area", + parser.offset+2, int(value2)) + } + + /* Generate the value of the surrogate pair. */ + + value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF) + } else { + w = 2 + } + + break + + default: + panic("Impossible") /* Impossible. */ + } + + /* Check if the raw buffer contains enough bytes to form a character. */ + + if incomplete { + break + } + + /* + * Check if the character is in the allowed range: + * #x9 | #xA | #xD | [#x20-#x7E] (8 bit) + * | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit) + * | [#x10000-#x10FFFF] (32 bit) + */ + + if !(value == 0x09 || value == 0x0A || value == 0x0D || + (value >= 0x20 && value <= 0x7E) || + (value == 0x85) || (value >= 0xA0 && value <= 0xD7FF) || + (value >= 0xE000 && value <= 0xFFFD) || + (value >= 0x10000 && value <= 0x10FFFF)) { + return yaml_parser_set_reader_error(parser, + "control characters are not allowed", + parser.offset, int(value)) + } + + /* Move the raw pointers. */ + + parser.raw_buffer_pos += w + parser.offset += w + + /* Finally put the character into the buffer. */ + + /* 0000 0000-0000 007F . 0xxxxxxx */ + if value <= 0x7F { + parser.buffer[buffer_end] = byte(value) + } else if value <= 0x7FF { + /* 0000 0080-0000 07FF . 110xxxxx 10xxxxxx */ + parser.buffer[buffer_end] = byte(0xC0 + (value >> 6)) + parser.buffer[buffer_end+1] = byte(0x80 + (value & 0x3F)) + } else if value <= 0xFFFF { + /* 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx */ + parser.buffer[buffer_end] = byte(0xE0 + (value >> 12)) + parser.buffer[buffer_end+1] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_end+2] = byte(0x80 + (value & 0x3F)) + } else { + /* 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + parser.buffer[buffer_end] = byte(0xF0 + (value >> 18)) + parser.buffer[buffer_end+1] = byte(0x80 + ((value >> 12) & 0x3F)) + parser.buffer[buffer_end+2] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_end+3] = byte(0x80 + (value & 0x3F)) + } + + buffer_end += w + parser.unread++ + } + + /* On EOF, put NUL into the buffer and return. */ + + if parser.eof { + parser.buffer[buffer_end] = 0 + buffer_end++ + parser.buffer = parser.buffer[:buffer_end] + parser.unread++ + return true + } + + } + + parser.buffer = parser.buffer[:buffer_end] + return true +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader_test.go new file mode 100644 index 00000000000..852b5c81d24 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/reader_test.go @@ -0,0 +1,291 @@ +/* +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 candiedyaml + +import ( + // "fmt" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +/* + * Test cases are stolen from + * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + */ + +type test_case struct { + title string + test string + result bool +} + +var _ = Describe("Reader", func() { + LONG := 100000 + + Context("UTF8 Sequences", func() { + utf8_sequences := []test_case{ + /* {"title", "test 1|test 2|...|test N!", (0 or 1)}, */ + + {"a simple test", "'test' is '\xd0\xbf\xd1\x80\xd0\xbe\xd0\xb2\xd0\xb5\xd1\x80\xd0\xba\xd0\xb0' in Russian!", true}, + + {"an empty line", "!", true}, + {"u-0 is a control character", "\x00!", false}, + {"u-80 is a control character", "\xc2\x80!", false}, + {"u-800 is valid", "\xe0\xa0\x80!", true}, + {"u-10000 is valid", "\xf0\x90\x80\x80!", true}, + {"5 bytes sequences are not allowed", "\xf8\x88\x80\x80\x80!", false}, + {"6 bytes sequences are not allowed", "\xfc\x84\x80\x80\x80\x80!", false}, + + {"u-7f is a control character", "\x7f!", false}, + {"u-7FF is valid", "\xdf\xbf!", true}, + {"u-FFFF is a control character", "\xef\xbf\xbf!", false}, + {"u-1FFFFF is too large", "\xf7\xbf\xbf\xbf!", false}, + {"u-3FFFFFF is 5 bytes", "\xfb\xbf\xbf\xbf\xbf!", false}, + {"u-7FFFFFFF is 6 bytes", "\xfd\xbf\xbf\xbf\xbf\xbf!", false}, + + {"u-D7FF", "\xed\x9f\xbf!", true}, + {"u-E000", "\xee\x80\x80!", true}, + {"u-FFFD", "\xef\xbf\xbd!", true}, + {"u-10FFFF", "\xf4\x8f\xbf\xbf!", true}, + {"u-110000", "\xf4\x90\x80\x80!", false}, + + {"first continuation byte", "\x80!", false}, + {"last continuation byte", "\xbf!", false}, + + {"2 continuation bytes", "\x80\xbf!", false}, + {"3 continuation bytes", "\x80\xbf\x80!", false}, + {"4 continuation bytes", "\x80\xbf\x80\xbf!", false}, + {"5 continuation bytes", "\x80\xbf\x80\xbf\x80!", false}, + {"6 continuation bytes", "\x80\xbf\x80\xbf\x80\xbf!", false}, + {"7 continuation bytes", "\x80\xbf\x80\xbf\x80\xbf\x80!", false}, + + {"sequence of all 64 possible continuation bytes", + "\x80|\x81|\x82|\x83|\x84|\x85|\x86|\x87|\x88|\x89|\x8a|\x8b|\x8c|\x8d|\x8e|\x8f|" + + "\x90|\x91|\x92|\x93|\x94|\x95|\x96|\x97|\x98|\x99|\x9a|\x9b|\x9c|\x9d|\x9e|\x9f|" + + "\xa0|\xa1|\xa2|\xa3|\xa4|\xa5|\xa6|\xa7|\xa8|\xa9|\xaa|\xab|\xac|\xad|\xae|\xaf|" + + "\xb0|\xb1|\xb2|\xb3|\xb4|\xb5|\xb6|\xb7|\xb8|\xb9|\xba|\xbb|\xbc|\xbd|\xbe|\xbf!", false}, + {"32 first bytes of 2-byte sequences {0xc0-0xdf}", + "\xc0 |\xc1 |\xc2 |\xc3 |\xc4 |\xc5 |\xc6 |\xc7 |\xc8 |\xc9 |\xca |\xcb |\xcc |\xcd |\xce |\xcf |" + + "\xd0 |\xd1 |\xd2 |\xd3 |\xd4 |\xd5 |\xd6 |\xd7 |\xd8 |\xd9 |\xda |\xdb |\xdc |\xdd |\xde |\xdf !", false}, + {"16 first bytes of 3-byte sequences {0xe0-0xef}", + "\xe0 |\xe1 |\xe2 |\xe3 |\xe4 |\xe5 |\xe6 |\xe7 |\xe8 |\xe9 |\xea |\xeb |\xec |\xed |\xee |\xef !", false}, + {"8 first bytes of 4-byte sequences {0xf0-0xf7}", "\xf0 |\xf1 |\xf2 |\xf3 |\xf4 |\xf5 |\xf6 |\xf7 !", false}, + {"4 first bytes of 5-byte sequences {0xf8-0xfb}", "\xf8 |\xf9 |\xfa |\xfb !", false}, + {"2 first bytes of 6-byte sequences {0xfc-0xfd}", "\xfc |\xfd !", false}, + + {"sequences with last byte missing {u-0}", + "\xc0|\xe0\x80|\xf0\x80\x80|\xf8\x80\x80\x80|\xfc\x80\x80\x80\x80!", false}, + {"sequences with last byte missing {u-...FF}", + "\xdf|\xef\xbf|\xf7\xbf\xbf|\xfb\xbf\xbf\xbf|\xfd\xbf\xbf\xbf\xbf!", false}, + + {"impossible bytes", "\xfe|\xff|\xfe\xfe\xff\xff!", false}, + + {"overlong sequences {u-2f}", + "\xc0\xaf|\xe0\x80\xaf|\xf0\x80\x80\xaf|\xf8\x80\x80\x80\xaf|\xfc\x80\x80\x80\x80\xaf!", false}, + + {"maximum overlong sequences", + "\xc1\xbf|\xe0\x9f\xbf|\xf0\x8f\xbf\xbf|\xf8\x87\xbf\xbf\xbf|\xfc\x83\xbf\xbf\xbf\xbf!", false}, + + {"overlong representation of the NUL character", + "\xc0\x80|\xe0\x80\x80|\xf0\x80\x80\x80|\xf8\x80\x80\x80\x80|\xfc\x80\x80\x80\x80\x80!", false}, + + {"single UTF-16 surrogates", + "\xed\xa0\x80|\xed\xad\xbf|\xed\xae\x80|\xed\xaf\xbf|\xed\xb0\x80|\xed\xbe\x80|\xed\xbf\xbf!", false}, + + {"paired UTF-16 surrogates", + "\xed\xa0\x80\xed\xb0\x80|\xed\xa0\x80\xed\xbf\xbf|\xed\xad\xbf\xed\xb0\x80|" + + "\xed\xad\xbf\xed\xbf\xbf|\xed\xae\x80\xed\xb0\x80|\xed\xae\x80\xed\xbf\xbf|" + + "\xed\xaf\xbf\xed\xb0\x80|\xed\xaf\xbf\xed\xbf\xbf!", false}, + + {"other illegal code positions", "\xef\xbf\xbe|\xef\xbf\xbf!", false}, + } + + check_sequence := func(tc test_case) { + It(tc.title, func() { + start := 0 + end := start + bytes := []byte(tc.test) + + for { + for bytes[end] != '|' && bytes[end] != '!' { + end++ + } + + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_string(&parser, bytes) + result := yaml_parser_update_buffer(&parser, end-start) + Ω(result).To(Equal(tc.result)) + // outcome := '+' + // if result != tc.result { + // outcome = '-' + // } + // fmt.Printf("\t\t %c %s", outcome, tc.title) + // if parser.error == yaml_NO_ERROR { + // fmt.Printf("(no error)\n") + // } else if parser.error == yaml_READER_ERROR { + // if parser.problem_value != -1 { + // fmt.Printf("(reader error: %s: #%X at %d)\n", + // parser.problem, parser.problem_value, parser.problem_offset) + // } else { + // fmt.Printf("(reader error: %s: at %d)\n", + // parser.problem, parser.problem_offset) + // } + // } + + if bytes[end] == '!' { + break + } + + end++ + start = end + yaml_parser_delete(&parser) + } + }) + } + + for _, test := range utf8_sequences { + check_sequence(test) + } + }) + + Context("BOMs", func() { + boms := []test_case{ + /* {"title", "test!", lenth}, */ + {"no bom (utf-8)", "Hi is \xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!", true}, + {"bom (utf-8)", "\xef\xbb\xbfHi is \xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!", true}, + {"bom (utf-16-le)", "\xff\xfeH\x00i\x00 \x00i\x00s\x00 \x00\x1f\x04@\x04" + "8\x04" + "2\x04" + "5\x04" + "B\x04!", true}, + {"bom (utf-16-be)", "\xfe\xff\x00H\x00i\x00 \x00i\x00s\x00 \x04\x1f\x04@\x04" + "8\x04" + "2\x04" + "5\x04" + "B!", true}, + } + + check_bom := func(tc test_case) { + It(tc.title, func() { + start := 0 + end := start + bytes := []byte(tc.test) + + for bytes[end] != '!' { + end++ + } + + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_string(&parser, bytes[:end-start]) + result := yaml_parser_update_buffer(&parser, end-start) + Ω(result).To(Equal(tc.result)) + yaml_parser_delete(&parser) + }) + } + + for _, test := range boms { + check_bom(test) + } + + }) + + Context("Long UTF8", func() { + It("parses properly", func() { + buffer := make([]byte, 0, 3+LONG*2) + buffer = append(buffer, '\xef', '\xbb', '\xbf') + for j := 0; j < LONG; j++ { + if j%2 == 1 { + buffer = append(buffer, '\xd0', '\x90') + } else { + buffer = append(buffer, '\xd0', '\xaf') + } + } + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_string(&parser, buffer) + + for k := 0; k < LONG; k++ { + if parser.unread == 0 { + updated := yaml_parser_update_buffer(&parser, 1) + Ω(updated).To(BeTrue()) + // printf("\treader error: %s at %d\n", parser.problem, parser.problem_offset); + } + Ω(parser.unread).ToNot(Equal(0)) + // printf("\tnot enough characters at %d\n", k); + var ch0, ch1 byte + if k%2 == 1 { + ch0 = '\xd0' + ch1 = '\x90' + } else { + ch0 = '\xd0' + ch1 = '\xaf' + } + Ω(parser.buffer[parser.buffer_pos]).To(Equal(ch0)) + Ω(parser.buffer[parser.buffer_pos+1]).To(Equal(ch1)) + // printf("\tincorrect UTF-8 sequence: %X %X instead of %X %X\n", + // (int)parser.buffer.pointer[0], (int)parser.buffer.pointer[1], + // (int)ch0, (int)ch1); + + parser.buffer_pos += 2 + parser.unread -= 1 + } + updated := yaml_parser_update_buffer(&parser, 1) + Ω(updated).To(BeTrue()) + // printf("\treader error: %s at %d\n", parser.problem, parser.problem_offset); + yaml_parser_delete(&parser) + }) + }) + + Context("Long UTF16", func() { + It("parses properly", func() { + buffer := make([]byte, 0, 2+LONG*2) + buffer = append(buffer, '\xff', '\xfe') + for j := 0; j < LONG; j++ { + if j%2 == 1 { + buffer = append(buffer, '\x10', '\x04') + } else { + buffer = append(buffer, '/', '\x04') + } + } + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_string(&parser, buffer) + + for k := 0; k < LONG; k++ { + if parser.unread == 0 { + updated := yaml_parser_update_buffer(&parser, 1) + Ω(updated).To(BeTrue()) + // printf("\treader error: %s at %d\n", parser.problem, parser.problem_offset); + } + Ω(parser.unread).ToNot(Equal(0)) + // printf("\tnot enough characters at %d\n", k); + var ch0, ch1 byte + if k%2 == 1 { + ch0 = '\xd0' + ch1 = '\x90' + } else { + ch0 = '\xd0' + ch1 = '\xaf' + } + Ω(parser.buffer[parser.buffer_pos]).To(Equal(ch0)) + Ω(parser.buffer[parser.buffer_pos+1]).To(Equal(ch1)) + // printf("\tincorrect UTF-8 sequence: %X %X instead of %X %X\n", + // (int)parser.buffer.pointer[0], (int)parser.buffer.pointer[1], + // (int)ch0, (int)ch1); + + parser.buffer_pos += 2 + parser.unread -= 1 + } + updated := yaml_parser_update_buffer(&parser, 1) + Ω(updated).To(BeTrue()) + // printf("\treader error: %s at %d\n", parser.problem, parser.problem_offset); + yaml_parser_delete(&parser) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver.go new file mode 100644 index 00000000000..28c9784eb6a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver.go @@ -0,0 +1,509 @@ +/* +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 candiedyaml + +import ( + "bytes" + "encoding/base64" + "errors" + "math" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +var byteSliceType = reflect.TypeOf([]byte(nil)) + +var binary_tags = [][]byte{[]byte("!binary"), []byte("tag:yaml.org,2002:binary")} +var bool_values map[string]bool +var null_values map[string]bool + +var signs = []byte{'-', '+'} +var nulls = []byte{'~', 'n', 'N'} +var bools = []byte{'t', 'T', 'f', 'F', 'y', 'Y', 'n', 'N', 'o', 'O'} + +var timestamp_regexp *regexp.Regexp +var ymd_regexp *regexp.Regexp + +func init() { + bool_values = make(map[string]bool) + bool_values["y"] = true + bool_values["yes"] = true + bool_values["n"] = false + bool_values["no"] = false + bool_values["true"] = true + bool_values["false"] = false + bool_values["on"] = true + bool_values["off"] = false + + null_values = make(map[string]bool) + null_values["~"] = true + null_values["null"] = true + null_values["Null"] = true + null_values["NULL"] = true + + timestamp_regexp = regexp.MustCompile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$") + ymd_regexp = regexp.MustCompile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$") +} + +func resolve(event yaml_event_t, v reflect.Value, useNumber bool) (string, error) { + val := string(event.value) + + if null_values[val] { + v.Set(reflect.Zero(v.Type())) + return "!!null", nil + } + + switch v.Kind() { + case reflect.String: + if useNumber && v.Type() == numberType { + tag, i := resolveInterface(event, useNumber) + if n, ok := i.(Number); ok { + v.Set(reflect.ValueOf(n)) + return tag, nil + } + return "", errors.New("Not a Number: " + reflect.TypeOf(i).String()) + } + + return resolve_string(val, v, event) + case reflect.Bool: + return resolve_bool(val, v) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return resolve_int(val, v, useNumber) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return resolve_uint(val, v, useNumber) + case reflect.Float32, reflect.Float64: + return resolve_float(val, v, useNumber) + case reflect.Interface: + _, i := resolveInterface(event, useNumber) + v.Set(reflect.ValueOf(i)) + case reflect.Struct: + return resolve_time(val, v) + case reflect.Slice: + if v.Type() != byteSliceType { + return "", errors.New("Cannot resolve into " + v.Type().String()) + } + b, err := decode_binary(event.value) + if err != nil { + return "", err + } + + v.Set(reflect.ValueOf(b)) + default: + return "", errors.New("Resolve failed for " + v.Kind().String()) + } + + return "!!str", nil +} + +func hasBinaryTag(event yaml_event_t) bool { + for _, tag := range binary_tags { + if bytes.Equal(event.tag, tag) { + return true + } + } + return false +} + +func decode_binary(value []byte) ([]byte, error) { + b := make([]byte, base64.StdEncoding.DecodedLen(len(value))) + n, err := base64.StdEncoding.Decode(b, value) + return b[:n], err +} + +func resolve_string(val string, v reflect.Value, event yaml_event_t) (string, error) { + if len(event.tag) > 0 { + if hasBinaryTag(event) { + b, err := decode_binary(event.value) + if err != nil { + return "", err + } + val = string(b) + } + } + v.SetString(val) + return "!!str", nil +} + +func resolve_bool(val string, v reflect.Value) (string, error) { + b, found := bool_values[strings.ToLower(val)] + if !found { + return "", errors.New("Invalid boolean: " + val) + } + + v.SetBool(b) + return "!!bool", nil +} + +func resolve_int(val string, v reflect.Value, useNumber bool) (string, error) { + original := val + val = strings.Replace(val, "_", "", -1) + var value uint64 + + isNumberValue := v.Type() == numberType + + sign := int64(1) + if val[0] == '-' { + sign = -1 + val = val[1:] + } else if val[0] == '+' { + val = val[1:] + } + + base := 0 + if val == "0" { + if isNumberValue { + v.SetString("0") + } else { + v.Set(reflect.Zero(v.Type())) + } + + return "!!int", nil + } + + var err error + if strings.Contains(val, ":") { + value, err = decode_int_base64(val) + if err != nil { + return "", errors.New("Integer: " + original) + } + } else { + if strings.HasPrefix(val, "0b") { + base = 2 + val = val[2:] + } + + value, err = strconv.ParseUint(val, base, 64) + if err != nil { + return "", errors.New("Integer: " + original) + } + } + + var val64 int64 + if value <= math.MaxInt64 { + val64 = int64(value) + if sign == -1 { + val64 = -val64 + } + } else if sign == -1 && value == uint64(math.MaxInt64)+1 { + val64 = math.MinInt64 + } else { + return "", errors.New("Integer: " + original) + } + + if isNumberValue { + v.SetString(strconv.FormatInt(val64, 10)) + } else { + if v.OverflowInt(val64) { + return "", errors.New("Integer: " + original) + } + v.SetInt(val64) + } + + return "!!int", nil +} + +func decode_int_base64(val string) (uint64, error) { + digits := strings.Split(val, ":") + + bes := uint64(1) + var value uint64 + for j := len(digits) - 1; j >= 0; j-- { + n, err := strconv.ParseUint(digits[j], 10, 64) + if err != nil { + return 0, err + } + + n *= bes + value += n + bes *= 60 + } + return value, nil +} + +func resolve_uint(val string, v reflect.Value, useNumber bool) (string, error) { + original := val + val = strings.Replace(val, "_", "", -1) + var value uint64 + + isNumberValue := v.Type() == numberType + + if val[0] == '-' { + return "", errors.New("Unsigned int with negative value: " + original) + } + + if val[0] == '+' { + val = val[1:] + } + + base := 10 + if val == "0" { + if isNumberValue { + v.SetString("0") + } else { + v.Set(reflect.Zero(v.Type())) + } + + return "!!int", nil + } + + if strings.HasPrefix(val, "0b") { + base = 2 + val = val[2:] + } else if strings.HasPrefix(val, "0x") { + base = 16 + val = val[2:] + } else if val[0] == '0' { + base = 8 + val = val[1:] + } else if strings.Contains(val, ":") { + digits := strings.Split(val, ":") + bes := uint64(1) + for j := len(digits) - 1; j >= 0; j-- { + n, err := strconv.ParseUint(digits[j], 10, 64) + n *= bes + if err != nil { + return "", errors.New("Unsigned Integer: " + original) + } + value += n + bes *= 60 + } + + if isNumberValue { + v.SetString(strconv.FormatUint(value, 10)) + } else { + if v.OverflowUint(value) { + return "", errors.New("Unsigned Integer: " + original) + } + + v.SetUint(value) + } + return "!!int", nil + } + + value, err := strconv.ParseUint(val, base, 64) + if err != nil { + return "", errors.New("Unsigned Integer: " + val) + } + + if isNumberValue { + v.SetString(strconv.FormatUint(value, 10)) + } else { + if v.OverflowUint(value) { + return "", errors.New("Unsigned Integer: " + val) + } + + v.SetUint(value) + } + + return "!!int", nil +} + +func resolve_float(val string, v reflect.Value, useNumber bool) (string, error) { + val = strings.Replace(val, "_", "", -1) + var value float64 + + isNumberValue := v.Type() == numberType + typeBits := 64 + if !isNumberValue { + typeBits = v.Type().Bits() + } + + sign := 1 + if val[0] == '-' { + sign = -1 + val = val[1:] + } else if val[0] == '+' { + val = val[1:] + } + + valLower := strings.ToLower(val) + if valLower == ".inf" { + value = math.Inf(sign) + } else if valLower == ".nan" { + value = math.NaN() + } else if strings.Contains(val, ":") { + digits := strings.Split(val, ":") + bes := float64(1) + for j := len(digits) - 1; j >= 0; j-- { + n, err := strconv.ParseFloat(digits[j], typeBits) + n *= bes + if err != nil { + return "", errors.New("Float: " + val) + } + value += n + bes *= 60 + } + value *= float64(sign) + } else { + var err error + value, err = strconv.ParseFloat(val, typeBits) + value *= float64(sign) + + if err != nil { + return "", errors.New("Float: " + val) + } + } + + if isNumberValue { + v.SetString(strconv.FormatFloat(value, 'g', -1, typeBits)) + } else { + if v.OverflowFloat(value) { + return "", errors.New("Float: " + val) + } + + v.SetFloat(value) + } + + return "!!float", nil +} + +func resolve_time(val string, v reflect.Value) (string, error) { + var parsedTime time.Time + matches := ymd_regexp.FindStringSubmatch(val) + if len(matches) > 0 { + year, _ := strconv.Atoi(matches[1]) + month, _ := strconv.Atoi(matches[2]) + day, _ := strconv.Atoi(matches[3]) + parsedTime = time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) + } else { + matches = timestamp_regexp.FindStringSubmatch(val) + if len(matches) == 0 { + return "", errors.New("Unexpected timestamp: " + val) + } + + year, _ := strconv.Atoi(matches[1]) + month, _ := strconv.Atoi(matches[2]) + day, _ := strconv.Atoi(matches[3]) + hour, _ := strconv.Atoi(matches[4]) + min, _ := strconv.Atoi(matches[5]) + sec, _ := strconv.Atoi(matches[6]) + + nsec := 0 + if matches[7] != "" { + millis, _ := strconv.Atoi(matches[7]) + nsec = int(time.Duration(millis) * time.Millisecond) + } + + loc := time.UTC + if matches[8] != "" { + sign := matches[8][0] + hr, _ := strconv.Atoi(matches[8][1:]) + min := 0 + if matches[9] != "" { + min, _ = strconv.Atoi(matches[9]) + } + + zoneOffset := (hr*60 + min) * 60 + if sign == '-' { + zoneOffset = -zoneOffset + } + + loc = time.FixedZone("", zoneOffset) + } + parsedTime = time.Date(year, time.Month(month), day, hour, min, sec, nsec, loc) + } + + v.Set(reflect.ValueOf(parsedTime)) + return "", nil +} + +func resolveInterface(event yaml_event_t, useNumber bool) (string, interface{}) { + val := string(event.value) + if len(event.tag) == 0 && !event.implicit { + return "", val + } + + if len(val) == 0 { + return "!!null", nil + } + + var result interface{} + + sign := false + c := val[0] + switch { + case bytes.IndexByte(signs, c) != -1: + sign = true + fallthrough + case c >= '0' && c <= '9': + i := int64(0) + result = &i + if useNumber { + var n Number + result = &n + } + + v := reflect.ValueOf(result).Elem() + if _, err := resolve_int(val, v, useNumber); err == nil { + return "!!int", v.Interface() + } + + f := float64(0) + result = &f + if useNumber { + var n Number + result = &n + } + + v = reflect.ValueOf(result).Elem() + if _, err := resolve_float(val, v, useNumber); err == nil { + return "!!float", v.Interface() + } + + if !sign { + t := time.Time{} + if _, err := resolve_time(val, reflect.ValueOf(&t).Elem()); err == nil { + return "", t + } + } + case bytes.IndexByte(nulls, c) != -1: + if null_values[val] { + return "!!null", nil + } + b := false + if _, err := resolve_bool(val, reflect.ValueOf(&b).Elem()); err == nil { + return "!!bool", b + } + case c == '.': + f := float64(0) + result = &f + if useNumber { + var n Number + result = &n + } + + v := reflect.ValueOf(result).Elem() + if _, err := resolve_float(val, v, useNumber); err == nil { + return "!!float", v.Interface() + } + case bytes.IndexByte(bools, c) != -1: + b := false + if _, err := resolve_bool(val, reflect.ValueOf(&b).Elem()); err == nil { + return "!!bool", b + } + } + + if hasBinaryTag(event) { + bytes, err := decode_binary(event.value) + if err == nil { + return "!!binary", bytes + } + } + + return "!!str", val +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver_test.go new file mode 100644 index 00000000000..61069ba992c --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/resolver_test.go @@ -0,0 +1,712 @@ +/* +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 candiedyaml + +import ( + "math" + "reflect" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Resolver", func() { + var event yaml_event_t + + var nulls = []string{"~", "null", "Null", "NULL"} + + BeforeEach(func() { + event = yaml_event_t{} + }) + + Context("Resolve", func() { + Context("Implicit events", func() { + checkNulls := func(f func()) { + for _, null := range nulls { + event = yaml_event_t{implicit: true} + event.value = []byte(null) + f() + } + } + + BeforeEach(func() { + event.implicit = true + }) + + Context("String", func() { + It("resolves a string", func() { + aString := "" + v := reflect.ValueOf(&aString) + event.value = []byte("abc") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!str")) + Ω(aString).To(Equal("abc")) + }) + + It("resolves the empty string", func() { + aString := "abc" + v := reflect.ValueOf(&aString) + event.value = []byte("") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!str")) + Ω(aString).To(Equal("")) + + }) + + It("resolves null", func() { + checkNulls(func() { + aString := "abc" + v := reflect.ValueOf(&aString) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(aString).To(Equal("")) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + aString := "abc" + pString := &aString + v := reflect.ValueOf(&pString) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pString).To(BeNil()) + }) + }) + + }) + + Context("Booleans", func() { + match_bool := func(val string, expected bool) { + b := !expected + + v := reflect.ValueOf(&b) + event.value = []byte(val) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!bool")) + Ω(b).To(Equal(expected)) + } + + It("resolves on", func() { + match_bool("on", true) + match_bool("ON", true) + }) + + It("resolves off", func() { + match_bool("off", false) + match_bool("OFF", false) + }) + + It("resolves true", func() { + match_bool("true", true) + match_bool("TRUE", true) + }) + + It("resolves false", func() { + match_bool("false", false) + match_bool("FALSE", false) + }) + + It("resolves yes", func() { + match_bool("yes", true) + match_bool("YES", true) + }) + + It("resolves no", func() { + match_bool("no", false) + match_bool("NO", false) + }) + + It("reports an error otherwise", func() { + b := true + v := reflect.ValueOf(&b) + event.value = []byte("fail") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("resolves null", func() { + checkNulls(func() { + b := true + v := reflect.ValueOf(&b) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(b).To(BeFalse()) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + b := true + pb := &b + v := reflect.ValueOf(&pb) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pb).To(BeNil()) + }) + }) + }) + + Context("Ints", func() { + It("simple ints", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("1234") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(1234)) + }) + + It("positive ints", func() { + i := int16(0) + v := reflect.ValueOf(&i) + event.value = []byte("+678") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(int16(678))) + }) + + It("negative ints", func() { + i := int32(0) + v := reflect.ValueOf(&i) + event.value = []byte("-2345") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(int32(-2345))) + }) + + It("base 2", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("0b11") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(3)) + }) + + It("base 8", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("012") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(10)) + }) + + It("base 16", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("0xff") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(255)) + }) + + It("base 60", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("1:30:00") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(5400)) + }) + + It("fails on overflow", func() { + i := int8(0) + v := reflect.ValueOf(&i) + event.value = []byte("2345") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("fails on invalid int", func() { + i := 0 + v := reflect.ValueOf(&i) + event.value = []byte("234f") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("resolves null", func() { + checkNulls(func() { + i := 1 + v := reflect.ValueOf(&i) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(i).To(Equal(0)) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + i := 1 + pi := &i + v := reflect.ValueOf(&pi) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pi).To(BeNil()) + }) + }) + + It("returns a Number", func() { + var i Number + v := reflect.ValueOf(&i) + + tag, err := resolve_int("12345", v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(Number("12345"))) + Ω(i.Int64()).Should(Equal(int64(12345))) + + event.value = []byte("1234") + tag, err = resolve(event, v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(Number("1234"))) + }) + }) + + Context("UInts", func() { + It("resolves simple uints", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("1234") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint(1234))) + }) + + It("resolves positive uints", func() { + i := uint16(0) + v := reflect.ValueOf(&i) + event.value = []byte("+678") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint16(678))) + }) + + It("base 2", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("0b11") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint(3))) + }) + + It("base 8", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("012") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint(10))) + }) + + It("base 16", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("0xff") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint(255))) + }) + + It("base 60", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("1:30:01") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(uint(5401))) + }) + + It("fails with negative ints", func() { + i := uint(0) + v := reflect.ValueOf(&i) + event.value = []byte("-2345") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("fails on overflow", func() { + i := uint8(0) + v := reflect.ValueOf(&i) + event.value = []byte("2345") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("resolves null", func() { + checkNulls(func() { + i := uint(1) + v := reflect.ValueOf(&i) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(i).To(Equal(uint(0))) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + i := uint(1) + pi := &i + v := reflect.ValueOf(&pi) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pi).To(BeNil()) + }) + }) + + It("returns a Number", func() { + var i Number + v := reflect.ValueOf(&i) + + tag, err := resolve_uint("12345", v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(Number("12345"))) + + event.value = []byte("1234") + tag, err = resolve(event, v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!int")) + Ω(i).To(Equal(Number("1234"))) + }) + }) + + Context("Floats", func() { + It("float32", func() { + f := float32(0) + v := reflect.ValueOf(&f) + event.value = []byte("2345.01") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(f).To(Equal(float32(2345.01))) + }) + + It("float64", func() { + f := float64(0) + v := reflect.ValueOf(&f) + event.value = []byte("-456456.01") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(f).To(Equal(float64(-456456.01))) + }) + + It("+inf", func() { + f := float64(0) + v := reflect.ValueOf(&f) + event.value = []byte("+.inf") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(f).To(Equal(math.Inf(1))) + }) + + It("-inf", func() { + f := float32(0) + v := reflect.ValueOf(&f) + event.value = []byte("-.inf") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(f).To(Equal(float32(math.Inf(-1)))) + }) + + It("nan", func() { + f := float64(0) + v := reflect.ValueOf(&f) + event.value = []byte(".NaN") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(math.IsNaN(f)).To(BeTrue()) + }) + + It("base 60", func() { + f := float64(0) + v := reflect.ValueOf(&f) + event.value = []byte("1:30:02") + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(f).To(Equal(float64(5402))) + }) + + It("fails on overflow", func() { + i := float32(0) + v := reflect.ValueOf(&i) + event.value = []byte("123e10000") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("fails on invalid float", func() { + i := float32(0) + v := reflect.ValueOf(&i) + event.value = []byte("123e1a") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + + It("resolves null", func() { + checkNulls(func() { + f := float64(1) + v := reflect.ValueOf(&f) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(f).To(Equal(0.0)) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + f := float64(1) + pf := &f + v := reflect.ValueOf(&pf) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pf).To(BeNil()) + }) + }) + + It("returns a Number", func() { + var i Number + v := reflect.ValueOf(&i) + + tag, err := resolve_float("12.345", v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(i).To(Equal(Number("12.345"))) + Ω(i.Float64()).Should(Equal(12.345)) + + event.value = []byte("1.234") + tag, err = resolve(event, v.Elem(), true) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!float")) + Ω(i).To(Equal(Number("1.234"))) + }) + }) + + Context("Timestamps", func() { + parse_date := func(val string, date time.Time) { + d := time.Now() + v := reflect.ValueOf(&d) + event.value = []byte(val) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("")) + Ω(d).To(Equal(date)) + } + + It("date", func() { + parse_date("2002-12-14", time.Date(2002, time.December, 14, 0, 0, 0, 0, time.UTC)) + }) + + It("canonical", func() { + parse_date("2001-12-15T02:59:43.1Z", time.Date(2001, time.December, 15, 2, 59, 43, int(1*time.Millisecond), time.UTC)) + }) + + It("iso8601", func() { + parse_date("2001-12-14t21:59:43.10-05:00", time.Date(2001, time.December, 14, 21, 59, 43, int(10*time.Millisecond), time.FixedZone("", -5*3600))) + }) + + It("space separated", func() { + parse_date("2001-12-14 21:59:43.10 -5", time.Date(2001, time.December, 14, 21, 59, 43, int(10*time.Millisecond), time.FixedZone("", -5*3600))) + }) + + It("no time zone", func() { + parse_date("2001-12-15 2:59:43.10", time.Date(2001, time.December, 15, 2, 59, 43, int(10*time.Millisecond), time.UTC)) + }) + + It("resolves null", func() { + checkNulls(func() { + d := time.Now() + v := reflect.ValueOf(&d) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(d).To(Equal(time.Time{})) + }) + }) + + It("resolves null pointers", func() { + checkNulls(func() { + d := time.Now() + pd := &d + v := reflect.ValueOf(&pd) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!null")) + Ω(pd).To(BeNil()) + }) + }) + }) + + Context("Binary tag", func() { + It("string", func() { + checkNulls(func() { + event.value = []byte("YWJjZGVmZw==") + event.tag = []byte("!binary") + aString := "" + v := reflect.ValueOf(&aString) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!str")) + Ω(aString).Should(Equal("abcdefg")) + }) + }) + + It("[]byte", func() { + checkNulls(func() { + event.value = []byte("YWJjZGVmZw==") + event.tag = []byte("!binary") + bytes := []byte(nil) + v := reflect.ValueOf(&bytes) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!str")) + Ω(bytes).Should(Equal([]byte("abcdefg"))) + }) + }) + + It("returns a []byte when provided no hints", func() { + checkNulls(func() { + event.value = []byte("YWJjZGVmZw==") + event.tag = []byte("!binary") + var intf interface{} + v := reflect.ValueOf(&intf) + + tag, err := resolve(event, v.Elem(), false) + Ω(err).ShouldNot(HaveOccurred()) + Ω(tag).Should(Equal("!!str")) + Ω(intf).Should(Equal([]byte("abcdefg"))) + }) + }) + }) + + It("fails to resolve a pointer", func() { + aString := "" + pString := &aString + v := reflect.ValueOf(&pString) + event.value = []byte("abc") + + _, err := resolve(event, v.Elem(), false) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("Not an implicit event && no tag", func() { + It("bool returns a string", func() { + event.value = []byte("on") + + tag, result := resolveInterface(event, false) + Ω(result).To(Equal("on")) + Ω(tag).Should(Equal("")) + }) + + It("number returns a string", func() { + event.value = []byte("1234") + + tag, result := resolveInterface(event, false) + Ω(result).To(Equal("1234")) + Ω(tag).Should(Equal("")) + }) + + It("returns the empty string", func() { + event.value = []byte("") + // event.implicit = true + + tag, result := resolveInterface(event, false) + Ω(result).To(Equal("")) + Ω(tag).Should(Equal("")) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/run_parser.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/run_parser.go new file mode 100644 index 00000000000..25c29816ee7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/run_parser.go @@ -0,0 +1,62 @@ +/* +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 candiedyaml + +import ( + "fmt" + "os" +) + +func Run_parser(cmd string, args []string) { + for i := 0; i < len(args); i++ { + fmt.Printf("[%d] Scanning '%s'", i, args[i]) + file, err := os.Open(args[i]) + if err != nil { + panic(fmt.Sprintf("Invalid file '%s': %s", args[i], err.Error())) + } + + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_reader(&parser, file) + + failed := false + token := yaml_token_t{} + count := 0 + for { + if !yaml_parser_scan(&parser, &token) { + failed = true + break + } + + if token.token_type == yaml_STREAM_END_TOKEN { + break + } + count++ + } + + file.Close() + + msg := "SUCCESS" + if failed { + msg = "FAILED" + if parser.error != yaml_NO_ERROR { + m := parser.problem_mark + fmt.Printf("ERROR: (%s) %s @ line: %d col: %d\n", + parser.context, parser.problem, m.line, m.column) + } + } + fmt.Printf("%s (%d tokens)\n", msg, count) + } +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner.go new file mode 100644 index 00000000000..fd28790b11b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner.go @@ -0,0 +1,3315 @@ +/* +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 candiedyaml + +import ( + "bytes" +) + +/* + * Introduction + * ************ + * + * The following notes assume that you are familiar with the YAML specification + * (http://yaml.org/spec/cvs/current.html). We mostly follow it, although in + * some cases we are less restrictive that it requires. + * + * The process of transforming a YAML stream into a sequence of events is + * divided on two steps: Scanning and Parsing. + * + * The Scanner transforms the input stream into a sequence of tokens, while the + * parser transform the sequence of tokens produced by the Scanner into a + * sequence of parsing events. + * + * The Scanner is rather clever and complicated. The Parser, on the contrary, + * is a straightforward implementation of a recursive-descendant parser (or, + * LL(1) parser, as it is usually called). + * + * Actually there are two issues of Scanning that might be called "clever", the + * rest is quite straightforward. The issues are "block collection start" and + * "simple keys". Both issues are explained below in details. + * + * Here the Scanning step is explained and implemented. We start with the list + * of all the tokens produced by the Scanner together with short descriptions. + * + * Now, tokens: + * + * STREAM-START(encoding) # The stream start. + * STREAM-END # The stream end. + * VERSION-DIRECTIVE(major,minor) # The '%YAML' directive. + * TAG-DIRECTIVE(handle,prefix) # The '%TAG' directive. + * DOCUMENT-START # '---' + * DOCUMENT-END # '...' + * BLOCK-SEQUENCE-START # Indentation increase denoting a block + * BLOCK-MAPPING-START # sequence or a block mapping. + * BLOCK-END # Indentation decrease. + * FLOW-SEQUENCE-START # '[' + * FLOW-SEQUENCE-END # ']' + * BLOCK-SEQUENCE-START # '{' + * BLOCK-SEQUENCE-END # '}' + * BLOCK-ENTRY # '-' + * FLOW-ENTRY # ',' + * KEY # '?' or nothing (simple keys). + * VALUE # ':' + * ALIAS(anchor) # '*anchor' + * ANCHOR(anchor) # '&anchor' + * TAG(handle,suffix) # '!handle!suffix' + * SCALAR(value,style) # A scalar. + * + * The following two tokens are "virtual" tokens denoting the beginning and the + * end of the stream: + * + * STREAM-START(encoding) + * STREAM-END + * + * We pass the information about the input stream encoding with the + * STREAM-START token. + * + * The next two tokens are responsible for tags: + * + * VERSION-DIRECTIVE(major,minor) + * TAG-DIRECTIVE(handle,prefix) + * + * Example: + * + * %YAML 1.1 + * %TAG ! !foo + * %TAG !yaml! tag:yaml.org,2002: + * --- + * + * The correspoding sequence of tokens: + * + * STREAM-START(utf-8) + * VERSION-DIRECTIVE(1,1) + * TAG-DIRECTIVE("!","!foo") + * TAG-DIRECTIVE("!yaml","tag:yaml.org,2002:") + * DOCUMENT-START + * STREAM-END + * + * Note that the VERSION-DIRECTIVE and TAG-DIRECTIVE tokens occupy a whole + * line. + * + * The document start and end indicators are represented by: + * + * DOCUMENT-START + * DOCUMENT-END + * + * Note that if a YAML stream contains an implicit document (without '---' + * and '...' indicators), no DOCUMENT-START and DOCUMENT-END tokens will be + * produced. + * + * In the following examples, we present whole documents together with the + * produced tokens. + * + * 1. An implicit document: + * + * 'a scalar' + * + * Tokens: + * + * STREAM-START(utf-8) + * SCALAR("a scalar",single-quoted) + * STREAM-END + * + * 2. An explicit document: + * + * --- + * 'a scalar' + * ... + * + * Tokens: + * + * STREAM-START(utf-8) + * DOCUMENT-START + * SCALAR("a scalar",single-quoted) + * DOCUMENT-END + * STREAM-END + * + * 3. Several documents in a stream: + * + * 'a scalar' + * --- + * 'another scalar' + * --- + * 'yet another scalar' + * + * Tokens: + * + * STREAM-START(utf-8) + * SCALAR("a scalar",single-quoted) + * DOCUMENT-START + * SCALAR("another scalar",single-quoted) + * DOCUMENT-START + * SCALAR("yet another scalar",single-quoted) + * STREAM-END + * + * We have already introduced the SCALAR token above. The following tokens are + * used to describe aliases, anchors, tag, and scalars: + * + * ALIAS(anchor) + * ANCHOR(anchor) + * TAG(handle,suffix) + * SCALAR(value,style) + * + * The following series of examples illustrate the usage of these tokens: + * + * 1. A recursive sequence: + * + * &A [ *A ] + * + * Tokens: + * + * STREAM-START(utf-8) + * ANCHOR("A") + * FLOW-SEQUENCE-START + * ALIAS("A") + * FLOW-SEQUENCE-END + * STREAM-END + * + * 2. A tagged scalar: + * + * !!float "3.14" # A good approximation. + * + * Tokens: + * + * STREAM-START(utf-8) + * TAG("!!","float") + * SCALAR("3.14",double-quoted) + * STREAM-END + * + * 3. Various scalar styles: + * + * --- # Implicit empty plain scalars do not produce tokens. + * --- a plain scalar + * --- 'a single-quoted scalar' + * --- "a double-quoted scalar" + * --- |- + * a literal scalar + * --- >- + * a folded + * scalar + * + * Tokens: + * + * STREAM-START(utf-8) + * DOCUMENT-START + * DOCUMENT-START + * SCALAR("a plain scalar",plain) + * DOCUMENT-START + * SCALAR("a single-quoted scalar",single-quoted) + * DOCUMENT-START + * SCALAR("a double-quoted scalar",double-quoted) + * DOCUMENT-START + * SCALAR("a literal scalar",literal) + * DOCUMENT-START + * SCALAR("a folded scalar",folded) + * STREAM-END + * + * Now it's time to review collection-related tokens. We will start with + * flow collections: + * + * FLOW-SEQUENCE-START + * FLOW-SEQUENCE-END + * FLOW-MAPPING-START + * FLOW-MAPPING-END + * FLOW-ENTRY + * KEY + * VALUE + * + * The tokens FLOW-SEQUENCE-START, FLOW-SEQUENCE-END, FLOW-MAPPING-START, and + * FLOW-MAPPING-END represent the indicators '[', ']', '{', and '}' + * correspondingly. FLOW-ENTRY represent the ',' indicator. Finally the + * indicators '?' and ':', which are used for denoting mapping keys and values, + * are represented by the KEY and VALUE tokens. + * + * The following examples show flow collections: + * + * 1. A flow sequence: + * + * [item 1, item 2, item 3] + * + * Tokens: + * + * STREAM-START(utf-8) + * FLOW-SEQUENCE-START + * SCALAR("item 1",plain) + * FLOW-ENTRY + * SCALAR("item 2",plain) + * FLOW-ENTRY + * SCALAR("item 3",plain) + * FLOW-SEQUENCE-END + * STREAM-END + * + * 2. A flow mapping: + * + * { + * a simple key: a value, # Note that the KEY token is produced. + * ? a complex key: another value, + * } + * + * Tokens: + * + * STREAM-START(utf-8) + * FLOW-MAPPING-START + * KEY + * SCALAR("a simple key",plain) + * VALUE + * SCALAR("a value",plain) + * FLOW-ENTRY + * KEY + * SCALAR("a complex key",plain) + * VALUE + * SCALAR("another value",plain) + * FLOW-ENTRY + * FLOW-MAPPING-END + * STREAM-END + * + * A simple key is a key which is not denoted by the '?' indicator. Note that + * the Scanner still produce the KEY token whenever it encounters a simple key. + * + * For scanning block collections, the following tokens are used (note that we + * repeat KEY and VALUE here): + * + * BLOCK-SEQUENCE-START + * BLOCK-MAPPING-START + * BLOCK-END + * BLOCK-ENTRY + * KEY + * VALUE + * + * The tokens BLOCK-SEQUENCE-START and BLOCK-MAPPING-START denote indentation + * increase that precedes a block collection (cf. the INDENT token in Python). + * The token BLOCK-END denote indentation decrease that ends a block collection + * (cf. the DEDENT token in Python). However YAML has some syntax pecularities + * that makes detections of these tokens more complex. + * + * The tokens BLOCK-ENTRY, KEY, and VALUE are used to represent the indicators + * '-', '?', and ':' correspondingly. + * + * The following examples show how the tokens BLOCK-SEQUENCE-START, + * BLOCK-MAPPING-START, and BLOCK-END are emitted by the Scanner: + * + * 1. Block sequences: + * + * - item 1 + * - item 2 + * - + * - item 3.1 + * - item 3.2 + * - + * key 1: value 1 + * key 2: value 2 + * + * Tokens: + * + * STREAM-START(utf-8) + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * SCALAR("item 1",plain) + * BLOCK-ENTRY + * SCALAR("item 2",plain) + * BLOCK-ENTRY + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * SCALAR("item 3.1",plain) + * BLOCK-ENTRY + * SCALAR("item 3.2",plain) + * BLOCK-END + * BLOCK-ENTRY + * BLOCK-MAPPING-START + * KEY + * SCALAR("key 1",plain) + * VALUE + * SCALAR("value 1",plain) + * KEY + * SCALAR("key 2",plain) + * VALUE + * SCALAR("value 2",plain) + * BLOCK-END + * BLOCK-END + * STREAM-END + * + * 2. Block mappings: + * + * a simple key: a value # The KEY token is produced here. + * ? a complex key + * : another value + * a mapping: + * key 1: value 1 + * key 2: value 2 + * a sequence: + * - item 1 + * - item 2 + * + * Tokens: + * + * STREAM-START(utf-8) + * BLOCK-MAPPING-START + * KEY + * SCALAR("a simple key",plain) + * VALUE + * SCALAR("a value",plain) + * KEY + * SCALAR("a complex key",plain) + * VALUE + * SCALAR("another value",plain) + * KEY + * SCALAR("a mapping",plain) + * BLOCK-MAPPING-START + * KEY + * SCALAR("key 1",plain) + * VALUE + * SCALAR("value 1",plain) + * KEY + * SCALAR("key 2",plain) + * VALUE + * SCALAR("value 2",plain) + * BLOCK-END + * KEY + * SCALAR("a sequence",plain) + * VALUE + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * SCALAR("item 1",plain) + * BLOCK-ENTRY + * SCALAR("item 2",plain) + * BLOCK-END + * BLOCK-END + * STREAM-END + * + * YAML does not always require to start a new block collection from a new + * line. If the current line contains only '-', '?', and ':' indicators, a new + * block collection may start at the current line. The following examples + * illustrate this case: + * + * 1. Collections in a sequence: + * + * - - item 1 + * - item 2 + * - key 1: value 1 + * key 2: value 2 + * - ? complex key + * : complex value + * + * Tokens: + * + * STREAM-START(utf-8) + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * SCALAR("item 1",plain) + * BLOCK-ENTRY + * SCALAR("item 2",plain) + * BLOCK-END + * BLOCK-ENTRY + * BLOCK-MAPPING-START + * KEY + * SCALAR("key 1",plain) + * VALUE + * SCALAR("value 1",plain) + * KEY + * SCALAR("key 2",plain) + * VALUE + * SCALAR("value 2",plain) + * BLOCK-END + * BLOCK-ENTRY + * BLOCK-MAPPING-START + * KEY + * SCALAR("complex key") + * VALUE + * SCALAR("complex value") + * BLOCK-END + * BLOCK-END + * STREAM-END + * + * 2. Collections in a mapping: + * + * ? a sequence + * : - item 1 + * - item 2 + * ? a mapping + * : key 1: value 1 + * key 2: value 2 + * + * Tokens: + * + * STREAM-START(utf-8) + * BLOCK-MAPPING-START + * KEY + * SCALAR("a sequence",plain) + * VALUE + * BLOCK-SEQUENCE-START + * BLOCK-ENTRY + * SCALAR("item 1",plain) + * BLOCK-ENTRY + * SCALAR("item 2",plain) + * BLOCK-END + * KEY + * SCALAR("a mapping",plain) + * VALUE + * BLOCK-MAPPING-START + * KEY + * SCALAR("key 1",plain) + * VALUE + * SCALAR("value 1",plain) + * KEY + * SCALAR("key 2",plain) + * VALUE + * SCALAR("value 2",plain) + * BLOCK-END + * BLOCK-END + * STREAM-END + * + * YAML also permits non-indented sequences if they are included into a block + * mapping. In this case, the token BLOCK-SEQUENCE-START is not produced: + * + * key: + * - item 1 # BLOCK-SEQUENCE-START is NOT produced here. + * - item 2 + * + * Tokens: + * + * STREAM-START(utf-8) + * BLOCK-MAPPING-START + * KEY + * SCALAR("key",plain) + * VALUE + * BLOCK-ENTRY + * SCALAR("item 1",plain) + * BLOCK-ENTRY + * SCALAR("item 2",plain) + * BLOCK-END + */ + +/* + * Ensure that the buffer contains the required number of characters. + * Return 1 on success, 0 on failure (reader error or memory error). + */ +func cache(parser *yaml_parser_t, length int) bool { + if parser.unread >= length { + return true + } + + return yaml_parser_update_buffer(parser, length) +} + +/* + * Advance the buffer pointer. + */ +func skip(parser *yaml_parser_t) { + parser.mark.index++ + parser.mark.column++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) +} + +func skip_line(parser *yaml_parser_t) { + if is_crlf_at(parser.buffer, parser.buffer_pos) { + parser.mark.index += 2 + parser.mark.column = 0 + parser.mark.line++ + parser.unread -= 2 + parser.buffer_pos += 2 + } else if is_break_at(parser.buffer, parser.buffer_pos) { + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) + } +} + +/* + * Copy a character to a string buffer and advance pointers. + */ + +func read(parser *yaml_parser_t, s []byte) []byte { + w := width(parser.buffer[parser.buffer_pos]) + if w == 0 { + panic("invalid character sequence") + } + if len(s) == 0 { + s = make([]byte, 0, 32) + } + if w == 1 && len(s)+w <= cap(s) { + s = s[:len(s)+1] + s[len(s)-1] = parser.buffer[parser.buffer_pos] + parser.buffer_pos++ + } else { + s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) + parser.buffer_pos += w + } + parser.mark.index++ + parser.mark.column++ + parser.unread-- + return s +} + +/* + * Copy a line break character to a string buffer and advance pointers. + */ +func read_line(parser *yaml_parser_t, s []byte) []byte { + buf := parser.buffer + pos := parser.buffer_pos + if buf[pos] == '\r' && buf[pos+1] == '\n' { + /* CR LF . LF */ + s = append(s, '\n') + parser.buffer_pos += 2 + parser.mark.index++ + parser.unread-- + } else if buf[pos] == '\r' || buf[pos] == '\n' { + /* CR|LF . LF */ + s = append(s, '\n') + parser.buffer_pos += 1 + } else if buf[pos] == '\xC2' && buf[pos+1] == '\x85' { + /* NEL . LF */ + s = append(s, '\n') + parser.buffer_pos += 2 + } else if buf[pos] == '\xE2' && buf[pos+1] == '\x80' && + (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9') { + // LS|PS . LS|PS + s = append(s, buf[parser.buffer_pos:pos+3]...) + parser.buffer_pos += 3 + } else { + return s + } + + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + return s +} + +/* + * Get the next token. + */ + +func yaml_parser_scan(parser *yaml_parser_t, token *yaml_token_t) bool { + /* Erase the token object. */ + *token = yaml_token_t{} + + /* No tokens after STREAM-END or error. */ + + if parser.stream_end_produced || parser.error != yaml_NO_ERROR { + return true + } + + /* Ensure that the tokens queue contains enough tokens. */ + + if !parser.token_available { + if !yaml_parser_fetch_more_tokens(parser) { + return false + } + } + + /* Fetch the next token from the queue. */ + + *token = parser.tokens[parser.tokens_head] + parser.tokens_head++ + parser.token_available = false + parser.tokens_parsed++ + + if token.token_type == yaml_STREAM_END_TOKEN { + parser.stream_end_produced = true + } + + return true +} + +/* + * Set the scanner error and return 0. + */ + +func yaml_parser_set_scanner_error(parser *yaml_parser_t, context string, + context_mark YAML_mark_t, problem string) bool { + parser.error = yaml_SCANNER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = parser.mark + + return false +} + +func yaml_parser_set_scanner_tag_error(parser *yaml_parser_t, directive bool, context_mark YAML_mark_t, problem string) bool { + context := "while parsing a %TAG directive" + if directive { + context = "while parsing a tag" + } + return yaml_parser_set_scanner_error(parser, context, context_mark, "did not find URI escaped octet") +} + +/* + * Ensure that the tokens queue contains at least one token which can be + * returned to the Parser. + */ + +func yaml_parser_fetch_more_tokens(parser *yaml_parser_t) bool { + /* While we need more tokens to fetch, do it. */ + + for { + /* + * Check if we really need to fetch more tokens. + */ + + need_more_tokens := false + + if parser.tokens_head == len(parser.tokens) { + /* Queue is empty. */ + + need_more_tokens = true + } else { + + /* Check if any potential simple key may occupy the head position. */ + + if !yaml_parser_stale_simple_keys(parser) { + return false + } + + for i := range parser.simple_keys { + simple_key := &parser.simple_keys[i] + + if simple_key.possible && + simple_key.token_number == parser.tokens_parsed { + need_more_tokens = true + break + } + } + } + if len(parser.simple_keys) > 0 { + + } + /* We are finished. */ + + if !need_more_tokens { + break + } + + /* Fetch the next token. */ + + if !yaml_parser_fetch_next_token(parser) { + return false + } + + } + + parser.token_available = true + + return true +} + +/* + * The dispatcher for token fetchers. + */ + +func yaml_parser_fetch_next_token(parser *yaml_parser_t) bool { + /* Ensure that the buffer is initialized. */ + + if !cache(parser, 1) { + return false + } + + /* Check if we just started scanning. Fetch STREAM-START then. */ + + if !parser.stream_start_produced { + return yaml_parser_fetch_stream_start(parser) + } + + /* Eat whitespaces and comments until we reach the next token. */ + + if !yaml_parser_scan_to_next_token(parser) { + return false + } + + /* Remove obsolete potential simple keys. */ + + if !yaml_parser_stale_simple_keys(parser) { + return false + } + + /* Check the indentation level against the current column. */ + + if !yaml_parser_unroll_indent(parser, parser.mark.column) { + return false + } + + /* + * Ensure that the buffer contains at least 4 characters. 4 is the length + * of the longest indicators ('--- ' and '... '). + */ + + if !cache(parser, 4) { + return false + } + + /* Is it the end of the stream? */ + buf := parser.buffer + pos := parser.buffer_pos + + if is_z(buf[pos]) { + return yaml_parser_fetch_stream_end(parser) + } + + /* Is it a directive? */ + + if parser.mark.column == 0 && buf[pos] == '%' { + return yaml_parser_fetch_directive(parser) + } + + /* Is it the document start indicator? */ + + if parser.mark.column == 0 && + buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && + is_blankz_at(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, + yaml_DOCUMENT_START_TOKEN) + } + + /* Is it the document end indicator? */ + + if parser.mark.column == 0 && + buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && + is_blankz_at(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, + yaml_DOCUMENT_END_TOKEN) + } + + /* Is it the flow sequence start indicator? */ + + if buf[pos] == '[' { + return yaml_parser_fetch_flow_collection_start(parser, + yaml_FLOW_SEQUENCE_START_TOKEN) + } + + /* Is it the flow mapping start indicator? */ + + if buf[pos] == '{' { + return yaml_parser_fetch_flow_collection_start(parser, + yaml_FLOW_MAPPING_START_TOKEN) + } + + /* Is it the flow sequence end indicator? */ + + if buf[pos] == ']' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_SEQUENCE_END_TOKEN) + } + + /* Is it the flow mapping end indicator? */ + + if buf[pos] == '}' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_MAPPING_END_TOKEN) + } + + /* Is it the flow entry indicator? */ + + if buf[pos] == ',' { + return yaml_parser_fetch_flow_entry(parser) + } + + /* Is it the block entry indicator? */ + if buf[pos] == '-' && is_blankz_at(buf, pos+1) { + return yaml_parser_fetch_block_entry(parser) + } + + /* Is it the key indicator? */ + if buf[pos] == '?' && + (parser.flow_level > 0 || is_blankz_at(buf, pos+1)) { + return yaml_parser_fetch_key(parser) + } + + /* Is it the value indicator? */ + if buf[pos] == ':' && + (parser.flow_level > 0 || is_blankz_at(buf, pos+1)) { + return yaml_parser_fetch_value(parser) + } + + /* Is it an alias? */ + if buf[pos] == '*' { + return yaml_parser_fetch_anchor(parser, yaml_ALIAS_TOKEN) + } + + /* Is it an anchor? */ + + if buf[pos] == '&' { + return yaml_parser_fetch_anchor(parser, yaml_ANCHOR_TOKEN) + } + + /* Is it a tag? */ + + if buf[pos] == '!' { + return yaml_parser_fetch_tag(parser) + } + + /* Is it a literal scalar? */ + if buf[pos] == '|' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, true) + } + + /* Is it a folded scalar? */ + if buf[pos] == '>' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, false) + } + + /* Is it a single-quoted scalar? */ + + if buf[pos] == '\'' { + return yaml_parser_fetch_flow_scalar(parser, true) + } + + /* Is it a double-quoted scalar? */ + if buf[pos] == '"' { + return yaml_parser_fetch_flow_scalar(parser, false) + } + + /* + * Is it a plain scalar? + * + * A plain scalar may start with any non-blank characters except + * + * '-', '?', ':', ',', '[', ']', '{', '}', + * '#', '&', '*', '!', '|', '>', '\'', '\"', + * '%', '@', '`'. + * + * In the block context (and, for the '-' indicator, in the flow context + * too), it may also start with the characters + * + * '-', '?', ':' + * + * if it is followed by a non-space character. + * + * The last rule is more restrictive than the specification requires. + */ + + b := buf[pos] + if !(is_blankz_at(buf, pos) || b == '-' || + b == '?' || b == ':' || + b == ',' || b == '[' || + b == ']' || b == '{' || + b == '}' || b == '#' || + b == '&' || b == '*' || + b == '!' || b == '|' || + b == '>' || b == '\'' || + b == '"' || b == '%' || + b == '@' || b == '`') || + (b == '-' && !is_blank(buf[pos+1])) || + (parser.flow_level == 0 && + (buf[pos] == '?' || buf[pos+1] == ':') && + !is_blank(buf[pos+1])) { + return yaml_parser_fetch_plain_scalar(parser) + } + + /* + * If we don't determine the token type so far, it is an error. + */ + + return yaml_parser_set_scanner_error(parser, + "while scanning for the next token", parser.mark, + "found character that cannot start any token") +} + +/* + * Check the list of potential simple keys and remove the positions that + * cannot contain simple keys anymore. + */ + +func yaml_parser_stale_simple_keys(parser *yaml_parser_t) bool { + /* Check for a potential simple key for each flow level. */ + + for i := range parser.simple_keys { + /* + * The specification requires that a simple key + * + * - is limited to a single line, + * - is shorter than 1024 characters. + */ + + simple_key := &parser.simple_keys[i] + if simple_key.possible && + (simple_key.mark.line < parser.mark.line || + simple_key.mark.index+1024 < parser.mark.index) { + + /* Check if the potential simple key to be removed is required. */ + + if simple_key.required { + return yaml_parser_set_scanner_error(parser, + "while scanning a simple key", simple_key.mark, + "could not find expected ':'") + } + + simple_key.possible = false + } + } + + return true +} + +/* + * Check if a simple key may start at the current position and add it if + * needed. + */ + +func yaml_parser_save_simple_key(parser *yaml_parser_t) bool { + /* + * A simple key is required at the current position if the scanner is in + * the block context and the current column coincides with the indentation + * level. + */ + + required := (parser.flow_level == 0 && + parser.indent == parser.mark.column) + + /* + * A simple key is required only when it is the first token in the current + * line. Therefore it is always allowed. But we add a check anyway. + */ + if required && !parser.simple_key_allowed { + panic("impossible") /* Impossible. */ + } + + /* + * If the current position may start a simple key, save it. + */ + + if parser.simple_key_allowed { + simple_key := yaml_simple_key_t{ + possible: true, + required: required, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + } + simple_key.mark = parser.mark + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_keys[len(parser.simple_keys)-1] = simple_key + } + + return true +} + +/* + * Remove a potential simple key at the current flow level. + */ + +func yaml_parser_remove_simple_key(parser *yaml_parser_t) bool { + simple_key := &parser.simple_keys[len(parser.simple_keys)-1] + + if simple_key.possible { + /* If the key is required, it is an error. */ + + if simple_key.required { + return yaml_parser_set_scanner_error(parser, + "while scanning a simple key", simple_key.mark, + "could not find expected ':'") + } + } + + /* Remove the key from the stack. */ + + simple_key.possible = false + + return true +} + +/* + * Increase the flow level and resize the simple key list if needed. + */ + +func yaml_parser_increase_flow_level(parser *yaml_parser_t) bool { + /* Reset the simple key on the next level. */ + + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{}) + + /* Increase the flow level. */ + + parser.flow_level++ + + return true +} + +/* + * Decrease the flow level. + */ + +func yaml_parser_decrease_flow_level(parser *yaml_parser_t) bool { + if parser.flow_level > 0 { + parser.flow_level-- + parser.simple_keys = parser.simple_keys[:len(parser.simple_keys)-1] + } + + return true +} + +/* + * Push the current indentation level to the stack and set the new level + * the current column is greater than the indentation level. In this case, + * append or insert the specified token into the token queue. + * + */ + +func yaml_parser_roll_indent(parser *yaml_parser_t, column int, + number int, token_type yaml_token_type_t, mark YAML_mark_t) bool { + /* In the flow context, do nothing. */ + + if parser.flow_level > 0 { + return true + } + + if parser.indent == -1 || parser.indent < column { + /* + * Push the current indentation level to the stack and set the new + * indentation level. + */ + + parser.indents = append(parser.indents, parser.indent) + parser.indent = column + + /* Create a token and insert it into the queue. */ + token := yaml_token_t{ + token_type: token_type, + start_mark: mark, + end_mark: mark, + } + + // number == -1 -> enqueue otherwise insert + if number > -1 { + number -= parser.tokens_parsed + } + insert_token(parser, number, &token) + } + + return true +} + +/* + * Pop indentation levels from the indents stack until the current level + * becomes less or equal to the column. For each intendation level, append + * the BLOCK-END token. + */ + +func yaml_parser_unroll_indent(parser *yaml_parser_t, column int) bool { + /* In the flow context, do nothing. */ + + if parser.flow_level > 0 { + return true + } + + /* + * column is unsigned and parser->indent is signed, so if + * parser->indent is less than zero the conditional in the while + * loop below is incorrect. Guard against that. + */ + + if parser.indent < 0 { + return true + } + + /* Loop through the intendation levels in the stack. */ + + for parser.indent > column { + /* Create a token and append it to the queue. */ + token := yaml_token_t{ + token_type: yaml_BLOCK_END_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + } + insert_token(parser, -1, &token) + + /* Pop the indentation level. */ + parser.indent = parser.indents[len(parser.indents)-1] + parser.indents = parser.indents[:len(parser.indents)-1] + + } + + return true +} + +/* + * Pop indentation levels from the indents stack until the current + * level resets to -1. For each intendation level, append the + * BLOCK-END token. + */ + +func yaml_parser_reset_indent(parser *yaml_parser_t) bool { + /* In the flow context, do nothing. */ + + if parser.flow_level > 0 { + return true + } + + /* Loop through the intendation levels in the stack. */ + + for parser.indent > -1 { + /* Create a token and append it to the queue. */ + + token := yaml_token_t{ + token_type: yaml_BLOCK_END_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + } + insert_token(parser, -1, &token) + + /* Pop the indentation level. */ + parser.indent = parser.indents[len(parser.indents)-1] + parser.indents = parser.indents[:len(parser.indents)-1] + } + + return true +} + +/* + * Initialize the scanner and produce the STREAM-START token. + */ + +func yaml_parser_fetch_stream_start(parser *yaml_parser_t) bool { + /* Set the initial indentation. */ + + parser.indent = -1 + + /* Initialize the simple key stack. */ + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{}) + + /* A simple key is allowed at the beginning of the stream. */ + + parser.simple_key_allowed = true + + /* We have started. */ + + parser.stream_start_produced = true + + /* Create the STREAM-START token and append it to the queue. */ + token := yaml_token_t{ + token_type: yaml_STREAM_START_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + encoding: parser.encoding, + } + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the STREAM-END token and shut down the scanner. + */ + +func yaml_parser_fetch_stream_end(parser *yaml_parser_t) bool { + /* Force new line. */ + + if parser.mark.column != 0 { + parser.mark.column = 0 + parser.mark.line++ + } + + /* Reset the indentation level. */ + + if !yaml_parser_reset_indent(parser) { + return false + } + + /* Reset simple keys. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + /* Create the STREAM-END token and append it to the queue. */ + token := yaml_token_t{ + token_type: yaml_STREAM_END_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. + */ + +func yaml_parser_fetch_directive(parser *yaml_parser_t) bool { + /* Reset the indentation level. */ + + if !yaml_parser_reset_indent(parser) { + return false + } + + /* Reset simple keys. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + /* Create the YAML-DIRECTIVE or TAG-DIRECTIVE token. */ + var token yaml_token_t + if !yaml_parser_scan_directive(parser, &token) { + return false + } + + /* Append the token to the queue. */ + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the DOCUMENT-START or DOCUMENT-END token. + */ + +func yaml_parser_fetch_document_indicator(parser *yaml_parser_t, + token_type yaml_token_type_t) bool { + + /* Reset the indentation level. */ + + if !yaml_parser_reset_indent(parser) { + return false + } + + /* Reset simple keys. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + /* Consume the token. */ + + start_mark := parser.mark + + skip(parser) + skip(parser) + skip(parser) + + end_mark := parser.mark + + /* Create the DOCUMENT-START or DOCUMENT-END token. */ + + token := yaml_token_t{ + token_type: token_type, + start_mark: start_mark, + end_mark: end_mark, + } + + /* Append the token to the queue. */ + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. + */ + +func yaml_parser_fetch_flow_collection_start(parser *yaml_parser_t, + token_type yaml_token_type_t) bool { + + /* The indicators '[' and '{' may start a simple key. */ + + if !yaml_parser_save_simple_key(parser) { + return false + } + + /* Increase the flow level. */ + + if !yaml_parser_increase_flow_level(parser) { + return false + } + + /* A simple key may follow the indicators '[' and '{'. */ + + parser.simple_key_allowed = true + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. */ + + token := yaml_token_t{ + token_type: token_type, + start_mark: start_mark, + end_mark: end_mark, + } + + /* Append the token to the queue. */ + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. + */ + +func yaml_parser_fetch_flow_collection_end(parser *yaml_parser_t, + token_type yaml_token_type_t) bool { + + /* Reset any potential simple key on the current flow level. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + /* Decrease the flow level. */ + + if !yaml_parser_decrease_flow_level(parser) { + return false + } + + /* No simple keys after the indicators ']' and '}'. */ + + parser.simple_key_allowed = false + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. */ + + token := yaml_token_t{ + token_type: token_type, + start_mark: start_mark, + end_mark: end_mark, + } + + /* Append the token to the queue. */ + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the FLOW-ENTRY token. + */ + +func yaml_parser_fetch_flow_entry(parser *yaml_parser_t) bool { + + /* Reset any potential simple keys on the current flow level. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + /* Simple keys are allowed after ','. */ + + parser.simple_key_allowed = true + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the FLOW-ENTRY token and append it to the queue. */ + + token := yaml_token_t{ + token_type: yaml_FLOW_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the BLOCK-ENTRY token. + */ + +func yaml_parser_fetch_block_entry(parser *yaml_parser_t) bool { + + /* Check if the scanner is in the block context. */ + + if parser.flow_level == 0 { + /* Check if we are allowed to start a new entry. */ + + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "block sequence entries are not allowed in this context") + } + + /* Add the BLOCK-SEQUENCE-START token if needed. */ + + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, + yaml_BLOCK_SEQUENCE_START_TOKEN, parser.mark) { + return false + } + } else { + /* + * It is an error for the '-' indicator to occur in the flow context, + * but we let the Parser detect and report about it because the Parser + * is able to point to the context. + */ + } + + /* Reset any potential simple keys on the current flow level. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + /* Simple keys are allowed after '-'. */ + + parser.simple_key_allowed = true + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the BLOCK-ENTRY token and append it to the queue. */ + + token := yaml_token_t{ + token_type: yaml_BLOCK_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the KEY token. + */ + +func yaml_parser_fetch_key(parser *yaml_parser_t) bool { + /* In the block context, additional checks are required. */ + + if parser.flow_level == 0 { + /* Check if we are allowed to start a new key (not nessesary simple). */ + + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping keys are not allowed in this context") + } + + /* Add the BLOCK-MAPPING-START token if needed. */ + + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, + yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + /* Reset any potential simple keys on the current flow level. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + /* Simple keys are allowed after '?' in the block context. */ + + parser.simple_key_allowed = (parser.flow_level == 0) + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the KEY token and append it to the queue. */ + + token := yaml_token_t{ + token_type: yaml_KEY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the VALUE token. + */ + +func yaml_parser_fetch_value(parser *yaml_parser_t) bool { + + simple_key := &parser.simple_keys[len(parser.simple_keys)-1] + + /* Have we found a simple key? */ + + if simple_key.possible { + + /* Create the KEY token and insert it into the queue. */ + + token := yaml_token_t{ + token_type: yaml_KEY_TOKEN, + start_mark: simple_key.mark, + end_mark: simple_key.mark, + } + + insert_token(parser, simple_key.token_number-parser.tokens_parsed, &token) + + /* In the block context, we may need to add the BLOCK-MAPPING-START token. */ + + if !yaml_parser_roll_indent(parser, simple_key.mark.column, + simple_key.token_number, + yaml_BLOCK_MAPPING_START_TOKEN, simple_key.mark) { + return false + } + + /* Remove the simple key. */ + + simple_key.possible = false + + /* A simple key cannot follow another simple key. */ + + parser.simple_key_allowed = false + } else { + /* The ':' indicator follows a complex key. */ + + /* In the block context, extra checks are required. */ + + if parser.flow_level == 0 { + /* Check if we are allowed to start a complex value. */ + + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping values are not allowed in this context") + } + + /* Add the BLOCK-MAPPING-START token if needed. */ + + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, + yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + /* Simple keys after ':' are allowed in the block context. */ + + parser.simple_key_allowed = (parser.flow_level == 0) + } + + /* Consume the token. */ + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + /* Create the VALUE token and append it to the queue. */ + + token := yaml_token_t{ + token_type: yaml_VALUE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the ALIAS or ANCHOR token. + */ + +func yaml_parser_fetch_anchor(parser *yaml_parser_t, token_type yaml_token_type_t) bool { + + /* An anchor or an alias could be a simple key. */ + + if !yaml_parser_save_simple_key(parser) { + return false + } + + /* A simple key cannot follow an anchor or an alias. */ + + parser.simple_key_allowed = false + + /* Create the ALIAS or ANCHOR token and append it to the queue. */ + var token yaml_token_t + if !yaml_parser_scan_anchor(parser, &token, token_type) { + return false + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the TAG token. + */ + +func yaml_parser_fetch_tag(parser *yaml_parser_t) bool { + /* A tag could be a simple key. */ + + if !yaml_parser_save_simple_key(parser) { + return false + } + + /* A simple key cannot follow a tag. */ + + parser.simple_key_allowed = false + + /* Create the TAG token and append it to the queue. */ + var token yaml_token_t + if !yaml_parser_scan_tag(parser, &token) { + return false + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. + */ + +func yaml_parser_fetch_block_scalar(parser *yaml_parser_t, literal bool) bool { + /* Remove any potential simple keys. */ + + if !yaml_parser_remove_simple_key(parser) { + return false + } + + /* A simple key may follow a block scalar. */ + + parser.simple_key_allowed = true + + /* Create the SCALAR token and append it to the queue. */ + var token yaml_token_t + if !yaml_parser_scan_block_scalar(parser, &token, literal) { + return false + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. + */ + +func yaml_parser_fetch_flow_scalar(parser *yaml_parser_t, single bool) bool { + + /* A plain scalar could be a simple key. */ + + if !yaml_parser_save_simple_key(parser) { + return false + } + + /* A simple key cannot follow a flow scalar. */ + + parser.simple_key_allowed = false + + /* Create the SCALAR token and append it to the queue. */ + var token yaml_token_t + if !yaml_parser_scan_flow_scalar(parser, &token, single) { + return false + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Produce the SCALAR(...,plain) token. + */ + +func yaml_parser_fetch_plain_scalar(parser *yaml_parser_t) bool { + /* A plain scalar could be a simple key. */ + + if !yaml_parser_save_simple_key(parser) { + return false + } + + /* A simple key cannot follow a flow scalar. */ + + parser.simple_key_allowed = false + + /* Create the SCALAR token and append it to the queue. */ + var token yaml_token_t + if !yaml_parser_scan_plain_scalar(parser, &token) { + return false + } + + insert_token(parser, -1, &token) + + return true +} + +/* + * Eat whitespaces and comments until the next token is found. + */ + +func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool { + /* Until the next token is not found. */ + + for { + /* Allow the BOM mark to start a line. */ + + if !cache(parser, 1) { + return false + } + + if parser.mark.column == 0 && is_bom_at(parser.buffer, parser.buffer_pos) { + skip(parser) + } + + /* + * Eat whitespaces. + * + * Tabs are allowed: + * + * - in the flow context; + * - in the block context, but not at the beginning of the line or + * after '-', '?', or ':' (complex value). + */ + + if !cache(parser, 1) { + return false + } + + for parser.buffer[parser.buffer_pos] == ' ' || + ((parser.flow_level > 0 || !parser.simple_key_allowed) && + parser.buffer[parser.buffer_pos] == '\t') { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + /* Eat a comment until a line break. */ + + if parser.buffer[parser.buffer_pos] == '#' { + for !is_breakz_at(parser.buffer, parser.buffer_pos) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + } + + /* If it is a line break, eat it. */ + + if is_break_at(parser.buffer, parser.buffer_pos) { + if !cache(parser, 2) { + return false + } + skip_line(parser) + + /* In the block context, a new line may start a simple key. */ + + if parser.flow_level == 0 { + parser.simple_key_allowed = true + } + } else { + /* We have found a token. */ + + break + } + } + + return true +} + +/* + * Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. + * + * Scope: + * %YAML 1.1 # a comment \n + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * %TAG !yaml! tag:yaml.org,2002: \n + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + */ + +func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool { + /* Eat '%'. */ + + start_mark := parser.mark + + skip(parser) + + /* Scan the directive name. */ + var name []byte + if !yaml_parser_scan_directive_name(parser, start_mark, &name) { + return false + } + + /* Is it a YAML directive? */ + var major, minor int + if bytes.Equal(name, []byte("YAML")) { + /* Scan the VERSION directive value. */ + + if !yaml_parser_scan_version_directive_value(parser, start_mark, + &major, &minor) { + return false + } + + end_mark := parser.mark + + /* Create a VERSION-DIRECTIVE token. */ + + *token = yaml_token_t{ + token_type: yaml_VERSION_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + major: major, + minor: minor, + } + } else if bytes.Equal(name, []byte("TAG")) { + /* Is it a TAG directive? */ + /* Scan the TAG directive value. */ + var handle, prefix []byte + if !yaml_parser_scan_tag_directive_value(parser, start_mark, + &handle, &prefix) { + return false + } + + end_mark := parser.mark + + /* Create a TAG-DIRECTIVE token. */ + + *token = yaml_token_t{ + token_type: yaml_TAG_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + prefix: prefix, + } + } else { + /* Unknown directive. */ + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found uknown directive name") + return false + } + + /* Eat the rest of the line including any comments. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + if parser.buffer[parser.buffer_pos] == '#' { + for !is_breakz_at(parser.buffer, parser.buffer_pos) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + } + + /* Check if we are at the end of the line. */ + + if !is_breakz_at(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "did not find expected comment or line break") + return false + } + + /* Eat a line break. */ + + if is_break_at(parser.buffer, parser.buffer_pos) { + if !cache(parser, 2) { + return false + } + skip_line(parser) + } + + return true +} + +/* + * Scan the directive name. + * + * Scope: + * %YAML 1.1 # a comment \n + * ^^^^ + * %TAG !yaml! tag:yaml.org,2002: \n + * ^^^ + */ + +func yaml_parser_scan_directive_name(parser *yaml_parser_t, + start_mark YAML_mark_t, name *[]byte) bool { + + /* Consume the directive name. */ + + if !cache(parser, 1) { + return false + } + + var s []byte + for is_alpha(parser.buffer[parser.buffer_pos]) { + s = read(parser, s) + if !cache(parser, 1) { + return false + } + } + + /* Check if the name is empty. */ + + if len(s) == 0 { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "could not find expected directive name") + return false + } + + /* Check for an blank character after the name. */ + + if !is_blankz_at(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found unexpected non-alphabetical character") + return false + } + + *name = s + + return true +} + +/* + * Scan the value of VERSION-DIRECTIVE. + * + * Scope: + * %YAML 1.1 # a comment \n + * ^^^^^^ + */ + +func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, + start_mark YAML_mark_t, major *int, minor *int) bool { + /* Eat whitespaces. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + /* Consume the major version number. */ + + if !yaml_parser_scan_version_directive_number(parser, start_mark, major) { + return false + } + + /* Eat '.'. */ + + if parser.buffer[parser.buffer_pos] != '.' { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected digit or '.' character") + } + + skip(parser) + + /* Consume the minor version number. */ + + if !yaml_parser_scan_version_directive_number(parser, start_mark, minor) { + return false + } + + return true +} + +const MAX_NUMBER_LENGTH = 9 + +/* + * Scan the version number of VERSION-DIRECTIVE. + * + * Scope: + * %YAML 1.1 # a comment \n + * ^ + * %YAML 1.1 # a comment \n + * ^ + */ + +func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, + start_mark YAML_mark_t, number *int) bool { + + /* Repeat while the next character is digit. */ + + if !cache(parser, 1) { + return false + } + + value := 0 + length := 0 + for is_digit(parser.buffer[parser.buffer_pos]) { + /* Check if the number is too long. */ + + length++ + if length > MAX_NUMBER_LENGTH { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "found extremely long version number") + } + + value = value*10 + as_digit(parser.buffer[parser.buffer_pos]) + + skip(parser) + + if !cache(parser, 1) { + return false + } + } + + /* Check if the number was present. */ + + if length == 0 { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected version number") + } + + *number = value + + return true +} + +/* + * Scan the value of a TAG-DIRECTIVE token. + * + * Scope: + * %TAG !yaml! tag:yaml.org,2002: \n + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + */ + +func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, + start_mark YAML_mark_t, handle, prefix *[]byte) bool { + + /* Eat whitespaces. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + /* Scan a handle. */ + var handle_value []byte + if !yaml_parser_scan_tag_handle(parser, true, start_mark, &handle_value) { + return false + } + + /* Expect a whitespace. */ + + if !cache(parser, 1) { + return false + } + + if !is_blank(parser.buffer[parser.buffer_pos]) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace") + return false + } + + /* Eat whitespaces. */ + + for is_blank(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + /* Scan a prefix. */ + var prefix_value []byte + if !yaml_parser_scan_tag_uri(parser, true, nil, start_mark, &prefix_value) { + return false + } + + /* Expect a whitespace or line break. */ + + if !cache(parser, 1) { + return false + } + + if !is_blankz_at(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace or line break") + return false + } + + *handle = handle_value + *prefix = prefix_value + + return true +} + +func yaml_parser_scan_anchor(parser *yaml_parser_t, token *yaml_token_t, + token_type yaml_token_type_t) bool { + + /* Eat the indicator character. */ + + start_mark := parser.mark + + skip(parser) + + /* Consume the value. */ + + if !cache(parser, 1) { + return false + } + + var s []byte + for is_alpha(parser.buffer[parser.buffer_pos]) { + s = read(parser, s) + if !cache(parser, 1) { + return false + } + } + + end_mark := parser.mark + + /* + * Check if length of the anchor is greater than 0 and it is followed by + * a whitespace character or one of the indicators: + * + * '?', ':', ',', ']', '}', '%', '@', '`'. + */ + + b := parser.buffer[parser.buffer_pos] + if len(s) == 0 || !(is_blankz_at(parser.buffer, parser.buffer_pos) || b == '?' || + b == ':' || b == ',' || + b == ']' || b == '}' || + b == '%' || b == '@' || + b == '`') { + context := "while scanning an anchor" + if token_type != yaml_ANCHOR_TOKEN { + context = "while scanning an alias" + } + yaml_parser_set_scanner_error(parser, context, start_mark, + "did not find expected alphabetic or numeric character") + return false + } + + /* Create a token. */ + *token = yaml_token_t{ + token_type: token_type, + start_mark: start_mark, + end_mark: end_mark, + value: s, + } + + return true +} + +/* + * Scan a TAG token. + */ + +func yaml_parser_scan_tag(parser *yaml_parser_t, token *yaml_token_t) bool { + start_mark := parser.mark + + /* Check if the tag is in the canonical form. */ + + if !cache(parser, 2) { + return false + } + + var handle []byte + var suffix []byte + if parser.buffer[parser.buffer_pos+1] == '<' { + /* Set the handle to '' */ + + /* Eat '!<' */ + + skip(parser) + skip(parser) + + /* Consume the tag value. */ + + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + + /* Check for '>' and eat it. */ + + if parser.buffer[parser.buffer_pos] != '>' { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find the expected '>'") + return false + } + + skip(parser) + } else { + /* The tag has either the '!suffix' or the '!handle!suffix' form. */ + + /* First, try to scan a handle. */ + + if !yaml_parser_scan_tag_handle(parser, false, start_mark, &handle) { + return false + } + + /* Check if it is, indeed, handle. */ + + if handle[0] == '!' && len(handle) > 0 && handle[len(handle)-1] == '!' { + /* Scan the suffix now. */ + + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + } else { + /* It wasn't a handle after all. Scan the rest of the tag. */ + + if !yaml_parser_scan_tag_uri(parser, false, handle, start_mark, &suffix) { + return false + } + + /* Set the handle to '!'. */ + + handle = []byte{'!'} + + /* + * A special case: the '!' tag. Set the handle to '' and the + * suffix to '!'. + */ + + if len(suffix) == 0 { + handle, suffix = suffix, handle + } + + } + } + + /* Check the character which ends the tag. */ + + if !cache(parser, 1) { + return false + } + + if !is_blankz_at(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find expected whitespace or line break") + return false + } + + end_mark := parser.mark + + /* Create a token. */ + + *token = yaml_token_t{ + token_type: yaml_TAG_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + suffix: suffix, + } + + return true +} + +/* + * Scan a tag handle. + */ + +func yaml_parser_scan_tag_handle(parser *yaml_parser_t, directive bool, + start_mark YAML_mark_t, handle *[]byte) bool { + + /* Check the initial '!' character. */ + + if !cache(parser, 1) { + return false + } + + if parser.buffer[parser.buffer_pos] != '!' { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + + /* Copy the '!' character. */ + var s []byte + s = read(parser, s) + + /* Copy all subsequent alphabetical and numerical characters. */ + + if !cache(parser, 1) { + return false + } + + for is_alpha(parser.buffer[parser.buffer_pos]) { + s = read(parser, s) + if !cache(parser, 1) { + return false + } + } + + /* Check if the trailing character is '!' and copy it. */ + + if parser.buffer[parser.buffer_pos] == '!' { + s = read(parser, s) + } else { + /* + * It's either the '!' tag or not really a tag handle. If it's a %TAG + * directive, it's an error. If it's a tag token, it must be a part of + * URI. + */ + + if directive && !(s[0] == '!' && len(s) == 1) { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + } + + *handle = s + + return true +} + +/* + * Scan a tag. + */ + +func yaml_parser_scan_tag_uri(parser *yaml_parser_t, directive bool, + head []byte, start_mark YAML_mark_t, uri *[]byte) bool { + + var s []byte + /* + * Copy the head if needed. + * + * Note that we don't copy the leading '!' character. + */ + if len(head) > 1 { + s = append(s, head[1:]...) + } + + /* Scan the tag. */ + if !cache(parser, 1) { + return false + } + + /* + * The set of characters that may appear in URI is as follows: + * + * '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', + * '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']', + * '%'. + */ + + b := parser.buffer[parser.buffer_pos] + for is_alpha(b) || b == ';' || + b == '/' || b == '?' || + b == ':' || b == '@' || + b == '&' || b == '=' || + b == '+' || b == '$' || + b == ',' || b == '.' || + b == '!' || b == '~' || + b == '*' || b == '\'' || + b == '(' || b == ')' || + b == '[' || b == ']' || + b == '%' { + /* Check if it is a URI-escape sequence. */ + + if b == '%' { + if !yaml_parser_scan_uri_escapes(parser, + directive, start_mark, &s) { + return false + } + } else { + s = read(parser, s) + } + + if !cache(parser, 1) { + return false + } + b = parser.buffer[parser.buffer_pos] + } + + /* Check if the tag is non-empty. */ + + if len(s) == 0 { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected tag URI") + return false + } + + *uri = s + + return true +} + +/* + * Decode an URI-escape sequence corresponding to a single UTF-8 character. + */ + +func yaml_parser_scan_uri_escapes(parser *yaml_parser_t, directive bool, + start_mark YAML_mark_t, s *[]byte) bool { + + /* Decode the required number of characters. */ + w := 10 + for w > 0 { + + /* Check for a URI-escaped octet. */ + + if !cache(parser, 3) { + return false + } + + if !(parser.buffer[parser.buffer_pos] == '%' && + is_hex(parser.buffer[parser.buffer_pos+1]) && + is_hex(parser.buffer[parser.buffer_pos+2])) { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find URI escaped octet") + } + + /* Get the octet. */ + octet := byte((as_hex(parser.buffer[parser.buffer_pos+1]) << 4) + + as_hex(parser.buffer[parser.buffer_pos+2])) + + /* If it is the leading octet, determine the length of the UTF-8 sequence. */ + + if w == 10 { + w = width(octet) + if w == 0 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect leading UTF-8 octet") + } + } else { + /* Check if the trailing octet is correct. */ + + if (octet & 0xC0) != 0x80 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect trailing UTF-8 octet") + } + } + + /* Copy the octet and move the pointers. */ + + *s = append(*s, octet) + skip(parser) + skip(parser) + skip(parser) + w-- + } + + return true +} + +/* + * Scan a block scalar. + */ + +func yaml_parser_scan_block_scalar(parser *yaml_parser_t, token *yaml_token_t, + literal bool) bool { + + /* Eat the indicator '|' or '>'. */ + + start_mark := parser.mark + + skip(parser) + + /* Scan the additional block scalar indicators. */ + + if !cache(parser, 1) { + return false + } + + /* Check for a chomping indicator. */ + chomping := 0 + increment := 0 + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + /* Set the chomping method and eat the indicator. */ + + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + + skip(parser) + + /* Check for an indentation indicator. */ + + if !cache(parser, 1) { + return false + } + + if is_digit(parser.buffer[parser.buffer_pos]) { + /* Check that the intendation is greater than 0. */ + + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an intendation indicator equal to 0") + return false + } + + /* Get the intendation level and eat the indicator. */ + + increment = as_digit(parser.buffer[parser.buffer_pos]) + + skip(parser) + } + } else if is_digit(parser.buffer[parser.buffer_pos]) { + + /* Do the same as above, but in the opposite order. */ + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an intendation indicator equal to 0") + return false + } + + increment = as_digit(parser.buffer[parser.buffer_pos]) + + skip(parser) + + if !cache(parser, 1) { + return false + } + + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + + skip(parser) + } + } + + /* Eat whitespaces and comments to the end of the line. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + + if parser.buffer[parser.buffer_pos] == '#' { + for !is_breakz_at(parser.buffer, parser.buffer_pos) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + } + + /* Check if we are at the end of the line. */ + + if !is_breakz_at(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "did not find expected comment or line break") + return false + } + + /* Eat a line break. */ + + if is_break_at(parser.buffer, parser.buffer_pos) { + if !cache(parser, 2) { + return false + } + + skip_line(parser) + } + + end_mark := parser.mark + + /* Set the intendation level if it was specified. */ + indent := 0 + if increment > 0 { + if parser.indent >= 0 { + indent = parser.indent + increment + } else { + indent = increment + } + } + + /* Scan the leading line breaks and determine the indentation level if needed. */ + var trailing_breaks []byte + if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, + start_mark, &end_mark) { + return false + } + + /* Scan the block scalar content. */ + + if !cache(parser, 1) { + return false + } + + var s []byte + var leading_break []byte + leading_blank := false + trailing_blank := false + for parser.mark.column == indent && !is_z(parser.buffer[parser.buffer_pos]) { + + /* + * We are at the beginning of a non-empty line. + */ + + /* Is it a trailing whitespace? */ + + trailing_blank = is_blank(parser.buffer[parser.buffer_pos]) + + /* Check if we need to fold the leading line break. */ + + if !literal && len(leading_break) > 0 && leading_break[0] == '\n' && + !leading_blank && !trailing_blank { + /* Do we need to join the lines by space? */ + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } + leading_break = leading_break[:0] + } else { + s = append(s, leading_break...) + leading_break = leading_break[:0] + } + + /* Append the remaining line breaks. */ + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] + + /* Is it a leading whitespace? */ + + leading_blank = is_blank(parser.buffer[parser.buffer_pos]) + + /* Consume the current line. */ + + for !is_breakz_at(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if !cache(parser, 1) { + return false + } + } + + /* Consume the line break. */ + + if !cache(parser, 2) { + return false + } + + leading_break = read_line(parser, leading_break) + + /* Eat the following intendation spaces and line breaks. */ + + if !yaml_parser_scan_block_scalar_breaks(parser, + &indent, &trailing_breaks, start_mark, &end_mark) { + return false + } + } + + /* Chomp the tail. */ + + if chomping != -1 { + s = append(s, leading_break...) + } + if chomping == 1 { + s = append(s, trailing_breaks...) + } + + /* Create a token. */ + + *token = yaml_token_t{ + token_type: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_LITERAL_SCALAR_STYLE, + } + if !literal { + token.style = yaml_FOLDED_SCALAR_STYLE + } + + return true +} + +/* + * Scan intendation spaces and line breaks for a block scalar. Determine the + * intendation level if needed. + */ + +func yaml_parser_scan_block_scalar_breaks(parser *yaml_parser_t, + indent *int, breaks *[]byte, + start_mark YAML_mark_t, end_mark *YAML_mark_t) bool { + + *end_mark = parser.mark + + /* Eat the intendation spaces and line breaks. */ + max_indent := 0 + for { + /* Eat the intendation spaces. */ + + if !cache(parser, 1) { + return false + } + + for (*indent == 0 || parser.mark.column < *indent) && + is_space(parser.buffer[parser.buffer_pos]) { + skip(parser) + if !cache(parser, 1) { + return false + } + } + if parser.mark.column > max_indent { + max_indent = parser.mark.column + } + + /* Check for a tab character messing the intendation. */ + + if (*indent == 0 || parser.mark.column < *indent) && + is_tab(parser.buffer[parser.buffer_pos]) { + return yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found a tab character where an intendation space is expected") + } + + /* Have we found a non-empty line? */ + + if !is_break_at(parser.buffer, parser.buffer_pos) { + break + } + + /* Consume the line break. */ + + if !cache(parser, 2) { + return false + } + + *breaks = read_line(parser, *breaks) + *end_mark = parser.mark + } + + /* Determine the indentation level if needed. */ + + if *indent == 0 { + *indent = max_indent + if *indent < parser.indent+1 { + *indent = parser.indent + 1 + } + if *indent < 1 { + *indent = 1 + } + } + + return true +} + +/* + * Scan a quoted scalar. + */ + +func yaml_parser_scan_flow_scalar(parser *yaml_parser_t, token *yaml_token_t, + single bool) bool { + + /* Eat the left quote. */ + + start_mark := parser.mark + + skip(parser) + + /* Consume the content of the quoted scalar. */ + var s []byte + var leading_break []byte + var trailing_breaks []byte + var whitespaces []byte + for { + /* Check that there are no document indicators at the beginning of the line. */ + + if !cache(parser, 4) { + return false + } + + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz_at(parser.buffer, parser.buffer_pos+3) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected document indicator") + return false + } + + /* Check for EOF. */ + + if is_z(parser.buffer[parser.buffer_pos]) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected end of stream") + return false + } + + /* Consume non-blank characters. */ + + if !cache(parser, 2) { + return false + } + + leading_blanks := false + + for !is_blankz_at(parser.buffer, parser.buffer_pos) { + /* Check for an escaped single quote. */ + + if single && parser.buffer[parser.buffer_pos] == '\'' && + parser.buffer[parser.buffer_pos+1] == '\'' { + // Is is an escaped single quote. + s = append(s, '\'') + skip(parser) + skip(parser) + } else if single && parser.buffer[parser.buffer_pos] == '\'' { + /* Check for the right quote. */ + break + } else if !single && parser.buffer[parser.buffer_pos] == '"' { + /* Check for the right quote. */ + break + } else if !single && parser.buffer[parser.buffer_pos] == '\\' && + is_break_at(parser.buffer, parser.buffer_pos+1) { + + /* Check for an escaped line break. */ + if !cache(parser, 3) { + return false + } + + skip(parser) + skip_line(parser) + leading_blanks = true + break + } else if !single && parser.buffer[parser.buffer_pos] == '\\' { + + /* Check for an escape sequence. */ + + code_length := 0 + + /* Check the escape character. */ + + switch parser.buffer[parser.buffer_pos+1] { + case '0': + s = append(s, 0) + case 'a': + s = append(s, '\x07') + case 'b': + s = append(s, '\x08') + case 't', '\t': + s = append(s, '\x09') + case 'n': + s = append(s, '\x0A') + case 'v': + s = append(s, '\x0B') + case 'f': + s = append(s, '\x0C') + case 'r': + s = append(s, '\x0D') + case 'e': + s = append(s, '\x1B') + case ' ': + s = append(s, '\x20') + case '"': + s = append(s, '"') + case '\'': + s = append(s, '\'') + case '\\': + s = append(s, '\\') + case 'N': /* NEL (#x85) */ + s = append(s, '\xC2') + s = append(s, '\x85') + case '_': /* #xA0 */ + s = append(s, '\xC2') + s = append(s, '\xA0') + case 'L': /* LS (#x2028) */ + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA8') + case 'P': /* PS (#x2029) */ + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA9') + case 'x': + code_length = 2 + case 'u': + code_length = 4 + case 'U': + code_length = 8 + default: + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found unknown escape character") + return false + } + + skip(parser) + skip(parser) + + /* Consume an arbitrary escape code. */ + + if code_length > 0 { + value := 0 + + /* Scan the character value. */ + + if !cache(parser, code_length) { + return false + } + + for k := 0; k < code_length; k++ { + if !is_hex(parser.buffer[parser.buffer_pos+k]) { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "did not find expected hexdecimal number") + return false + } + value = (value << 4) + as_hex(parser.buffer[parser.buffer_pos+k]) + } + + /* Check the value and write the character. */ + + if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found invalid Unicode character escape code") + return false + } + + if value <= 0x7F { + s = append(s, byte(value)) + } else if value <= 0x7FF { + s = append(s, byte(0xC0+(value>>6))) + s = append(s, byte(0x80+(value&0x3F))) + } else if value <= 0xFFFF { + s = append(s, byte(0xE0+(value>>12))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } else { + s = append(s, byte(0xF0+(value>>18))) + s = append(s, byte(0x80+((value>>12)&0x3F))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } + + /* Advance the pointer. */ + + for k := 0; k < code_length; k++ { + skip(parser) + } + } + } else { + /* It is a non-escaped non-blank character. */ + + s = read(parser, s) + } + + if !cache(parser, 2) { + return false + } + } + + /* Check if we are at the end of the scalar. */ + b := parser.buffer[parser.buffer_pos] + if single { + if b == '\'' { + break + } + } else if b == '"' { + break + } + + /* Consume blank characters. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) || is_break_at(parser.buffer, parser.buffer_pos) { + if is_blank(parser.buffer[parser.buffer_pos]) { + /* Consume a space or a tab character. */ + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if !cache(parser, 2) { + return false + } + + /* Check if it is a first line break. */ + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + + if !cache(parser, 1) { + return false + } + } + + /* Join the whitespaces or fold line breaks. */ + + if leading_blanks { + /* Do we need to fold line breaks? */ + + if len(leading_break) > 0 && leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] + } + + leading_break = leading_break[:0] + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + leading_break = leading_break[:0] + trailing_breaks = trailing_breaks[:0] + } + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + /* Eat the right quote. */ + + skip(parser) + + end_mark := parser.mark + + /* Create a token. */ + + *token = yaml_token_t{ + token_type: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_SINGLE_QUOTED_SCALAR_STYLE, + } + if !single { + token.style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + return true +} + +/* + * Scan a plain scalar. + */ + +func yaml_parser_scan_plain_scalar(parser *yaml_parser_t, token *yaml_token_t) bool { + var s []byte + var leading_break []byte + var trailing_breaks []byte + var whitespaces []byte + leading_blanks := false + indent := parser.indent + 1 + + start_mark := parser.mark + end_mark := parser.mark + + /* Consume the content of the plain scalar. */ + + for { + /* Check for a document indicator. */ + + if !cache(parser, 4) { + return false + } + + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz_at(parser.buffer, parser.buffer_pos+3) { + break + } + + /* Check for a comment. */ + + if parser.buffer[parser.buffer_pos] == '#' { + break + } + + /* Consume non-blank characters. */ + + for !is_blankz_at(parser.buffer, parser.buffer_pos) { + /* Check for 'x:x' in the flow context. TODO: Fix the test "spec-08-13". */ + + if parser.flow_level > 0 && + parser.buffer[parser.buffer_pos] == ':' && + !is_blankz_at(parser.buffer, parser.buffer_pos+1) { + yaml_parser_set_scanner_error(parser, "while scanning a plain scalar", + start_mark, "found unexpected ':'") + return false + } + + /* Check for indicators that may end a plain scalar. */ + b := parser.buffer[parser.buffer_pos] + if (b == ':' && is_blankz_at(parser.buffer, parser.buffer_pos+1)) || + (parser.flow_level > 0 && + (b == ',' || b == ':' || + b == '?' || b == '[' || + b == ']' || b == '{' || + b == '}')) { + break + } + + /* Check if we need to join whitespaces and breaks. */ + + if leading_blanks || len(whitespaces) > 0 { + if leading_blanks { + /* Do we need to fold line breaks? */ + + if leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] + } + leading_break = leading_break[:0] + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + leading_break = leading_break[:0] + trailing_breaks = trailing_breaks[:0] + } + + leading_blanks = false + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + /* Copy the character. */ + + s = read(parser, s) + end_mark = parser.mark + + if !cache(parser, 2) { + return false + } + } + + /* Is it the end? */ + + if !(is_blank(parser.buffer[parser.buffer_pos]) || + is_break_at(parser.buffer, parser.buffer_pos)) { + break + } + + /* Consume blank characters. */ + + if !cache(parser, 1) { + return false + } + + for is_blank(parser.buffer[parser.buffer_pos]) || + is_break_at(parser.buffer, parser.buffer_pos) { + + if is_blank(parser.buffer[parser.buffer_pos]) { + /* Check for tab character that abuse intendation. */ + + if leading_blanks && parser.mark.column < indent && + is_tab(parser.buffer[parser.buffer_pos]) { + yaml_parser_set_scanner_error(parser, "while scanning a plain scalar", + start_mark, "found a tab character that violate intendation") + return false + } + + /* Consume a space or a tab character. */ + + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if !cache(parser, 2) { + return false + } + + /* Check if it is a first line break. */ + + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + if !cache(parser, 1) { + return false + } + } + + /* Check intendation level. */ + + if parser.flow_level == 0 && parser.mark.column < indent { + break + } + } + + /* Create a token. */ + + *token = yaml_token_t{ + token_type: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_PLAIN_SCALAR_STYLE, + } + + /* Note that we change the 'simple_key_allowed' flag. */ + + if leading_blanks { + parser.simple_key_allowed = true + } + + return true +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner_test.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner_test.go new file mode 100644 index 00000000000..ee3b898a241 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/scanner_test.go @@ -0,0 +1,80 @@ +/* +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 candiedyaml + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var scan = func(filename string) { + It("scan "+filename, func() { + file, err := os.Open(filename) + Ω(err).To(BeNil()) + + parser := yaml_parser_t{} + yaml_parser_initialize(&parser) + yaml_parser_set_input_reader(&parser, file) + + failed := false + token := yaml_token_t{} + + for { + if !yaml_parser_scan(&parser, &token) { + failed = true + break + } + + if token.token_type == yaml_STREAM_END_TOKEN { + break + } + } + + file.Close() + + // msg := "SUCCESS" + // if failed { + // msg = "FAILED" + // if parser.error != yaml_NO_ERROR { + // m := parser.problem_mark + // fmt.Printf("ERROR: (%s) %s @ line: %d col: %d\n", + // parser.context, parser.problem, m.line, m.column) + // } + // } + Ω(failed).To(BeFalse()) + }) +} + +var scanYamls = func(dirname string) { + fileInfos, err := ioutil.ReadDir(dirname) + if err != nil { + panic(err.Error()) + } + + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + scan(filepath.Join(dirname, fileInfo.Name())) + } + } +} + +var _ = Describe("Scanner", func() { + scanYamls("fixtures/specification") + scanYamls("fixtures/specification/types") +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/tags.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/tags.go new file mode 100644 index 00000000000..4df0b0a7cee --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/tags.go @@ -0,0 +1,343 @@ +/* +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 candiedyaml + +import ( + "reflect" + "sort" + "strings" + "sync" + "unicode" +) + +// A field represents a single field found in a struct. +type field struct { + name string + tag bool + index []int + typ reflect.Type + omitEmpty bool + flow bool +} + +// byName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from json tag", then +// breaking ties with index sequence. +type byName []field + +func (x byName) Len() int { return len(x) } + +func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byName) Less(i, j int) bool { + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) + } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) +} + +// byIndex sorts field by index sequence. +type byIndex []field + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } + } + return len(x[i].index) < len(x[j].index) +} + +// typeFields returns a list of fields that JSON should recognize for the given type. +// The algorithm is breadth-first search over the set of structs to include - the top struct +// and then any reachable anonymous structs. +func typeFields(t reflect.Type) []field { + // Anonymous fields to explore at the current level and the next. + current := []field{} + next := []field{{typ: t}} + + // Count of queued names for current level and the next. + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + // Types already visited at an earlier level. + visited := map[reflect.Type]bool{} + + // Fields found. + var fields []field + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if visited[f.typ] { + continue + } + visited[f.typ] = true + + // Scan f.typ for fields to include. + for i := 0; i < f.typ.NumField(); i++ { + sf := f.typ.Field(i) + if sf.PkgPath != "" { // unexported + continue + } + tag := sf.Tag.Get("yaml") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if !isValidTag(name) { + name = "" + } + index := make([]int, len(f.index)+1) + copy(index, f.index) + index[len(f.index)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // Record found field and index sequence. + if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { + tagged := name != "" + if name == "" { + name = sf.Name + } + fields = append(fields, field{name, tagged, index, ft, + opts.Contains("omitempty"), opts.Contains("flow")}) + if count[f.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, fields[len(fields)-1]) + } + continue + } + + // Record new anonymous struct to explore in next round. + nextCount[ft]++ + if nextCount[ft] == 1 { + next = append(next, field{name: ft.Name(), index: index, typ: ft}) + } + } + } + } + + sort.Sort(byName(fields)) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with JSON tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(byIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// JSON tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.index) > length { + fields = fields[:i] + break + } + if f.tag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +var fieldCache struct { + sync.RWMutex + m map[reflect.Type][]field +} + +// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. +func cachedTypeFields(t reflect.Type) []field { + fieldCache.RLock() + f := fieldCache.m[t] + fieldCache.RUnlock() + if f != nil { + return f + } + + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = typeFields(t) + if f == nil { + f = []field{} + } + + fieldCache.Lock() + if fieldCache.m == nil { + fieldCache.m = map[reflect.Type][]field{} + } + fieldCache.m[t] = f + fieldCache.Unlock() + return f +} + +// tagOptions is the string following a comma in a struct field's "json" +// tag, or the empty string. It does not include the leading comma. +type tagOptions string + +func isValidTag(s string) bool { + if s == "" { + return false + } + for _, c := range s { + switch { + case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): + // Backslash and quote chars are reserved, but + // otherwise any punctuation chars are allowed + // in a tag name. + default: + if !unicode.IsLetter(c) && !unicode.IsDigit(c) { + return false + } + } + } + return true +} + +func fieldByIndex(v reflect.Value, index []int) reflect.Value { + for _, i := range index { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + v = v.Field(i) + } + return v +} + +func typeByIndex(t reflect.Type, index []int) reflect.Type { + for _, i := range index { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + t = t.Field(i).Type + } + return t +} + +// stringValues is a slice of reflect.Value holding *reflect.StringValue. +// It implements the methods to sort by string. +type stringValues []reflect.Value + +func (sv stringValues) Len() int { return len(sv) } +func (sv stringValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] } +func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) } +func (sv stringValues) get(i int) string { return sv[i].String() } + +// parseTag splits a struct field's json tag into its name and +// comma-separated options. +func parseTag(tag string) (string, tagOptions) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tagOptions(tag[idx+1:]) + } + return tag, tagOptions("") +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o tagOptions) Contains(optionName string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var next string + i := strings.Index(s, ",") + if i >= 0 { + s, next = s[:i], s[i+1:] + } + if s == optionName { + return true + } + s = next + } + return false +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/writer.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/writer.go new file mode 100644 index 00000000000..a76b6336387 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/writer.go @@ -0,0 +1,128 @@ +/* +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 candiedyaml + +/* + * Set the writer error and return 0. + */ + +func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_WRITER_ERROR + emitter.problem = problem + + return false +} + +/* + * Flush the output buffer. + */ + +func yaml_emitter_flush(emitter *yaml_emitter_t) bool { + if emitter.write_handler == nil { + panic("Write handler must be set") /* Write handler must be set. */ + } + if emitter.encoding == yaml_ANY_ENCODING { + panic("Encoding must be set") /* Output encoding must be set. */ + } + + /* Check if the buffer is empty. */ + + if emitter.buffer_pos == 0 { + return true + } + + /* If the output encoding is UTF-8, we don't need to recode the buffer. */ + + if emitter.encoding == yaml_UTF8_ENCODING { + if err := emitter.write_handler(emitter, + emitter.buffer[:emitter.buffer_pos]); err != nil { + return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) + } + emitter.buffer_pos = 0 + return true + } + + /* Recode the buffer into the raw buffer. */ + + var low, high int + if emitter.encoding == yaml_UTF16LE_ENCODING { + low, high = 0, 1 + } else { + high, low = 1, 0 + } + + pos := 0 + for pos < emitter.buffer_pos { + + /* + * See the "reader.c" code for more details on UTF-8 encoding. Note + * that we assume that the buffer contains a valid UTF-8 sequence. + */ + + /* Read the next UTF-8 character. */ + + octet := emitter.buffer[pos] + + var w int + var value rune + switch { + case octet&0x80 == 0x00: + w, value = 1, rune(octet&0x7F) + case octet&0xE0 == 0xC0: + w, value = 2, rune(octet&0x1F) + case octet&0xF0 == 0xE0: + w, value = 3, rune(octet&0x0F) + case octet&0xF8 == 0xF0: + w, value = 4, rune(octet&0x07) + } + + for k := 1; k < w; k++ { + octet = emitter.buffer[pos+k] + value = (value << 6) + (rune(octet) & 0x3F) + } + + pos += w + + /* Write the character. */ + + if value < 0x10000 { + var b [2]byte + b[high] = byte(value >> 8) + b[low] = byte(value & 0xFF) + emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1]) + } else { + /* Write the character using a surrogate pair (check "reader.c"). */ + + var b [4]byte + value -= 0x10000 + b[high] = byte(0xD8 + (value >> 18)) + b[low] = byte((value >> 10) & 0xFF) + b[high+2] = byte(0xDC + ((value >> 8) & 0xFF)) + b[low+2] = byte(value & 0xFF) + emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1], b[2], b[3]) + } + } + + /* Write the raw buffer. */ + + // Write the raw buffer. + if err := emitter.write_handler(emitter, emitter.raw_buffer); err != nil { + return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) + } + + emitter.buffer_pos = 0 + emitter.raw_buffer = emitter.raw_buffer[:0] + return true +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_definesh.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_definesh.go new file mode 100644 index 00000000000..de4c05ad81a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_definesh.go @@ -0,0 +1,22 @@ +/* +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 candiedyaml + +const ( + yaml_VERSION_MAJOR = 0 + yaml_VERSION_MINOR = 1 + yaml_VERSION_PATCH = 6 + yaml_VERSION_STRING = "0.1.6" +) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_privateh.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_privateh.go new file mode 100644 index 00000000000..2b3b7d7496b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yaml_privateh.go @@ -0,0 +1,891 @@ +/* +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 candiedyaml + +const ( + INPUT_RAW_BUFFER_SIZE = 1024 + + /* + * The size of the input buffer. + * + * It should be possible to decode the whole raw buffer. + */ + INPUT_BUFFER_SIZE = (INPUT_RAW_BUFFER_SIZE * 3) + + /* + * The size of the output buffer. + */ + + OUTPUT_BUFFER_SIZE = 512 + + /* + * The size of the output raw buffer. + * + * It should be possible to encode the whole output buffer. + */ + + OUTPUT_RAW_BUFFER_SIZE = (OUTPUT_BUFFER_SIZE*2 + 2) + + INITIAL_STACK_SIZE = 16 + INITIAL_QUEUE_SIZE = 16 +) + +func width(b byte) int { + if b&0x80 == 0 { + return 1 + } + + if b&0xE0 == 0xC0 { + return 2 + } + + if b&0xF0 == 0xE0 { + return 3 + } + + if b&0xF8 == 0xF0 { + return 4 + } + + return 0 +} + +func copy_bytes(dest []byte, dest_pos *int, src []byte, src_pos *int) { + w := width(src[*src_pos]) + switch w { + case 4: + dest[*dest_pos+3] = src[*src_pos+3] + fallthrough + case 3: + dest[*dest_pos+2] = src[*src_pos+2] + fallthrough + case 2: + dest[*dest_pos+1] = src[*src_pos+1] + fallthrough + case 1: + dest[*dest_pos] = src[*src_pos] + default: + panic("invalid width") + } + *dest_pos += w + *src_pos += w +} + +// /* +// * Check if the character at the specified position is an alphabetical +// * character, a digit, '_', or '-'. +// */ + +func is_alpha(b byte) bool { + return (b >= '0' && b <= '9') || + (b >= 'A' && b <= 'Z') || + (b >= 'a' && b <= 'z') || + b == '_' || b == '-' +} + +// /* +// * Check if the character at the specified position is a digit. +// */ +// +func is_digit(b byte) bool { + return b >= '0' && b <= '9' +} + +// /* +// * Get the value of a digit. +// */ +// +func as_digit(b byte) int { + return int(b) - '0' +} + +// /* +// * Check if the character at the specified position is a hex-digit. +// */ +// +func is_hex(b byte) bool { + return (b >= '0' && b <= '9') || + (b >= 'A' && b <= 'F') || + (b >= 'a' && b <= 'f') +} + +// +// /* +// * Get the value of a hex-digit. +// */ +// +func as_hex(b byte) int { + if b >= 'A' && b <= 'F' { + return int(b) - 'A' + 10 + } else if b >= 'a' && b <= 'f' { + return int(b) - 'a' + 10 + } + return int(b) - '0' +} + +// #define AS_HEX_AT(string,offset) \ +// (((string).pointer[offset] >= (yaml_char_t) 'A' && \ +// (string).pointer[offset] <= (yaml_char_t) 'F') ? \ +// ((string).pointer[offset] - (yaml_char_t) 'A' + 10) : \ +// ((string).pointer[offset] >= (yaml_char_t) 'a' && \ +// (string).pointer[offset] <= (yaml_char_t) 'f') ? \ +// ((string).pointer[offset] - (yaml_char_t) 'a' + 10) : \ +// ((string).pointer[offset] - (yaml_char_t) '0')) + +// /* +// * Check if the character is a line break, space, tab, or NUL. +// */ +func is_blankz_at(b []byte, i int) bool { + return is_blank(b[i]) || is_breakz_at(b, i) +} + +// /* +// * Check if the character at the specified position is a line break. +// */ +func is_break_at(b []byte, i int) bool { + return b[i] == '\r' || /* CR (#xD)*/ + b[i] == '\n' || /* LF (#xA) */ + (b[i] == 0xC2 && b[i+1] == 0x85) || /* NEL (#x85) */ + (b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8) || /* LS (#x2028) */ + (b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) /* PS (#x2029) */ +} + +func is_breakz_at(b []byte, i int) bool { + return is_break_at(b, i) || is_z(b[i]) +} + +func is_crlf_at(b []byte, i int) bool { + return b[i] == '\r' && b[i+1] == '\n' +} + +// /* +// * Check if the character at the specified position is NUL. +// */ +func is_z(b byte) bool { + return b == 0x0 +} + +// /* +// * Check if the character at the specified position is space. +// */ +func is_space(b byte) bool { + return b == ' ' +} + +// +// /* +// * Check if the character at the specified position is tab. +// */ +func is_tab(b byte) bool { + return b == '\t' +} + +// /* +// * Check if the character at the specified position is blank (space or tab). +// */ +func is_blank(b byte) bool { + return is_space(b) || is_tab(b) +} + +// /* +// * Check if the character is ASCII. +// */ +func is_ascii(b byte) bool { + return b <= '\x7f' +} + +// /* +// * Check if the character can be printed unescaped. +// */ +func is_printable_at(b []byte, i int) bool { + return ((b[i] == 0x0A) || /* . == #x0A */ + (b[i] >= 0x20 && b[i] <= 0x7E) || /* #x20 <= . <= #x7E */ + (b[i] == 0xC2 && b[i+1] >= 0xA0) || /* #0xA0 <= . <= #xD7FF */ + (b[i] > 0xC2 && b[i] < 0xED) || + (b[i] == 0xED && b[i+1] < 0xA0) || + (b[i] == 0xEE) || + (b[i] == 0xEF && /* && . != #xFEFF */ + !(b[i+1] == 0xBB && b[i+2] == 0xBF) && + !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) +} + +func insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) { + // collapse the slice + if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { + if parser.tokens_head != len(parser.tokens) { + // move the tokens down + copy(parser.tokens, parser.tokens[parser.tokens_head:]) + } + // readjust the length + parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] + parser.tokens_head = 0 + } + + parser.tokens = append(parser.tokens, *token) + if pos < 0 { + return + } + copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) + parser.tokens[parser.tokens_head+pos] = *token +} + +// /* +// * Check if the character at the specified position is BOM. +// */ +// +func is_bom_at(b []byte, i int) bool { + return b[i] == 0xEF && b[i+1] == 0xBB && b[i+2] == 0xBF +} + +// +// #ifdef HAVE_CONFIG_H +// #include +// #endif +// +// #include "./yaml.h" +// +// #include +// #include +// +// /* +// * Memory management. +// */ +// +// yaml_DECLARE(void *) +// yaml_malloc(size_t size); +// +// yaml_DECLARE(void *) +// yaml_realloc(void *ptr, size_t size); +// +// yaml_DECLARE(void) +// yaml_free(void *ptr); +// +// yaml_DECLARE(yaml_char_t *) +// yaml_strdup(const yaml_char_t *); +// +// /* +// * Reader: Ensure that the buffer contains at least `length` characters. +// */ +// +// yaml_DECLARE(int) +// yaml_parser_update_buffer(yaml_parser_t *parser, size_t length); +// +// /* +// * Scanner: Ensure that the token stack contains at least one token ready. +// */ +// +// yaml_DECLARE(int) +// yaml_parser_fetch_more_tokens(yaml_parser_t *parser); +// +// /* +// * The size of the input raw buffer. +// */ +// +// #define INPUT_RAW_BUFFER_SIZE 16384 +// +// /* +// * The size of the input buffer. +// * +// * It should be possible to decode the whole raw buffer. +// */ +// +// #define INPUT_BUFFER_SIZE (INPUT_RAW_BUFFER_SIZE*3) +// +// /* +// * The size of the output buffer. +// */ +// +// #define OUTPUT_BUFFER_SIZE 16384 +// +// /* +// * The size of the output raw buffer. +// * +// * It should be possible to encode the whole output buffer. +// */ +// +// #define OUTPUT_RAW_BUFFER_SIZE (OUTPUT_BUFFER_SIZE*2+2) +// +// /* +// * The size of other stacks and queues. +// */ +// +// #define INITIAL_STACK_SIZE 16 +// #define INITIAL_QUEUE_SIZE 16 +// #define INITIAL_STRING_SIZE 16 +// +// /* +// * Buffer management. +// */ +// +// #define BUFFER_INIT(context,buffer,size) \ +// (((buffer).start = yaml_malloc(size)) ? \ +// ((buffer).last = (buffer).pointer = (buffer).start, \ +// (buffer).end = (buffer).start+(size), \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define BUFFER_DEL(context,buffer) \ +// (yaml_free((buffer).start), \ +// (buffer).start = (buffer).pointer = (buffer).end = 0) +// +// /* +// * String management. +// */ +// +// typedef struct { +// yaml_char_t *start; +// yaml_char_t *end; +// yaml_char_t *pointer; +// } yaml_string_t; +// +// yaml_DECLARE(int) +// yaml_string_extend(yaml_char_t **start, +// yaml_char_t **pointer, yaml_char_t **end); +// +// yaml_DECLARE(int) +// yaml_string_join( +// yaml_char_t **a_start, yaml_char_t **a_pointer, yaml_char_t **a_end, +// yaml_char_t **b_start, yaml_char_t **b_pointer, yaml_char_t **b_end); +// +// #define NULL_STRING { NULL, NULL, NULL } +// +// #define STRING(string,length) { (string), (string)+(length), (string) } +// +// #define STRING_ASSIGN(value,string,length) \ +// ((value).start = (string), \ +// (value).end = (string)+(length), \ +// (value).pointer = (string)) +// +// #define STRING_INIT(context,string,size) \ +// (((string).start = yaml_malloc(size)) ? \ +// ((string).pointer = (string).start, \ +// (string).end = (string).start+(size), \ +// memset((string).start, 0, (size)), \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define STRING_DEL(context,string) \ +// (yaml_free((string).start), \ +// (string).start = (string).pointer = (string).end = 0) +// +// #define STRING_EXTEND(context,string) \ +// (((string).pointer+5 < (string).end) \ +// || yaml_string_extend(&(string).start, \ +// &(string).pointer, &(string).end)) +// +// #define CLEAR(context,string) \ +// ((string).pointer = (string).start, \ +// memset((string).start, 0, (string).end-(string).start)) +// +// #define JOIN(context,string_a,string_b) \ +// ((yaml_string_join(&(string_a).start, &(string_a).pointer, \ +// &(string_a).end, &(string_b).start, \ +// &(string_b).pointer, &(string_b).end)) ? \ +// ((string_b).pointer = (string_b).start, \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// /* +// * String check operations. +// */ +// +// /* +// * Check the octet at the specified position. +// */ +// +// #define CHECK_AT(string,octet,offset) \ +// ((string).pointer[offset] == (yaml_char_t)(octet)) +// +// /* +// * Check the current octet in the buffer. +// */ +// +// #define CHECK(string,octet) CHECK_AT((string),(octet),0) +// +// /* +// * Check if the character at the specified position is an alphabetical +// * character, a digit, '_', or '-'. +// */ +// +// #define IS_ALPHA_AT(string,offset) \ +// (((string).pointer[offset] >= (yaml_char_t) '0' && \ +// (string).pointer[offset] <= (yaml_char_t) '9') || \ +// ((string).pointer[offset] >= (yaml_char_t) 'A' && \ +// (string).pointer[offset] <= (yaml_char_t) 'Z') || \ +// ((string).pointer[offset] >= (yaml_char_t) 'a' && \ +// (string).pointer[offset] <= (yaml_char_t) 'z') || \ +// (string).pointer[offset] == '_' || \ +// (string).pointer[offset] == '-') +// +// #define IS_ALPHA(string) IS_ALPHA_AT((string),0) +// +// /* +// * Check if the character at the specified position is a digit. +// */ +// +// #define IS_DIGIT_AT(string,offset) \ +// (((string).pointer[offset] >= (yaml_char_t) '0' && \ +// (string).pointer[offset] <= (yaml_char_t) '9')) +// +// #define IS_DIGIT(string) IS_DIGIT_AT((string),0) +// +// /* +// * Get the value of a digit. +// */ +// +// #define AS_DIGIT_AT(string,offset) \ +// ((string).pointer[offset] - (yaml_char_t) '0') +// +// #define AS_DIGIT(string) AS_DIGIT_AT((string),0) +// +// /* +// * Check if the character at the specified position is a hex-digit. +// */ +// +// #define IS_HEX_AT(string,offset) \ +// (((string).pointer[offset] >= (yaml_char_t) '0' && \ +// (string).pointer[offset] <= (yaml_char_t) '9') || \ +// ((string).pointer[offset] >= (yaml_char_t) 'A' && \ +// (string).pointer[offset] <= (yaml_char_t) 'F') || \ +// ((string).pointer[offset] >= (yaml_char_t) 'a' && \ +// (string).pointer[offset] <= (yaml_char_t) 'f')) +// +// #define IS_HEX(string) IS_HEX_AT((string),0) +// +// /* +// * Get the value of a hex-digit. +// */ +// +// #define AS_HEX_AT(string,offset) \ +// (((string).pointer[offset] >= (yaml_char_t) 'A' && \ +// (string).pointer[offset] <= (yaml_char_t) 'F') ? \ +// ((string).pointer[offset] - (yaml_char_t) 'A' + 10) : \ +// ((string).pointer[offset] >= (yaml_char_t) 'a' && \ +// (string).pointer[offset] <= (yaml_char_t) 'f') ? \ +// ((string).pointer[offset] - (yaml_char_t) 'a' + 10) : \ +// ((string).pointer[offset] - (yaml_char_t) '0')) +// +// #define AS_HEX(string) AS_HEX_AT((string),0) +// +// /* +// * Check if the character is ASCII. +// */ +// +// #define IS_ASCII_AT(string,offset) \ +// ((string).pointer[offset] <= (yaml_char_t) '\x7F') +// +// #define IS_ASCII(string) IS_ASCII_AT((string),0) +// +// /* +// * Check if the character can be printed unescaped. +// */ +// +// #define IS_PRINTABLE_AT(string,offset) \ +// (((string).pointer[offset] == 0x0A) /* . == #x0A */ \ +// || ((string).pointer[offset] >= 0x20 /* #x20 <= . <= #x7E */ \ +// && (string).pointer[offset] <= 0x7E) \ +// || ((string).pointer[offset] == 0xC2 /* #0xA0 <= . <= #xD7FF */ \ +// && (string).pointer[offset+1] >= 0xA0) \ +// || ((string).pointer[offset] > 0xC2 \ +// && (string).pointer[offset] < 0xED) \ +// || ((string).pointer[offset] == 0xED \ +// && (string).pointer[offset+1] < 0xA0) \ +// || ((string).pointer[offset] == 0xEE) \ +// || ((string).pointer[offset] == 0xEF /* #xE000 <= . <= #xFFFD */ \ +// && !((string).pointer[offset+1] == 0xBB /* && . != #xFEFF */ \ +// && (string).pointer[offset+2] == 0xBF) \ +// && !((string).pointer[offset+1] == 0xBF \ +// && ((string).pointer[offset+2] == 0xBE \ +// || (string).pointer[offset+2] == 0xBF)))) +// +// #define IS_PRINTABLE(string) IS_PRINTABLE_AT((string),0) +// +// /* +// * Check if the character at the specified position is NUL. +// */ +// +// #define IS_Z_AT(string,offset) CHECK_AT((string),'\0',(offset)) +// +// #define IS_Z(string) IS_Z_AT((string),0) +// +// /* +// * Check if the character at the specified position is BOM. +// */ +// +// #define IS_BOM_AT(string,offset) \ +// (CHECK_AT((string),'\xEF',(offset)) \ +// && CHECK_AT((string),'\xBB',(offset)+1) \ +// && CHECK_AT((string),'\xBF',(offset)+2)) /* BOM (#xFEFF) */ +// +// #define IS_BOM(string) IS_BOM_AT(string,0) +// +// /* +// * Check if the character at the specified position is space. +// */ +// +// #define IS_SPACE_AT(string,offset) CHECK_AT((string),' ',(offset)) +// +// #define IS_SPACE(string) IS_SPACE_AT((string),0) +// +// /* +// * Check if the character at the specified position is tab. +// */ +// +// #define IS_TAB_AT(string,offset) CHECK_AT((string),'\t',(offset)) +// +// #define IS_TAB(string) IS_TAB_AT((string),0) +// +// /* +// * Check if the character at the specified position is blank (space or tab). +// */ +// +// #define IS_BLANK_AT(string,offset) \ +// (IS_SPACE_AT((string),(offset)) || IS_TAB_AT((string),(offset))) +// +// #define IS_BLANK(string) IS_BLANK_AT((string),0) +// +// /* +// * Check if the character at the specified position is a line break. +// */ +// +// #define IS_BREAK_AT(string,offset) \ +// (CHECK_AT((string),'\r',(offset)) /* CR (#xD)*/ \ +// || CHECK_AT((string),'\n',(offset)) /* LF (#xA) */ \ +// || (CHECK_AT((string),'\xC2',(offset)) \ +// && CHECK_AT((string),'\x85',(offset)+1)) /* NEL (#x85) */ \ +// || (CHECK_AT((string),'\xE2',(offset)) \ +// && CHECK_AT((string),'\x80',(offset)+1) \ +// && CHECK_AT((string),'\xA8',(offset)+2)) /* LS (#x2028) */ \ +// || (CHECK_AT((string),'\xE2',(offset)) \ +// && CHECK_AT((string),'\x80',(offset)+1) \ +// && CHECK_AT((string),'\xA9',(offset)+2))) /* PS (#x2029) */ +// +// #define IS_BREAK(string) IS_BREAK_AT((string),0) +// +// #define IS_CRLF_AT(string,offset) \ +// (CHECK_AT((string),'\r',(offset)) && CHECK_AT((string),'\n',(offset)+1)) +// +// #define IS_CRLF(string) IS_CRLF_AT((string),0) +// +// /* +// * Check if the character is a line break or NUL. +// */ +// +// #define IS_BREAKZ_AT(string,offset) \ +// (IS_BREAK_AT((string),(offset)) || IS_Z_AT((string),(offset))) +// +// #define IS_BREAKZ(string) IS_BREAKZ_AT((string),0) +// +// /* +// * Check if the character is a line break, space, or NUL. +// */ +// +// #define IS_SPACEZ_AT(string,offset) \ +// (IS_SPACE_AT((string),(offset)) || IS_BREAKZ_AT((string),(offset))) +// +// #define IS_SPACEZ(string) IS_SPACEZ_AT((string),0) +// +// /* +// * Check if the character is a line break, space, tab, or NUL. +// */ +// +// #define IS_BLANKZ_AT(string,offset) \ +// (IS_BLANK_AT((string),(offset)) || IS_BREAKZ_AT((string),(offset))) +// +// #define IS_BLANKZ(string) IS_BLANKZ_AT((string),0) +// +// /* +// * Determine the width of the character. +// */ +// +// #define WIDTH_AT(string,offset) \ +// (((string).pointer[offset] & 0x80) == 0x00 ? 1 : \ +// ((string).pointer[offset] & 0xE0) == 0xC0 ? 2 : \ +// ((string).pointer[offset] & 0xF0) == 0xE0 ? 3 : \ +// ((string).pointer[offset] & 0xF8) == 0xF0 ? 4 : 0) +// +// #define WIDTH(string) WIDTH_AT((string),0) +// +// /* +// * Move the string pointer to the next character. +// */ +// +// #define MOVE(string) ((string).pointer += WIDTH((string))) +// +// /* +// * Copy a character and move the pointers of both strings. +// */ +// +// #define COPY(string_a,string_b) \ +// ((*(string_b).pointer & 0x80) == 0x00 ? \ +// (*((string_a).pointer++) = *((string_b).pointer++)) : \ +// (*(string_b).pointer & 0xE0) == 0xC0 ? \ +// (*((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++)) : \ +// (*(string_b).pointer & 0xF0) == 0xE0 ? \ +// (*((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++)) : \ +// (*(string_b).pointer & 0xF8) == 0xF0 ? \ +// (*((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++), \ +// *((string_a).pointer++) = *((string_b).pointer++)) : 0) +// +// /* +// * Stack and queue management. +// */ +// +// yaml_DECLARE(int) +// yaml_stack_extend(void **start, void **top, void **end); +// +// yaml_DECLARE(int) +// yaml_queue_extend(void **start, void **head, void **tail, void **end); +// +// #define STACK_INIT(context,stack,size) \ +// (((stack).start = yaml_malloc((size)*sizeof(*(stack).start))) ? \ +// ((stack).top = (stack).start, \ +// (stack).end = (stack).start+(size), \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define STACK_DEL(context,stack) \ +// (yaml_free((stack).start), \ +// (stack).start = (stack).top = (stack).end = 0) +// +// #define STACK_EMPTY(context,stack) \ +// ((stack).start == (stack).top) +// +// #define PUSH(context,stack,value) \ +// (((stack).top != (stack).end \ +// || yaml_stack_extend((void **)&(stack).start, \ +// (void **)&(stack).top, (void **)&(stack).end)) ? \ +// (*((stack).top++) = value, \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define POP(context,stack) \ +// (*(--(stack).top)) +// +// #define QUEUE_INIT(context,queue,size) \ +// (((queue).start = yaml_malloc((size)*sizeof(*(queue).start))) ? \ +// ((queue).head = (queue).tail = (queue).start, \ +// (queue).end = (queue).start+(size), \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define QUEUE_DEL(context,queue) \ +// (yaml_free((queue).start), \ +// (queue).start = (queue).head = (queue).tail = (queue).end = 0) +// +// #define QUEUE_EMPTY(context,queue) \ +// ((queue).head == (queue).tail) +// +// #define ENQUEUE(context,queue,value) \ +// (((queue).tail != (queue).end \ +// || yaml_queue_extend((void **)&(queue).start, (void **)&(queue).head, \ +// (void **)&(queue).tail, (void **)&(queue).end)) ? \ +// (*((queue).tail++) = value, \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// #define DEQUEUE(context,queue) \ +// (*((queue).head++)) +// +// #define QUEUE_INSERT(context,queue,index,value) \ +// (((queue).tail != (queue).end \ +// || yaml_queue_extend((void **)&(queue).start, (void **)&(queue).head, \ +// (void **)&(queue).tail, (void **)&(queue).end)) ? \ +// (memmove((queue).head+(index)+1,(queue).head+(index), \ +// ((queue).tail-(queue).head-(index))*sizeof(*(queue).start)), \ +// *((queue).head+(index)) = value, \ +// (queue).tail++, \ +// 1) : \ +// ((context)->error = yaml_MEMORY_ERROR, \ +// 0)) +// +// /* +// * Token initializers. +// */ +// +// #define TOKEN_INIT(token,token_type,token_start_mark,token_end_mark) \ +// (memset(&(token), 0, sizeof(yaml_token_t)), \ +// (token).type = (token_type), \ +// (token).start_mark = (token_start_mark), \ +// (token).end_mark = (token_end_mark)) +// +// #define STREAM_START_TOKEN_INIT(token,token_encoding,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_STREAM_START_TOKEN,(start_mark),(end_mark)), \ +// (token).data.stream_start.encoding = (token_encoding)) +// +// #define STREAM_END_TOKEN_INIT(token,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_STREAM_END_TOKEN,(start_mark),(end_mark))) +// +// #define ALIAS_TOKEN_INIT(token,token_value,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_ALIAS_TOKEN,(start_mark),(end_mark)), \ +// (token).data.alias.value = (token_value)) +// +// #define ANCHOR_TOKEN_INIT(token,token_value,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_ANCHOR_TOKEN,(start_mark),(end_mark)), \ +// (token).data.anchor.value = (token_value)) +// +// #define TAG_TOKEN_INIT(token,token_handle,token_suffix,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_TAG_TOKEN,(start_mark),(end_mark)), \ +// (token).data.tag.handle = (token_handle), \ +// (token).data.tag.suffix = (token_suffix)) +// +// #define SCALAR_TOKEN_INIT(token,token_value,token_length,token_style,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_SCALAR_TOKEN,(start_mark),(end_mark)), \ +// (token).data.scalar.value = (token_value), \ +// (token).data.scalar.length = (token_length), \ +// (token).data.scalar.style = (token_style)) +// +// #define VERSION_DIRECTIVE_TOKEN_INIT(token,token_major,token_minor,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_VERSION_DIRECTIVE_TOKEN,(start_mark),(end_mark)), \ +// (token).data.version_directive.major = (token_major), \ +// (token).data.version_directive.minor = (token_minor)) +// +// #define TAG_DIRECTIVE_TOKEN_INIT(token,token_handle,token_prefix,start_mark,end_mark) \ +// (TOKEN_INIT((token),yaml_TAG_DIRECTIVE_TOKEN,(start_mark),(end_mark)), \ +// (token).data.tag_directive.handle = (token_handle), \ +// (token).data.tag_directive.prefix = (token_prefix)) +// +// /* +// * Event initializers. +// */ +// +// #define EVENT_INIT(event,event_type,event_start_mark,event_end_mark) \ +// (memset(&(event), 0, sizeof(yaml_event_t)), \ +// (event).type = (event_type), \ +// (event).start_mark = (event_start_mark), \ +// (event).end_mark = (event_end_mark)) +// +// #define STREAM_START_EVENT_INIT(event,event_encoding,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_STREAM_START_EVENT,(start_mark),(end_mark)), \ +// (event).data.stream_start.encoding = (event_encoding)) +// +// #define STREAM_END_EVENT_INIT(event,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_STREAM_END_EVENT,(start_mark),(end_mark))) +// +// #define DOCUMENT_START_EVENT_INIT(event,event_version_directive, \ +// event_tag_directives_start,event_tag_directives_end,event_implicit,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_DOCUMENT_START_EVENT,(start_mark),(end_mark)), \ +// (event).data.document_start.version_directive = (event_version_directive), \ +// (event).data.document_start.tag_directives.start = (event_tag_directives_start), \ +// (event).data.document_start.tag_directives.end = (event_tag_directives_end), \ +// (event).data.document_start.implicit = (event_implicit)) +// +// #define DOCUMENT_END_EVENT_INIT(event,event_implicit,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_DOCUMENT_END_EVENT,(start_mark),(end_mark)), \ +// (event).data.document_end.implicit = (event_implicit)) +// +// #define ALIAS_EVENT_INIT(event,event_anchor,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_ALIAS_EVENT,(start_mark),(end_mark)), \ +// (event).data.alias.anchor = (event_anchor)) +// +// #define SCALAR_EVENT_INIT(event,event_anchor,event_tag,event_value,event_length, \ +// event_plain_implicit, event_quoted_implicit,event_style,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_SCALAR_EVENT,(start_mark),(end_mark)), \ +// (event).data.scalar.anchor = (event_anchor), \ +// (event).data.scalar.tag = (event_tag), \ +// (event).data.scalar.value = (event_value), \ +// (event).data.scalar.length = (event_length), \ +// (event).data.scalar.plain_implicit = (event_plain_implicit), \ +// (event).data.scalar.quoted_implicit = (event_quoted_implicit), \ +// (event).data.scalar.style = (event_style)) +// +// #define SEQUENCE_START_EVENT_INIT(event,event_anchor,event_tag, \ +// event_implicit,event_style,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_SEQUENCE_START_EVENT,(start_mark),(end_mark)), \ +// (event).data.sequence_start.anchor = (event_anchor), \ +// (event).data.sequence_start.tag = (event_tag), \ +// (event).data.sequence_start.implicit = (event_implicit), \ +// (event).data.sequence_start.style = (event_style)) +// +// #define SEQUENCE_END_EVENT_INIT(event,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_SEQUENCE_END_EVENT,(start_mark),(end_mark))) +// +// #define MAPPING_START_EVENT_INIT(event,event_anchor,event_tag, \ +// event_implicit,event_style,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_MAPPING_START_EVENT,(start_mark),(end_mark)), \ +// (event).data.mapping_start.anchor = (event_anchor), \ +// (event).data.mapping_start.tag = (event_tag), \ +// (event).data.mapping_start.implicit = (event_implicit), \ +// (event).data.mapping_start.style = (event_style)) +// +// #define MAPPING_END_EVENT_INIT(event,start_mark,end_mark) \ +// (EVENT_INIT((event),yaml_MAPPING_END_EVENT,(start_mark),(end_mark))) +// +// /* +// * Document initializer. +// */ +// +// #define DOCUMENT_INIT(document,document_nodes_start,document_nodes_end, \ +// document_version_directive,document_tag_directives_start, \ +// document_tag_directives_end,document_start_implicit, \ +// document_end_implicit,document_start_mark,document_end_mark) \ +// (memset(&(document), 0, sizeof(yaml_document_t)), \ +// (document).nodes.start = (document_nodes_start), \ +// (document).nodes.end = (document_nodes_end), \ +// (document).nodes.top = (document_nodes_start), \ +// (document).version_directive = (document_version_directive), \ +// (document).tag_directives.start = (document_tag_directives_start), \ +// (document).tag_directives.end = (document_tag_directives_end), \ +// (document).start_implicit = (document_start_implicit), \ +// (document).end_implicit = (document_end_implicit), \ +// (document).start_mark = (document_start_mark), \ +// (document).end_mark = (document_end_mark)) +// +// /* +// * Node initializers. +// */ +// +// #define NODE_INIT(node,node_type,node_tag,node_start_mark,node_end_mark) \ +// (memset(&(node), 0, sizeof(yaml_node_t)), \ +// (node).type = (node_type), \ +// (node).tag = (node_tag), \ +// (node).start_mark = (node_start_mark), \ +// (node).end_mark = (node_end_mark)) +// +// #define SCALAR_NODE_INIT(node,node_tag,node_value,node_length, \ +// node_style,start_mark,end_mark) \ +// (NODE_INIT((node),yaml_SCALAR_NODE,(node_tag),(start_mark),(end_mark)), \ +// (node).data.scalar.value = (node_value), \ +// (node).data.scalar.length = (node_length), \ +// (node).data.scalar.style = (node_style)) +// +// #define SEQUENCE_NODE_INIT(node,node_tag,node_items_start,node_items_end, \ +// node_style,start_mark,end_mark) \ +// (NODE_INIT((node),yaml_SEQUENCE_NODE,(node_tag),(start_mark),(end_mark)), \ +// (node).data.sequence.items.start = (node_items_start), \ +// (node).data.sequence.items.end = (node_items_end), \ +// (node).data.sequence.items.top = (node_items_start), \ +// (node).data.sequence.style = (node_style)) +// +// #define MAPPING_NODE_INIT(node,node_tag,node_pairs_start,node_pairs_end, \ +// node_style,start_mark,end_mark) \ +// (NODE_INIT((node),yaml_MAPPING_NODE,(node_tag),(start_mark),(end_mark)), \ +// (node).data.mapping.pairs.start = (node_pairs_start), \ +// (node).data.mapping.pairs.end = (node_pairs_end), \ +// (node).data.mapping.pairs.top = (node_pairs_start), \ +// (node).data.mapping.style = (node_style)) +// diff --git a/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yamlh.go b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yamlh.go new file mode 100644 index 00000000000..d3f0da0b7d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry-incubator/candiedyaml/yamlh.go @@ -0,0 +1,946 @@ +/* +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 candiedyaml + +import ( + "io" +) + +/** The version directive data. */ +type yaml_version_directive_t struct { + major int // The major version number + minor int // The minor version number +} + +/** The tag directive data. */ +type yaml_tag_directive_t struct { + handle []byte // The tag handle + prefix []byte // The tag prefix +} + +/** The stream encoding. */ +type yaml_encoding_t int + +const ( + /** Let the parser choose the encoding. */ + yaml_ANY_ENCODING yaml_encoding_t = iota + /** The defau lt UTF-8 encoding. */ + yaml_UTF8_ENCODING + /** The UTF-16-LE encoding with BOM. */ + yaml_UTF16LE_ENCODING + /** The UTF-16-BE encoding with BOM. */ + yaml_UTF16BE_ENCODING +) + +/** Line break types. */ +type yaml_break_t int + +const ( + yaml_ANY_BREAK yaml_break_t = iota /** Let the parser choose the break type. */ + yaml_CR_BREAK /** Use CR for line breaks (Mac style). */ + yaml_LN_BREAK /** Use LN for line breaks (Unix style). */ + yaml_CRLN_BREAK /** Use CR LN for line breaks (DOS style). */ +) + +/** Many bad things could happen with the parser and emitter. */ +type YAML_error_type_t int + +const ( + /** No error is produced. */ + yaml_NO_ERROR YAML_error_type_t = iota + + /** Cannot allocate or reallocate a block of memory. */ + yaml_MEMORY_ERROR + + /** Cannot read or decode the input stream. */ + yaml_READER_ERROR + /** Cannot scan the input stream. */ + yaml_SCANNER_ERROR + /** Cannot parse the input stream. */ + yaml_PARSER_ERROR + /** Cannot compose a YAML document. */ + yaml_COMPOSER_ERROR + + /** Cannot write to the output stream. */ + yaml_WRITER_ERROR + /** Cannot emit a YAML stream. */ + yaml_EMITTER_ERROR +) + +/** The pointer position. */ +type YAML_mark_t struct { + /** The position index. */ + index int + + /** The position line. */ + line int + + /** The position column. */ + column int +} + +/** @} */ + +/** + * @defgroup styles Node Styles + * @{ + */ + +type yaml_style_t int + +/** Scalar styles. */ +type yaml_scalar_style_t yaml_style_t + +const ( + /** Let the emitter choose the style. */ + yaml_ANY_SCALAR_STYLE yaml_scalar_style_t = iota + + /** The plain scalar style. */ + yaml_PLAIN_SCALAR_STYLE + + /** The single-quoted scalar style. */ + yaml_SINGLE_QUOTED_SCALAR_STYLE + /** The double-quoted scalar style. */ + yaml_DOUBLE_QUOTED_SCALAR_STYLE + + /** The literal scalar style. */ + yaml_LITERAL_SCALAR_STYLE + /** The folded scalar style. */ + yaml_FOLDED_SCALAR_STYLE +) + +/** Sequence styles. */ +type yaml_sequence_style_t yaml_style_t + +const ( + /** Let the emitter choose the style. */ + yaml_ANY_SEQUENCE_STYLE yaml_sequence_style_t = iota + + /** The block sequence style. */ + yaml_BLOCK_SEQUENCE_STYLE + /** The flow sequence style. */ + yaml_FLOW_SEQUENCE_STYLE +) + +/** Mapping styles. */ +type yaml_mapping_style_t yaml_style_t + +const ( + /** Let the emitter choose the style. */ + yaml_ANY_MAPPING_STYLE yaml_mapping_style_t = iota + + /** The block mapping style. */ + yaml_BLOCK_MAPPING_STYLE + /** The flow mapping style. */ + yaml_FLOW_MAPPING_STYLE + +/* yaml_FLOW_SET_MAPPING_STYLE */ +) + +/** @} */ + +/** + * @defgroup tokens Tokens + * @{ + */ + +/** Token types. */ +type yaml_token_type_t int + +const ( + /** An empty token. */ + yaml_NO_TOKEN yaml_token_type_t = iota + + /** A STREAM-START token. */ + yaml_STREAM_START_TOKEN + /** A STREAM-END token. */ + yaml_STREAM_END_TOKEN + + /** A VERSION-DIRECTIVE token. */ + yaml_VERSION_DIRECTIVE_TOKEN + /** A TAG-DIRECTIVE token. */ + yaml_TAG_DIRECTIVE_TOKEN + /** A DOCUMENT-START token. */ + yaml_DOCUMENT_START_TOKEN + /** A DOCUMENT-END token. */ + yaml_DOCUMENT_END_TOKEN + + /** A BLOCK-SEQUENCE-START token. */ + yaml_BLOCK_SEQUENCE_START_TOKEN + /** A BLOCK-SEQUENCE-END token. */ + yaml_BLOCK_MAPPING_START_TOKEN + /** A BLOCK-END token. */ + yaml_BLOCK_END_TOKEN + + /** A FLOW-SEQUENCE-START token. */ + yaml_FLOW_SEQUENCE_START_TOKEN + /** A FLOW-SEQUENCE-END token. */ + yaml_FLOW_SEQUENCE_END_TOKEN + /** A FLOW-MAPPING-START token. */ + yaml_FLOW_MAPPING_START_TOKEN + /** A FLOW-MAPPING-END token. */ + yaml_FLOW_MAPPING_END_TOKEN + + /** A BLOCK-ENTRY token. */ + yaml_BLOCK_ENTRY_TOKEN + /** A FLOW-ENTRY token. */ + yaml_FLOW_ENTRY_TOKEN + /** A KEY token. */ + yaml_KEY_TOKEN + /** A VALUE token. */ + yaml_VALUE_TOKEN + + /** An ALIAS token. */ + yaml_ALIAS_TOKEN + /** An ANCHOR token. */ + yaml_ANCHOR_TOKEN + /** A TAG token. */ + yaml_TAG_TOKEN + /** A SCALAR token. */ + yaml_SCALAR_TOKEN +) + +/** The token structure. */ +type yaml_token_t struct { + + /** The token type. */ + token_type yaml_token_type_t + + /** The token data. */ + /** The stream start (for @c yaml_STREAM_START_TOKEN). */ + encoding yaml_encoding_t + + /** The alias (for @c yaml_ALIAS_TOKEN, yaml_ANCHOR_TOKEN, yaml_SCALAR_TOKEN,yaml_TAG_TOKEN ). */ + /** The anchor (for @c ). */ + /** The scalar value (for @c ). */ + value []byte + + /** The tag suffix. */ + suffix []byte + + /** The scalar value (for @c yaml_SCALAR_TOKEN). */ + /** The scalar style. */ + style yaml_scalar_style_t + + /** The version directive (for @c yaml_VERSION_DIRECTIVE_TOKEN). */ + version_directive yaml_version_directive_t + + /** The tag directive (for @c yaml_TAG_DIRECTIVE_TOKEN). */ + prefix []byte + + /** The beginning of the token. */ + start_mark YAML_mark_t + /** The end of the token. */ + end_mark YAML_mark_t + + major, minor int +} + +/** + * @defgroup events Events + * @{ + */ + +/** Event types. */ +type yaml_event_type_t int + +const ( + /** An empty event. */ + yaml_NO_EVENT yaml_event_type_t = iota + + /** A STREAM-START event. */ + yaml_STREAM_START_EVENT + /** A STREAM-END event. */ + yaml_STREAM_END_EVENT + + /** A DOCUMENT-START event. */ + yaml_DOCUMENT_START_EVENT + /** A DOCUMENT-END event. */ + yaml_DOCUMENT_END_EVENT + + /** An ALIAS event. */ + yaml_ALIAS_EVENT + /** A SCALAR event. */ + yaml_SCALAR_EVENT + + /** A SEQUENCE-START event. */ + yaml_SEQUENCE_START_EVENT + /** A SEQUENCE-END event. */ + yaml_SEQUENCE_END_EVENT + + /** A MAPPING-START event. */ + yaml_MAPPING_START_EVENT + /** A MAPPING-END event. */ + yaml_MAPPING_END_EVENT +) + +/** The event structure. */ +type yaml_event_t struct { + + /** The event type. */ + event_type yaml_event_type_t + + /** The stream parameters (for @c yaml_STREAM_START_EVENT). */ + encoding yaml_encoding_t + + /** The document parameters (for @c yaml_DOCUMENT_START_EVENT). */ + version_directive *yaml_version_directive_t + + /** The beginning and end of the tag directives list. */ + tag_directives []yaml_tag_directive_t + + /** The document parameters (for @c yaml_DOCUMENT_START_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_START_EVENT,yaml_MAPPING_START_EVENT). */ + /** Is the document indicator implicit? */ + implicit bool + + /** The alias parameters (for @c yaml_ALIAS_EVENT,yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). */ + /** The anchor. */ + anchor []byte + + /** The scalar parameters (for @c yaml_SCALAR_EVENT,yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). */ + /** The tag. */ + tag []byte + /** The scalar value. */ + value []byte + + /** Is the tag optional for the plain style? */ + plain_implicit bool + /** Is the tag optional for any non-plain style? */ + quoted_implicit bool + + /** The sequence parameters (for @c yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). */ + /** The sequence style. */ + /** The scalar style. */ + style yaml_style_t + + /** The beginning of the event. */ + start_mark, end_mark YAML_mark_t +} + +/** + * @defgroup nodes Nodes + * @{ + */ + +const ( + /** The tag @c !!null with the only possible value: @c null. */ + yaml_NULL_TAG = "tag:yaml.org,2002:null" + /** The tag @c !!bool with the values: @c true and @c falce. */ + yaml_BOOL_TAG = "tag:yaml.org,2002:bool" + /** The tag @c !!str for string values. */ + yaml_STR_TAG = "tag:yaml.org,2002:str" + /** The tag @c !!int for integer values. */ + yaml_INT_TAG = "tag:yaml.org,2002:int" + /** The tag @c !!float for float values. */ + yaml_FLOAT_TAG = "tag:yaml.org,2002:float" + /** The tag @c !!timestamp for date and time values. */ + yaml_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" + + /** The tag @c !!seq is used to denote sequences. */ + yaml_SEQ_TAG = "tag:yaml.org,2002:seq" + /** The tag @c !!map is used to denote mapping. */ + yaml_MAP_TAG = "tag:yaml.org,2002:map" + + /** The default scalar tag is @c !!str. */ + yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG + /** The default sequence tag is @c !!seq. */ + yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG + /** The default mapping tag is @c !!map. */ + yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG +) + +/** Node types. */ +type yaml_node_type_t int + +const ( + /** An empty node. */ + yaml_NO_NODE yaml_node_type_t = iota + + /** A scalar node. */ + yaml_SCALAR_NODE + /** A sequence node. */ + yaml_SEQUENCE_NODE + /** A mapping node. */ + yaml_MAPPING_NODE +) + +/** An element of a sequence node. */ +type yaml_node_item_t int + +/** An element of a mapping node. */ +type yaml_node_pair_t struct { + /** The key of the element. */ + key int + /** The value of the element. */ + value int +} + +/** The node structure. */ +type yaml_node_t struct { + + /** The node type. */ + node_type yaml_node_type_t + + /** The node tag. */ + tag []byte + + /** The scalar parameters (for @c yaml_SCALAR_NODE). */ + scalar struct { + /** The scalar value. */ + value []byte + /** The scalar style. */ + style yaml_scalar_style_t + } + + /** The sequence parameters (for @c yaml_SEQUENCE_NODE). */ + sequence struct { + /** The stack of sequence items. */ + items []yaml_node_item_t + /** The sequence style. */ + style yaml_sequence_style_t + } + + /** The mapping parameters (for @c yaml_MAPPING_NODE). */ + mapping struct { + /** The stack of mapping pairs (key, value). */ + pairs []yaml_node_pair_t + /** The mapping style. */ + style yaml_mapping_style_t + } + + /** The beginning of the node. */ + start_mark YAML_mark_t + /** The end of the node. */ + end_mark YAML_mark_t +} + +/** The document structure. */ +type yaml_document_t struct { + + /** The document nodes. */ + nodes []yaml_node_t + + /** The version directive. */ + version_directive *yaml_version_directive_t + + /** The list of tag directives. */ + tags []yaml_tag_directive_t + + /** Is the document start indicator implicit? */ + start_implicit bool + /** Is the document end indicator implicit? */ + end_implicit bool + + /** The beginning of the document. */ + start_mark YAML_mark_t + /** The end of the document. */ + end_mark YAML_mark_t +} + +/** + * The prototype of a read handler. + * + * The read handler is called when the parser needs to read more bytes from the + * source. The handler should write not more than @a size bytes to the @a + * buffer. The number of written bytes should be set to the @a length variable. + * + * @param[in,out] data A pointer to an application data specified by + * yaml_parser_set_input(). + * @param[out] buffer The buffer to write the data from the source. + * @param[in] size The size of the buffer. + * @param[out] size_read The actual number of bytes read from the source. + * + * @returns On success, the handler should return @c 1. If the handler failed, + * the returned value should be @c 0. On EOF, the handler should set the + * @a size_read to @c 0 and return @c 1. + */ + +type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error) + +/** + * This structure holds information about a potential simple key. + */ + +type yaml_simple_key_t struct { + /** Is a simple key possible? */ + possible bool + + /** Is a simple key required? */ + required bool + + /** The number of the token. */ + token_number int + + /** The position mark. */ + mark YAML_mark_t +} + +/** + * The states of the parser. + */ +type yaml_parser_state_t int + +const ( + /** Expect STREAM-START. */ + yaml_PARSE_STREAM_START_STATE yaml_parser_state_t = iota + /** Expect the beginning of an implicit document. */ + yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE + /** Expect DOCUMENT-START. */ + yaml_PARSE_DOCUMENT_START_STATE + /** Expect the content of a document. */ + yaml_PARSE_DOCUMENT_CONTENT_STATE + /** Expect DOCUMENT-END. */ + yaml_PARSE_DOCUMENT_END_STATE + /** Expect a block node. */ + yaml_PARSE_BLOCK_NODE_STATE + /** Expect a block node or indentless sequence. */ + yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE + /** Expect a flow node. */ + yaml_PARSE_FLOW_NODE_STATE + /** Expect the first entry of a block sequence. */ + yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE + /** Expect an entry of a block sequence. */ + yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE + /** Expect an entry of an indentless sequence. */ + yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + /** Expect the first key of a block mapping. */ + yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE + /** Expect a block mapping key. */ + yaml_PARSE_BLOCK_MAPPING_KEY_STATE + /** Expect a block mapping value. */ + yaml_PARSE_BLOCK_MAPPING_VALUE_STATE + /** Expect the first entry of a flow sequence. */ + yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE + /** Expect an entry of a flow sequence. */ + yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE + /** Expect a key of an ordered mapping. */ + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE + /** Expect a value of an ordered mapping. */ + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE + /** Expect the and of an ordered mapping entry. */ + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE + /** Expect the first key of a flow mapping. */ + yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE + /** Expect a key of a flow mapping. */ + yaml_PARSE_FLOW_MAPPING_KEY_STATE + /** Expect a value of a flow mapping. */ + yaml_PARSE_FLOW_MAPPING_VALUE_STATE + /** Expect an empty value of a flow mapping. */ + yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE + /** Expect nothing. */ + yaml_PARSE_END_STATE +) + +/** + * This structure holds aliases data. + */ + +type yaml_alias_data_t struct { + /** The anchor. */ + anchor []byte + /** The node id. */ + index int + /** The anchor mark. */ + mark YAML_mark_t +} + +/** + * The parser structure. + * + * All members are internal. Manage the structure using the @c yaml_parser_ + * family of functions. + */ + +type yaml_parser_t struct { + + /** + * @name Error handling + * @{ + */ + + /** Error type. */ + error YAML_error_type_t + /** Error description. */ + problem string + /** The byte about which the problem occured. */ + problem_offset int + /** The problematic value (@c -1 is none). */ + problem_value int + /** The problem position. */ + problem_mark YAML_mark_t + /** The error context. */ + context string + /** The context position. */ + context_mark YAML_mark_t + + /** + * @} + */ + + /** + * @name Reader stuff + * @{ + */ + + /** Read handler. */ + read_handler yaml_read_handler_t + + /** Reader input data. */ + input_reader io.Reader + input []byte + input_pos int + + /** EOF flag */ + eof bool + + /** The working buffer. */ + buffer []byte + buffer_pos int + + /* The number of unread characters in the buffer. */ + unread int + + /** The raw buffer. */ + raw_buffer []byte + raw_buffer_pos int + + /** The input encoding. */ + encoding yaml_encoding_t + + /** The offset of the current position (in bytes). */ + offset int + + /** The mark of the current position. */ + mark YAML_mark_t + + /** + * @} + */ + + /** + * @name Scanner stuff + * @{ + */ + + /** Have we started to scan the input stream? */ + stream_start_produced bool + + /** Have we reached the end of the input stream? */ + stream_end_produced bool + + /** The number of unclosed '[' and '{' indicators. */ + flow_level int + + /** The tokens queue. */ + tokens []yaml_token_t + tokens_head int + + /** The number of tokens fetched from the queue. */ + tokens_parsed int + + /* Does the tokens queue contain a token ready for dequeueing. */ + token_available bool + + /** The indentation levels stack. */ + indents []int + + /** The current indentation level. */ + indent int + + /** May a simple key occur at the current position? */ + simple_key_allowed bool + + /** The stack of simple keys. */ + simple_keys []yaml_simple_key_t + + /** + * @} + */ + + /** + * @name Parser stuff + * @{ + */ + + /** The parser states stack. */ + states []yaml_parser_state_t + + /** The current parser state. */ + state yaml_parser_state_t + + /** The stack of marks. */ + marks []YAML_mark_t + + /** The list of TAG directives. */ + tag_directives []yaml_tag_directive_t + + /** + * @} + */ + + /** + * @name Dumper stuff + * @{ + */ + + /** The alias data. */ + aliases []yaml_alias_data_t + + /** The currently parsed document. */ + document *yaml_document_t + + /** + * @} + */ + +} + +/** + * The prototype of a write handler. + * + * The write handler is called when the emitter needs to flush the accumulated + * characters to the output. The handler should write @a size bytes of the + * @a buffer to the output. + * + * @param[in,out] data A pointer to an application data specified by + * yaml_emitter_set_output(). + * @param[in] buffer The buffer with bytes to be written. + * @param[in] size The size of the buffer. + * + * @returns On success, the handler should return @c 1. If the handler failed, + * the returned value should be @c 0. + */ + +type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error + +/** The emitter states. */ +type yaml_emitter_state_t int + +const ( + /** Expect STREAM-START. */ + yaml_EMIT_STREAM_START_STATE yaml_emitter_state_t = iota + /** Expect the first DOCUMENT-START or STREAM-END. */ + yaml_EMIT_FIRST_DOCUMENT_START_STATE + /** Expect DOCUMENT-START or STREAM-END. */ + yaml_EMIT_DOCUMENT_START_STATE + /** Expect the content of a document. */ + yaml_EMIT_DOCUMENT_CONTENT_STATE + /** Expect DOCUMENT-END. */ + yaml_EMIT_DOCUMENT_END_STATE + /** Expect the first item of a flow sequence. */ + yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE + /** Expect an item of a flow sequence. */ + yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE + /** Expect the first key of a flow mapping. */ + yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE + /** Expect a key of a flow mapping. */ + yaml_EMIT_FLOW_MAPPING_KEY_STATE + /** Expect a value for a simple key of a flow mapping. */ + yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE + /** Expect a value of a flow mapping. */ + yaml_EMIT_FLOW_MAPPING_VALUE_STATE + /** Expect the first item of a block sequence. */ + yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE + /** Expect an item of a block sequence. */ + yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE + /** Expect the first key of a block mapping. */ + yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE + /** Expect the key of a block mapping. */ + yaml_EMIT_BLOCK_MAPPING_KEY_STATE + /** Expect a value for a simple key of a block mapping. */ + yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE + /** Expect a value of a block mapping. */ + yaml_EMIT_BLOCK_MAPPING_VALUE_STATE + /** Expect nothing. */ + yaml_EMIT_END_STATE +) + +/** + * The emitter structure. + * + * All members are internal. Manage the structure using the @c yaml_emitter_ + * family of functions. + */ + +type yaml_emitter_t struct { + + /** + * @name Error handling + * @{ + */ + + /** Error type. */ + error YAML_error_type_t + /** Error description. */ + problem string + + /** + * @} + */ + + /** + * @name Writer stuff + * @{ + */ + + /** Write handler. */ + write_handler yaml_write_handler_t + + /** Standard (string or file) output data. */ + output_buffer *[]byte + output_writer io.Writer + + /** The working buffer. */ + buffer []byte + buffer_pos int + + /** The raw buffer. */ + raw_buffer []byte + raw_buffer_pos int + + /** The stream encoding. */ + encoding yaml_encoding_t + + /** + * @} + */ + + /** + * @name Emitter stuff + * @{ + */ + + /** If the output is in the canonical style? */ + canonical bool + /** The number of indentation spaces. */ + best_indent int + /** The preferred width of the output lines. */ + best_width int + /** Allow unescaped non-ASCII characters? */ + unicode bool + /** The preferred line break. */ + line_break yaml_break_t + + /** The stack of states. */ + states []yaml_emitter_state_t + + /** The current emitter state. */ + state yaml_emitter_state_t + + /** The event queue. */ + events []yaml_event_t + events_head int + + /** The stack of indentation levels. */ + indents []int + + /** The list of tag directives. */ + tag_directives []yaml_tag_directive_t + + /** The current indentation level. */ + indent int + + /** The current flow level. */ + flow_level int + + /** Is it the document root context? */ + root_context bool + /** Is it a sequence context? */ + sequence_context bool + /** Is it a mapping context? */ + mapping_context bool + /** Is it a simple mapping key context? */ + simple_key_context bool + + /** The current line. */ + line int + /** The current column. */ + column int + /** If the last character was a whitespace? */ + whitespace bool + /** If the last character was an indentation character (' ', '-', '?', ':')? */ + indention bool + /** If an explicit document end is required? */ + open_ended bool + + /** Anchor analysis. */ + anchor_data struct { + /** The anchor value. */ + anchor []byte + /** Is it an alias? */ + alias bool + } + + /** Tag analysis. */ + tag_data struct { + /** The tag handle. */ + handle []byte + /** The tag suffix. */ + suffix []byte + } + + /** Scalar analysis. */ + scalar_data struct { + /** The scalar value. */ + value []byte + /** Does the scalar contain line breaks? */ + multiline bool + /** Can the scalar be expessed in the flow plain style? */ + flow_plain_allowed bool + /** Can the scalar be expressed in the block plain style? */ + block_plain_allowed bool + /** Can the scalar be expressed in the single quoted style? */ + single_quoted_allowed bool + /** Can the scalar be expressed in the literal or folded styles? */ + block_allowed bool + /** The output style. */ + style yaml_scalar_style_t + } + + /** + * @} + */ + + /** + * @name Dumper stuff + * @{ + */ + + /** If the stream was already opened? */ + opened bool + /** If the stream was already closed? */ + closed bool + + /** The information associated with the document nodes. */ + anchors *struct { + /** The number of references. */ + references int + /** The anchor id. */ + anchor int + /** If the node has been emitted? */ + serialized bool + } + + /** The last assigned anchor id. */ + last_anchor_id int + + /** The currently emitted document. */ + document *yaml_document_t + + /** + * @} + */ + +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/dir_utils.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/dir_utils.go new file mode 100644 index 00000000000..a430217ec56 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/dir_utils.go @@ -0,0 +1,20 @@ +package fileutils + +import ( + "os" +) + +func IsDirEmpty(dir string) (isEmpty bool, err error) { + dirFile, err := os.Open(dir) + if err != nil { + return + } + + _, readErr := dirFile.Readdirnames(1) + if readErr != nil { + isEmpty = true + } else { + isEmpty = false + } + return +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils.go new file mode 100644 index 00000000000..17ef1fb8c5b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils.go @@ -0,0 +1,91 @@ +package fileutils + +import ( + "io" + "io/ioutil" + "os" + "path" + "path/filepath" +) + +func Open(path string) (file *os.File, err error) { + err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm) + if err != nil { + return + } + + return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +} + +func Create(path string) (file *os.File, err error) { + err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm) + if err != nil { + return + } + + return os.Create(path) +} + +func CopyPathToPath(fromPath, toPath string) (err error) { + srcFileInfo, err := os.Stat(fromPath) + if err != nil { + return err + } + + if srcFileInfo.IsDir() { + err = os.MkdirAll(toPath, srcFileInfo.Mode()) + if err != nil { + return err + } + + files, err := ioutil.ReadDir(fromPath) + if err != nil { + return err + } + + for _, file := range files { + err = CopyPathToPath(path.Join(fromPath, file.Name()), path.Join(toPath, file.Name())) + if err != nil { + return err + } + } + } else { + var dst *os.File + dst, err = Create(toPath) + if err != nil { + return err + } + defer dst.Close() + + dst.Chmod(srcFileInfo.Mode()) + + err = CopyPathToWriter(fromPath, dst) + } + return err +} + +func CopyPathToWriter(originalFilePath string, targetWriter io.Writer) (err error) { + originalFile, err := os.Open(originalFilePath) + if err != nil { + return + } + defer originalFile.Close() + + _, err = io.Copy(targetWriter, originalFile) + if err != nil { + return + } + + return +} + +func CopyReaderToPath(src io.Reader, targetPath string) (err error) { + destFile, err := Create(targetPath) + if err != nil { + return + } + defer destFile.Close() + + _, err = io.Copy(destFile, src) + return +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_notwin.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_notwin.go new file mode 100644 index 00000000000..13bd3ab5310 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_notwin.go @@ -0,0 +1,13 @@ +// + +// +build !windows + +package fileutils + +import ( + "os" +) + +func IsRegular(f os.FileInfo) bool { + return f.Mode().IsRegular() +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_test.go new file mode 100644 index 00000000000..6cd61742fb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_test.go @@ -0,0 +1,193 @@ +package fileutils_test + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/cloudfoundry/gofileutils/fileutils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Fileutils File", func() { + var fixturePath = filepath.Clean("../fixtures/fileutils/supervirus.zsh") + var fixtureBytes []byte + + BeforeEach(func() { + var err error + fixtureBytes, err = ioutil.ReadFile(fixturePath) + Expect(err).NotTo(HaveOccurred()) + }) + + Describe("Open", func() { + It("opens an existing file", func() { + fd, err := fileutils.Open(fixturePath) + Expect(err).NotTo(HaveOccurred()) + + fileBytes, err := ioutil.ReadAll(fd) + Expect(err).NotTo(HaveOccurred()) + fd.Close() + + Expect(fileBytes).To(Equal(fixtureBytes)) + }) + + It("creates a non-existing file and all intermediary directories", func() { + filePath := fileutils.TempPath("open_test") + + fd, err := fileutils.Open(filePath) + Expect(err).NotTo(HaveOccurred()) + + _, err = fd.WriteString("Never Gonna Give You Up") + Expect(err).NotTo(HaveOccurred()) + fd.Close() + + fileBytes, err := ioutil.ReadFile(filePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(fileBytes)).To(Equal("Never Gonna Give You Up")) + }) + }) + + Describe("Create", func() { + It("truncates an existing file", func() { + tmpFile, err := ioutil.TempFile("", "create_test") + Expect(err).NotTo(HaveOccurred()) + _, err = tmpFile.WriteString("Never Gonna Give You Up") + Expect(err).NotTo(HaveOccurred()) + filePath := tmpFile.Name() + tmpFile.Close() + + fd, err := fileutils.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + + fileBytes, err := ioutil.ReadAll(fd) + Expect(err).NotTo(HaveOccurred()) + Expect(len(fileBytes)).To(Equal(0)) + fd.Close() + }) + + It("creates a non-existing file and all intermediary directories", func() { + filePath := fileutils.TempPath("create_test") + + fd, err := fileutils.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + + _, err = fd.WriteString("Never Gonna Let You Down") + Expect(err).NotTo(HaveOccurred()) + fd.Close() + + fileBytes, err := ioutil.ReadFile(filePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(fileBytes)).To(Equal("Never Gonna Let You Down")) + }) + }) + + Describe("CopyPathToPath", func() { + var destPath string + + BeforeEach(func() { + destPath = fileutils.TempPath("copy_test") + }) + + Describe("when the source is a file", func() { + BeforeEach(func() { + err := fileutils.CopyPathToPath(fixturePath, destPath) + Expect(err).NotTo(HaveOccurred()) + }) + + It("copies the file contents", func() { + fileBytes, err := ioutil.ReadFile(destPath) + Expect(err).NotTo(HaveOccurred()) + + fixtureBytes, err := ioutil.ReadFile(fixturePath) + Expect(err).NotTo(HaveOccurred()) + Expect(fileBytes).To(Equal(fixtureBytes)) + }) + + It("preserves the file mode", func() { + fileInfo, err := os.Stat(destPath) + Expect(err).NotTo(HaveOccurred()) + + expectedFileInfo, err := os.Stat(fixturePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(fileInfo.Mode()).To(Equal(expectedFileInfo.Mode())) + }) + }) + + Describe("when the source is a directory", func() { + dirPath := filepath.Join(filepath.Dir(fixturePath), "some-dir") + + BeforeEach(func() { + destPath = filepath.Join(destPath, "some-other-dir") + err := fileutils.CopyPathToPath(dirPath, destPath) + Expect(err).NotTo(HaveOccurred()) + }) + + It("creates a directory at the destination path", func() { + fileInfo, err := os.Stat(destPath) + Expect(err).NotTo(HaveOccurred()) + Expect(fileInfo.IsDir()).To(BeTrue()) + }) + + It("copies all of the files from the src directory", func() { + fileInfo, err := os.Stat(path.Join(destPath, "some-file")) + Expect(err).NotTo(HaveOccurred()) + Expect(fileInfo.IsDir()).To(BeFalse()) + }) + + It("preserves the directory's mode", func() { + fileInfo, err := os.Stat(destPath) + Expect(err).NotTo(HaveOccurred()) + + expectedFileInfo, err := os.Stat(dirPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(fileInfo.Mode()).To(Equal(expectedFileInfo.Mode())) + }) + }) + }) + + Describe("CopyPathToWriter", func() { + var destPath string + + BeforeEach(func() { + destFile, err := ioutil.TempFile("", "copy_test") + Expect(err).NotTo(HaveOccurred()) + defer destFile.Close() + + destPath = destFile.Name() + + err = fileutils.CopyPathToWriter(fixturePath, destFile) + Expect(err).NotTo(HaveOccurred()) + }) + + It("copies the file contents", func() { + fileBytes, err := ioutil.ReadFile(destPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(fileBytes).To(Equal(fixtureBytes)) + }) + }) + + Describe("CopyReaderToPath", func() { + var destPath = fileutils.TempPath("copy_test") + + BeforeEach(func() { + fixtureReader, err := os.Open(fixturePath) + Expect(err).NotTo(HaveOccurred()) + defer fixtureReader.Close() + + err = fileutils.CopyReaderToPath(fixtureReader, destPath) + Expect(err).NotTo(HaveOccurred()) + }) + + It("copies the file contents", func() { + fileBytes, err := ioutil.ReadFile(destPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(fileBytes).To(Equal(fixtureBytes)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_windows.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_windows.go new file mode 100644 index 00000000000..a19d75b4af5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/file_utils_windows.go @@ -0,0 +1,20 @@ +package fileutils + +import ( + "os" + "syscall" +) + +const ( + FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 +) + +func IsRegular(f os.FileInfo) bool { + if fileattrs, ok := f.Sys().(*syscall.Win32FileAttributeData); ok { + println("FILEATTRS", f.Name(), fileattrs.FileAttributes) + if fileattrs.FileAttributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return false + } + } + return f.Mode().IsRegular() +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/fileutils_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/fileutils_suite_test.go new file mode 100644 index 00000000000..b2400c92993 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/fileutils_suite_test.go @@ -0,0 +1,13 @@ +package fileutils_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestApp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fileutils Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/temp_utils.go b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/temp_utils.go new file mode 100644 index 00000000000..5ae91bf9aac --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/gofileutils/fileutils/temp_utils.go @@ -0,0 +1,61 @@ +package fileutils + +import ( + "io/ioutil" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +func TempDir(namePrefix string, cb func(tmpDir string, err error)) { + tmpDir, err := ioutil.TempDir("", namePrefix) + + defer func() { + os.RemoveAll(tmpDir) + }() + + cb(tmpDir, err) +} + +func TempFile(namePrefix string, cb func(tmpFile *os.File, err error)) { + tmpFile, err := ioutil.TempFile("", namePrefix) + + defer func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }() + + cb(tmpFile, err) +} + +// TempPath generates a random file path in tmp, but does +// NOT create the actual directory +func TempPath(namePrefix string) string { + return filepath.Join(os.TempDir(), namePrefix, nextSuffix()) +} + +// copied from http://golang.org/src/pkg/io/ioutil/tempfile.go +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} + +func nextSuffix() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.gitignore b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.gitignore new file mode 100644 index 00000000000..357f0286b2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.gitignore @@ -0,0 +1,2 @@ +*.coverprofile +.idea diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.travis.yml b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.travis.yml new file mode 100644 index 00000000000..5ad364dfb17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/.travis.yml @@ -0,0 +1,26 @@ +language: go +notifications: + email: + - cf-lamb@pivotallabs.com + +before_install: +- go get code.google.com/p/go.tools/cmd/cover +- go get github.com/mattn/goveralls +- go get github.com/onsi/ginkgo/ginkgo + +after_success: +- 'echo "mode: set" > all.coverprofile' +- 'find . -name "*.coverprofile" -exec grep -v mode: {} >> all.coverprofile \;' +- PATH=$HOME/gopath/bin:$PATH goveralls -coverprofile=all.coverprofile -repotoken=$COVERALLS_TOKEN + +install: +- go get -d -v -t ./... + +script: PATH=$HOME/gopath/bin:$PATH ginkgo --race --randomizeAllSpecs --failOnPending --skipMeasurements --cover + +go: +- 1.2.1 + +env: + global: + secure: B8pgLMTc+VaGra0bpSOvNLkbxADjrsIylF6wCNQNnLRzUeqtfnJy+M1nz8y4MEalc6hxj5iuwfn67K5AQo8vTkAzE/n+jxxL2pKa0VZS0yLiYxMoWdcOhu7yWNRY6u9t2P0bpJ9zrLN2AjKo+V2FtadNMK0O3yFhEmFzNDEBQ1g= diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/LICENSE b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/LICENSE new file mode 100644 index 00000000000..e661c8d8328 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/LICENSE @@ -0,0 +1,295 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================= + +cf-loggregatorlib + +cf-loggregatorlib : includes a number of subcomponents with +separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for the these subcomponents is subject to the terms and +conditions of the following license. + + +SECTION 1: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES + + >>> gogoprotobuf-402a137c079e420e1f2fd3fb11ec6789a203de58 + >>> testify-f3d115a59739ad0ffb3b132c74894bb100228e52 + + + + +--------------- SECTION 1: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES ---------- + +BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES are applicable to the following component(s). + + +>>> gogoprotobuf-402a137c079e420e1f2fd3fb11ec6789a203de58 + +Go support for Protocol Buffers - Google's data interchange format + +Copyright 2010 The Go Authors. All rights reserved. +http://code.google.com/p/goprotobuf/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +>>> testify-f3d115a59739ad0ffb3b132c74894bb100228e52 + +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +=========================================================================== +To the extent any open source components are licensed under the +GPL and/or LGPL, or other similar licenses that require the +source code and/or modifications to source code to be made +available (as would be noted above), you may obtain a copy of +the source code corresponding to the binaries for such open +source components and modifications thereto, if any, (the +"Source Files"), by downloading the Source Files from VMware's website at +http://www.vmware.com/download/open_source.html, or by sending a request, +with your name and address to: Pivotal Software Inc, 1900 S. Norfolk Street #125, +San Mateo, CA 94403, Attention: General Counsel. All such requests should clearly +specify: OPEN SOURCE FILES REQUEST, +Attention General Counsel. Pivotal Software Inc, shall mail a copy of the +Source Files to you on a CD or equivalent physical medium. This +offer to obtain a copy of the Source Files is valid for three +years from the date you acquired this Software product. +Alternatively, the Source Files may accompany the Pivotal Software Inc, product. + +[CFLOGGREGATORLIB11152013SS120413] diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/README.md b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/README.md new file mode 100644 index 00000000000..fc7720c42ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/README.md @@ -0,0 +1,31 @@ +#loggregator_consumer + +[![Build Status](https://travis-ci.org/cloudfoundry/loggregator_consumer.svg?branch=master)](https://travis-ci.org/cloudfoundry/loggregator_consumer) [![GoDoc](https://godoc.org/github.com/cloudfoundry/loggregator_consumer?status.png)](https://godoc.org/github.com/cloudfoundry/loggregator_consumer) [![Coverage Status](https://coveralls.io/repos/cloudfoundry/loggregator_consumer/badge.png)](https://coveralls.io/r/cloudfoundry/loggregator_consumer) + +Loggregator consumer is a library that allows an application developer to set up +a connection to a loggregator server, and begin receiving log messages from it. +It includes the ability to tail logs as well as get the recent logs. + +Usage +------------------ +See the included sample application. In order to use the sample, you will have to export the following environment variables: + +* CF_ACCESS_TOKEN - You can get this value from reading the AccessToken looking at your cf configuration file (`$ cat ~/.cf/config.json`). Example: + + ``` +export CF_ACCESS_TOKEN="bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI3YmM2MzllOC0wZGM0LTQ4YzItYTAzYS0xYjkyYzRhMWFlZTIiLCJzdWIiOiI5YTc5MTVkOS04MDc1LTQ3OTUtOTBmOS02MGM0MTU0YTJlMDkiLCJzY29wZSI6WyJzY2ltLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLmFkbWluIiwicGFzc3dvcmQud3JpdGUiLCJzY2ltLndyaXRlIiwib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlci53cml0ZSIsImNsb3VkX2NvbnRyb2xsZXIucmVhZCJdLCJjbGllbnRfaWQiOiJjZiIsImNpZCI6ImNmIiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9pZCI6IjlhNzkxNWQ5LTgwNzUtNDc5NS05MGY5LTYwYzQxNTRhMmUwOSIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbiIsImlhdCI6MTQwNDg0NzU3NywiZXhwIjoxNDA0ODQ4MTc3LCJpc3MiOiJodHRwczovL3VhYS4xMC4yNDQuMC4zNC54aXAuaW8vb2F1dGgvdG9rZW4iLCJhdWQiOlsic2NpbSIsIm9wZW5pZCIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCJdfQ.mAaOJthCotW763lf9fysygqdES_Mz1KFQ3HneKbwY4VJx-ARuxxiLh8l_8Srx7NJBwGlyEtfYOCBcIdvyeDCiQ0wT78Zw7ZJYFjnJ5-ZkDy5NbMqHbImDFkHRnPzKFjJHip39jyjAZpkFcrZ8_pUD8XxZraqJ4zEf6LFdAHKFBM" +``` +* APP_GUID - You can get this value from running `$ CF_TRACE=true cf app dora` and then extracting the app guid from the request URL. Example: + +``` +export APP_GUID=55fdb274-d6c9-4b8c-9b1f-9b7e7f3a346c +``` + +Then you can run the sample app like this: + +``` +export GOPATH=`pwd` +export PATH=$PATH:$GOPATH/bin +go get github.com/cloudfoundry/loggregator_consumer/sample_consumer +sample_consumer +``` diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer.go new file mode 100644 index 00000000000..93efffe1214 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer.go @@ -0,0 +1,412 @@ +// Package loggregator_consumer provides a simple, channel-based API for clients to communicate with +// loggregator servers. +package loggregator_consumer + +import ( + "bufio" + "bytes" + "code.google.com/p/gogoprotobuf/proto" + "crypto/tls" + "errors" + "fmt" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/gorilla/websocket" + "io/ioutil" + "mime/multipart" + "net" + "net/http" + "net/url" + "regexp" + "sort" + "strings" + "time" +) + +var ( + // KeepAlive sets the interval between keep-alive messages sent by the client to loggregator. + KeepAlive = 25 * time.Second + boundaryRegexp = regexp.MustCompile("boundary=(.*)") + ErrNotFound = errors.New("/recent path not found or has issues") + ErrBadResponse = errors.New("bad server response") + ErrBadRequest = errors.New("bad client request") +) + +/* LoggregatorConsumer represents the actions that can be performed against a loggregator server. + */ +type LoggregatorConsumer interface { + + // Tail listens indefinitely for log messages. It returns two channels; the first is populated + // with log messages, while the second contains errors (e.g. from parsing messages). It returns + // immediately. Call Close() to terminate the connection when you are finished listening. + // + // Messages are presented in the order received from the loggregator server. Chronological or + // other ordering is not guaranteed. It is the responsibility of the consumer of these channels + // to provide any desired sorting mechanism. + Tail(appGuid string, authToken string) (<-chan *logmessage.LogMessage, error) + + // Recent connects to loggregator via its 'recent' endpoint and returns a slice of recent messages. + // It does not guarantee any order of the messages; they are in the order returned by loggregator. + // + // The SortRecent method is provided to sort the data returned by this method. + Recent(appGuid string, authToken string) ([]*logmessage.LogMessage, error) + + // Close terminates the websocket connection to loggregator. + Close() error + + // SetOnConnectCallback sets a callback function to be called with the websocket connection is established. + SetOnConnectCallback(func()) + + // SetDebugPrinter enables logging of the websocket handshake + SetDebugPrinter(DebugPrinter) +} + +type DebugPrinter interface { + Print(title, dump string) +} + +type nullDebugPrinter struct { +} + +func (nullDebugPrinter) Print(title, body string) { +} + +type consumer struct { + endpoint string + tlsConfig *tls.Config + ws *websocket.Conn + callback func() + proxy func(*http.Request) (*url.URL, error) + debugPrinter DebugPrinter +} + +/* New creates a new consumer to a loggregator endpoint. + */ +func New(endpoint string, tlsConfig *tls.Config, proxy func(*http.Request) (*url.URL, error)) LoggregatorConsumer { + return &consumer{endpoint: endpoint, tlsConfig: tlsConfig, proxy: proxy, debugPrinter: nullDebugPrinter{}} +} + +/* SetDebugPrinter enables logging of the websocket handshake + */ +func (cnsmr *consumer) SetDebugPrinter(debugPrinter DebugPrinter) { + cnsmr.debugPrinter = debugPrinter +} + +/* +Tail listens indefinitely for log messages. It returns two channels; the first is populated +with log messages, while the second contains errors (e.g. from parsing messages). It returns immediately. +Call Close() to terminate the connection when you are finished listening. + +Messages are presented in the order received from the loggregator server. Chronological or other ordering +is not guaranteed. It is the responsibility of the consumer of these channels to provide any desired sorting +mechanism. +*/ +func (cnsmr *consumer) Tail(appGuid string, authToken string) (<-chan *logmessage.LogMessage, error) { + incomingChan := make(chan *logmessage.LogMessage) + var err error + + tailPath := fmt.Sprintf("/tail/?app=%s", appGuid) + cnsmr.ws, err = cnsmr.establishWebsocketConnection(tailPath, authToken) + + if err == nil { + go cnsmr.sendKeepAlive(KeepAlive) + + go func() { + defer close(incomingChan) + cnsmr.listenForMessages(incomingChan) + }() + } + + return incomingChan, err +} + +/* +Recent connects to loggregator via its 'recent' http(s) endpoint and returns a slice of recent messages. +If the new http 'recent' endpoint isn't supported (ie you are connecting to an older loggregator server), +we will fallback to the old Websocket 'dump' endpoint. + +It does not guarantee any order of the messages; they are in the order returned by loggregator. + +The SortRecent method is provided to sort the data returned by this method. +*/ +func (cnsmr *consumer) Recent(appGuid string, authToken string) ([]*logmessage.LogMessage, error) { + messages, err := cnsmr.httpRecent(appGuid, authToken) + if err != ErrBadRequest { + return messages, err + } else { + return cnsmr.dump(appGuid, authToken) + } +} + +/* +httpRecent connects to loggregator via its 'recent' http(s) endpoint and returns a slice of recent messages. +It does not guarantee any order of the messages; they are in the order returned by loggregator. +*/ +func (cnsmr *consumer) httpRecent(appGuid string, authToken string) ([]*logmessage.LogMessage, error) { + endpointUrl, err := url.ParseRequestURI(cnsmr.endpoint) + if err != nil { + return nil, err + } + + scheme := "https" + + if endpointUrl.Scheme == "ws" { + scheme = "http" + } + + recentPath := fmt.Sprintf("%s://%s/recent?app=%s", scheme, endpointUrl.Host, appGuid) + transport := &http.Transport{Proxy: cnsmr.proxy, TLSClientConfig: cnsmr.tlsConfig} + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", recentPath, nil) + req.Header.Set("Authorization", authToken) + + resp, err := client.Do(req) + if err != nil { + return nil, errors.New(fmt.Sprintf("Error dialing loggregator server: %s.\nPlease ask your Cloud Foundry Operator to check the platform configuration (loggregator endpoint is %s).", err.Error(), cnsmr.endpoint)) + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusUnauthorized { + data, _ := ioutil.ReadAll(resp.Body) + return nil, noaa_errors.NewUnauthorizedError(string(data)) + } + + if resp.StatusCode == http.StatusBadRequest { + return nil, ErrBadRequest + } + + if resp.StatusCode != http.StatusOK { + return nil, ErrNotFound + } + + contentType := resp.Header.Get("Content-Type") + + if len(strings.TrimSpace(contentType)) == 0 { + return nil, ErrBadResponse + } + + matches := boundaryRegexp.FindStringSubmatch(contentType) + + if len(matches) != 2 || len(strings.TrimSpace(matches[1])) == 0 { + return nil, ErrBadResponse + } + + reader := multipart.NewReader(resp.Body, matches[1]) + + var buffer bytes.Buffer + messages := make([]*logmessage.LogMessage, 0, 200) + + for part, loopErr := reader.NextPart(); loopErr == nil; part, loopErr = reader.NextPart() { + buffer.Reset() + + msg := new(logmessage.LogMessage) + _, err := buffer.ReadFrom(part) + if err != nil { + break + } + proto.Unmarshal(buffer.Bytes(), msg) + messages = append(messages, msg) + } + + return messages, err +} + +/* +dump connects to loggregator via its 'dump' ws(s) endpoint and returns a slice of recent messages. It does not +guarantee any order of the messages; they are in the order returned by loggregator. + +The SortRecent method is provided to sort the data returned by this method. +*/ +func (cnsmr *consumer) dump(appGuid string, authToken string) ([]*logmessage.LogMessage, error) { + var err error + + dumpPath := fmt.Sprintf("/dump/?app=%s", appGuid) + cnsmr.ws, err = cnsmr.establishWebsocketConnection(dumpPath, authToken) + + if err != nil { + return nil, err + } + + messages := []*logmessage.LogMessage{} + messageChan := make(chan *logmessage.LogMessage) + + go func() { + err = cnsmr.listenForMessages(messageChan) + close(messageChan) + }() + +drainLoop: + for { + select { + case msg, ok := <-messageChan: + if !ok { + break drainLoop + } + + messages = append(messages, msg) + } + } + + return messages, nil +} + +/* Close terminates the websocket connection to loggregator. + */ +func (cnsmr *consumer) Close() error { + if cnsmr.ws == nil { + return errors.New("connection does not exist") + } + + return cnsmr.ws.Close() +} + +func (cnsmr *consumer) SetOnConnectCallback(cb func()) { + cnsmr.callback = cb +} + +/* +SortRecent sorts a slice of LogMessages by timestamp. The sort is stable, so +messages with the same timestamp are sorted in the order that they are received. + +The input slice is sorted; the return value is simply a pointer to the same slice. +*/ +func SortRecent(messages []*logmessage.LogMessage) []*logmessage.LogMessage { + sort.Stable(logMessageSlice(messages)) + return messages +} + +type logMessageSlice []*logmessage.LogMessage + +func (lms logMessageSlice) Len() int { + return len(lms) +} + +func (lms logMessageSlice) Less(i, j int) bool { + return *(lms[i]).Timestamp < *(lms[j]).Timestamp +} + +func (lms logMessageSlice) Swap(i, j int) { + lms[i], lms[j] = lms[j], lms[i] +} + +func (cnsmr *consumer) sendKeepAlive(interval time.Duration) { + for { + err := cnsmr.ws.WriteMessage(websocket.TextMessage, []byte("I'm alive!")) + if err != nil { + return + } + time.Sleep(interval) + } +} + +func (cnsmr *consumer) listenForMessages(msgChan chan<- *logmessage.LogMessage) error { + defer cnsmr.ws.Close() + + for { + var data []byte + + _, data, err := cnsmr.ws.ReadMessage() + if err != nil { + return err + } + + msg, msgErr := logmessage.ParseMessage(data) + if msgErr != nil { + continue + } + + msgChan <- msg.GetLogMessage() + } +} + +func headersString(header http.Header) string { + var result string + for name, values := range header { + result += name + ": " + strings.Join(values, ", ") + "\n" + } + return result +} + +func (cnsmr *consumer) establishWebsocketConnection(path string, authToken string) (*websocket.Conn, error) { + header := http.Header{"Origin": []string{"http://localhost"}, "Authorization": []string{authToken}} + + dialer := websocket.Dialer{NetDial: cnsmr.proxyDial, TLSClientConfig: cnsmr.tlsConfig} + + url := cnsmr.endpoint + path + + cnsmr.debugPrinter.Print("WEBSOCKET REQUEST:", + "GET "+path+" HTTP/1.1\n"+ + "Host: "+cnsmr.endpoint+"\n"+ + "Upgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Version: 13\nSec-WebSocket-Key: [HIDDEN]\n"+ + headersString(header)) + + ws, resp, err := dialer.Dial(url, header) + + if resp != nil { + cnsmr.debugPrinter.Print("WEBSOCKET RESPONSE:", + resp.Proto+" "+resp.Status+"\n"+ + headersString(resp.Header)) + } + + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + bodyData, _ := ioutil.ReadAll(resp.Body) + err = noaa_errors.NewUnauthorizedError(string(bodyData)) + return ws, err + } + + if err == nil && cnsmr.callback != nil { + cnsmr.callback() + } + + if err != nil { + return nil, errors.New(fmt.Sprintf("Error dialing loggregator server: %s.\nPlease ask your Cloud Foundry Operator to check the platform configuration (loggregator endpoint is %s).", err.Error(), cnsmr.endpoint)) + } + + return ws, err +} + +func (cnsmr *consumer) proxyDial(network, addr string) (net.Conn, error) { + targetUrl, err := url.Parse("http://" + addr) + if err != nil { + return nil, err + } + + proxy := cnsmr.proxy + if proxy == nil { + proxy = http.ProxyFromEnvironment + } + + proxyUrl, err := proxy(&http.Request{URL: targetUrl}) + if err != nil { + return nil, err + } + if proxyUrl == nil { + return net.Dial(network, addr) + } + + proxyConn, err := net.Dial(network, proxyUrl.Host) + if err != nil { + return nil, err + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: targetUrl, + Host: targetUrl.Host, + Header: make(http.Header), + } + connectReq.Write(proxyConn) + + connectResp, err := http.ReadResponse(bufio.NewReader(proxyConn), connectReq) + if err != nil { + proxyConn.Close() + return nil, err + } + if connectResp.StatusCode != http.StatusOK { + f := strings.SplitN(connectResp.Status, " ", 2) + proxyConn.Close() + return nil, errors.New(f[1]) + } + + return proxyConn, nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_proxy_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_proxy_test.go new file mode 100644 index 00000000000..bba57d4de97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_proxy_test.go @@ -0,0 +1,168 @@ +package loggregator_consumer_test + +import ( + "bytes" + "crypto/tls" + "errors" + consumer "github.com/cloudfoundry/loggregator_consumer" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/cloudfoundry/loggregatorlib/server/handlers" + "github.com/elazarl/goproxy" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "log" + "net/http" + "net/http/httptest" + "net/url" + "time" +) + +var _ = Describe("Loggregator Consumer behind a Proxy", func() { + var ( + connection consumer.LoggregatorConsumer + endpoint string + testServer *httptest.Server + tlsSettings *tls.Config + consumerProxyFunc func(*http.Request) (*url.URL, error) + + appGuid string + authToken string + incomingChan <-chan *logmessage.LogMessage + messagesToSend chan []byte + testProxyServer *httptest.Server + goProxyHandler *goproxy.ProxyHttpServer + + err error + ) + + BeforeEach(func() { + messagesToSend = make(chan []byte, 256) + + testServer = httptest.NewServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "ws://" + testServer.Listener.Addr().String() + goProxyHandler = goproxy.NewProxyHttpServer() + goProxyHandler.Logger = log.New(bytes.NewBufferString(""), "", 0) + testProxyServer = httptest.NewServer(goProxyHandler) + consumerProxyFunc = func(*http.Request) (*url.URL, error) { + return url.Parse(testProxyServer.URL) + } + }) + + AfterEach(func() { + consumerProxyFunc = nil + if testProxyServer != nil { + testProxyServer.Close() + } + if testServer != nil { + testServer.Close() + } + }) + + Describe("Tail", func() { + + AfterEach(func() { + close(messagesToSend) + }) + + perform := func() { + connection = consumer.New(endpoint, tlsSettings, consumerProxyFunc) + incomingChan, err = connection.Tail(appGuid, authToken) + } + + It("connects using valid URL to running consumerProxyFunc server", func() { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + perform() + + message := <-incomingChan + + Expect(message.Message).To(Equal([]byte("hello"))) + }) + + It("connects using valid URL to a stopped consumerProxyFunc server", func() { + testProxyServer.Close() + + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("connection refused")) + }) + + It("connects using invalid URL", func() { + errMsg := "Invalid consumerProxyFunc URL" + consumerProxyFunc = func(*http.Request) (*url.URL, error) { + return nil, errors.New(errMsg) + } + + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errMsg)) + }) + + It("connects to a consumerProxyFunc server rejecting CONNECT requests", func() { + goProxyHandler.OnRequest().HandleConnect(goproxy.AlwaysReject) + + perform() + + Expect(err).To(HaveOccurred()) + }) + + It("connects to a non-consumerProxyFunc server", func() { + nonProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Go away, I am not a consumerProxyFunc!", http.StatusBadRequest) + })) + consumerProxyFunc = func(*http.Request) (*url.URL, error) { + return url.Parse(nonProxyServer.URL) + } + + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(http.StatusText(http.StatusBadRequest))) + }) + }) + + Describe("Recent", func() { + var httpTestServer *httptest.Server + var incomingMessages []*logmessage.LogMessage + + perform := func() { + close(messagesToSend) + connection = consumer.New(endpoint, tlsSettings, consumerProxyFunc) + incomingMessages, err = connection.Recent(appGuid, authToken) + } + + BeforeEach(func() { + httpTestServer = httptest.NewServer(handlers.NewHttpHandler(messagesToSend)) + endpoint = "ws://" + httpTestServer.Listener.Addr().String() + }) + + AfterEach(func() { + httpTestServer.Close() + }) + + It("returns messages from the server", func() { + messagesToSend <- marshalMessage(createMessage("test-message-0", 0)) + messagesToSend <- marshalMessage(createMessage("test-message-1", 0)) + + perform() + + Expect(err).NotTo(HaveOccurred()) + Expect(incomingMessages).To(HaveLen(2)) + Expect(incomingMessages[0].Message).To(Equal([]byte("test-message-0"))) + Expect(incomingMessages[1].Message).To(Equal([]byte("test-message-1"))) + }) + + It("connects using failing proxyFunc", func() { + errMsg := "Invalid consumerProxyFunc URL" + consumerProxyFunc = func(*http.Request) (*url.URL, error) { + return nil, errors.New(errMsg) + } + + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errMsg)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_test.go new file mode 100644 index 00000000000..3217b4263a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/consumer_test.go @@ -0,0 +1,695 @@ +package loggregator_consumer_test + +import ( + "bytes" + "code.google.com/p/go.net/websocket" + "code.google.com/p/gogoprotobuf/proto" + "crypto/tls" + "fmt" + consumer "github.com/cloudfoundry/loggregator_consumer" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/cloudfoundry/loggregatorlib/server/handlers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "log" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "sync/atomic" + "time" +) + +var _ = Describe("Loggregator Consumer", func() { + var ( + connection consumer.LoggregatorConsumer + endpoint string + testServer *httptest.Server + fakeHandler *FakeHandler + tlsSettings *tls.Config + consumerProxyFunc func(*http.Request) (*url.URL, error) + + appGuid string + authToken string + incomingChan <-chan *logmessage.LogMessage + messagesToSend chan []byte + + err error + ) + + BeforeSuite(func() { + buf := &bytes.Buffer{} + log.SetOutput(buf) + }) + + BeforeEach(func() { + messagesToSend = make(chan []byte, 256) + }) + + AfterEach(func() { + if testServer != nil { + testServer.Close() + } + }) + + Describe("SetOnConnectCallback", func() { + BeforeEach(func() { + testServer = httptest.NewServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "ws://" + testServer.Listener.Addr().String() + close(messagesToSend) + }) + + It("sets a callback and calls it when connecting", func() { + called := false + cb := func() { called = true } + + connection = consumer.New(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.Tail(appGuid, authToken) + + Eventually(func() bool { return called }).Should(BeTrue()) + }) + + Context("when the connection fails", func() { + It("does not call the callback", func() { + endpoint = "!!!bad-endpoint" + + called := false + cb := func() { called = true } + + connection = consumer.New(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.Tail(appGuid, authToken) + + Consistently(func() bool { return called }).Should(BeFalse()) + }) + }) + + Context("when authorization fails", func() { + var failer authFailer + var endpoint string + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("does not call the callback", func() { + called := false + cb := func() { called = true } + + connection = consumer.New(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.Tail(appGuid, authToken) + + Consistently(func() bool { return called }).Should(BeFalse()) + }) + + }) + }) + + var startFakeTrafficController = func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + appGuid = "app-guid" + } + + Describe("Debug Printing", func() { + var debugPrinter *fakeDebugPrinter + + BeforeEach(func() { + startFakeTrafficController() + + debugPrinter = &fakeDebugPrinter{} + connection = consumer.New(endpoint, tlsSettings, consumerProxyFunc) + connection.SetDebugPrinter(debugPrinter) + }) + + It("includes websocket handshake", func() { + close(messagesToSend) + connection.Tail(appGuid, authToken) + + Expect(debugPrinter.Messages[0].Body).To(ContainSubstring("Sec-WebSocket-Version: 13")) + }) + + It("does not include messages sent or received", func() { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + + close(messagesToSend) + connection.Tail(appGuid, authToken) + + Expect(debugPrinter.Messages[0].Body).ToNot(ContainSubstring("hello")) + }) + }) + + Describe("Tail", func() { + perform := func() { + connection = consumer.New(endpoint, tlsSettings, consumerProxyFunc) + incomingChan, err = connection.Tail(appGuid, authToken) + } + + BeforeEach(func() { + startFakeTrafficController() + }) + + Context("when there is no TLS Config or consumerProxyFunc setting", func() { + Context("when the connection can be established", func() { + It("receives messages on the incoming channel", func(done Done) { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + + perform() + message := <-incomingChan + + Expect(message.Message).To(Equal([]byte("hello"))) + close(messagesToSend) + + close(done) + }) + + It("closes the channel after the server closes the connection", func(done Done) { + perform() + close(messagesToSend) + + Eventually(incomingChan).Should(BeClosed()) + + close(done) + }) + + It("sends a keepalive to the server", func() { + messageCountingServer := &messageCountingHandler{} + testServer := httptest.NewServer(websocket.Handler(messageCountingServer.handle)) + defer testServer.Close() + + consumer.KeepAlive = 10 * time.Millisecond + + connection = consumer.New("ws://"+testServer.Listener.Addr().String(), tlsSettings, consumerProxyFunc) + incomingChan, err = connection.Tail(appGuid, authToken) + defer connection.Close() + + Eventually(messageCountingServer.count).Should(BeNumerically("~", 10, 2)) + }) + + It("sends messages for a specific app", func() { + appGuid = "the-app-guid" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getLastURL).Should(ContainSubstring("/tail/?app=the-app-guid")) + }) + + It("sends an Authorization header with an access token", func() { + authToken = "auth-token" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getAuthHeader).Should(Equal("auth-token")) + }) + + Context("when the message fails to parse", func() { + It("skips that message but continues to read messages", func(done Done) { + messagesToSend <- []byte{0} + messagesToSend <- marshalMessage(createMessage("hello", 0)) + perform() + close(messagesToSend) + + message := <-incomingChan + + Expect(message.Message).To(Equal([]byte("hello"))) + + close(done) + }) + }) + }) + + Context("when the connection cannot be established", func() { + BeforeEach(func() { + endpoint = "!!!bad-endpoint" + }) + + It("returns an error", func(done Done) { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Please ask your Cloud Foundry Operator")) + + close(done) + }) + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + Expect(err).To(BeAssignableToTypeOf(&noaa_errors.UnauthorizedError{})) + }) + }) + }) + + Context("when SSL settings are passed in", func() { + BeforeEach(func() { + // fakeHandler = &FakeHandler{innerHandler: } + testServer = httptest.NewTLSServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "wss://" + testServer.Listener.Addr().String() + + tlsSettings = &tls.Config{InsecureSkipVerify: true} + }) + + It("connects using those settings", func() { + perform() + close(messagesToSend) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("Close", func() { + BeforeEach(func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + Context("when a connection is not open", func() { + It("returns an error", func() { + connection = consumer.New(endpoint, nil, nil) + err := connection.Close() + + Expect(err.Error()).To(Equal("connection does not exist")) + }) + }) + + Context("when a connection is open", func() { + It("closes any open channels", func(done Done) { + connection = consumer.New(endpoint, nil, nil) + incomingChan, err := connection.Tail("app-guid", "auth-token") + close(messagesToSend) + + Eventually(fakeHandler.wasCalled).Should(BeTrue()) + + connection.Close() + + Expect(err).NotTo(HaveOccurred()) + Eventually(incomingChan).Should(BeClosed()) + + close(done) + }) + }) + }) + + Describe("Recent with http", func() { + var ( + appGuid = "appGuid" + authToken = "authToken" + receivedLogMessages []*logmessage.LogMessage + recentError error + ) + + perform := func() { + close(messagesToSend) + connection = consumer.New(endpoint, nil, nil) + receivedLogMessages, recentError = connection.Recent(appGuid, authToken) + } + + Context("when the connection cannot be established", func() { + It("invalid endpoints return error", func() { + endpoint = "invalid-endpoint" + perform() + + Expect(recentError).ToNot(BeNil()) + }) + }) + + Context("when the connection can be established", func() { + + BeforeEach(func() { + testServer = httptest.NewServer(handlers.NewHttpHandler(messagesToSend)) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("returns messages from the server", func() { + messagesToSend <- marshalMessage(createMessage("test-message-0", 0)) + messagesToSend <- marshalMessage(createMessage("test-message-1", 0)) + + perform() + + Expect(recentError).NotTo(HaveOccurred()) + Expect(receivedLogMessages).To(HaveLen(2)) + Expect(receivedLogMessages[0].Message).To(Equal([]byte("test-message-0"))) + Expect(receivedLogMessages[1].Message).To(Equal([]byte("test-message-1"))) + }) + }) + + Context("when the content type is missing", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/recent", func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Content-Type", "") + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(consumer.ErrBadResponse)) + }) + + }) + + Context("when the content length is unknown", func() { + BeforeEach(func() { + fakeHandler = &FakeHandler{contentLen: "-1", innerHandler: handlers.NewHttpHandler(messagesToSend)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it handles that without throwing an error", func() { + messagesToSend <- marshalMessage(createMessage("bad-content-length", 0)) + perform() + + Expect(recentError).NotTo(HaveOccurred()) + Expect(receivedLogMessages).To(HaveLen(1)) + }) + + }) + + Context("when the content type doesn't have a boundary", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/recent", func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(consumer.ErrBadResponse)) + }) + + }) + + Context("when the content type's boundary is blank", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/recent", func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Content-Type", "boundary=") + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(consumer.ErrBadResponse)) + }) + + }) + + Context("when the path is not found", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/recent", func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusNotFound) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a not found reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(consumer.ErrNotFound)) + }) + + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + serverMux := http.NewServeMux() + serverMux.Handle("/recent", failer) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + Expect(recentError).To(BeAssignableToTypeOf(&noaa_errors.UnauthorizedError{})) + }) + }) + }) + + Describe("Recent", func() { + var ( + appGuid string + authToken string + logMessages []*logmessage.LogMessage + recentError error + ) + + perform := func() { + close(messagesToSend) + connection = consumer.New(endpoint, nil, nil) + logMessages, recentError = connection.Recent(appGuid, authToken) + } + + BeforeEach(func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + Context("when the connection cannot be established", func() { + It("returns an error", func() { + endpoint = "invalid-endpoint" + perform() + + Expect(recentError).ToNot(BeNil()) + }) + }) + + Context("when the connection can be established", func() { + It("connects to the loggregator server", func() { + perform() + + Expect(fakeHandler.wasCalled()).To(BeTrue()) + }) + + It("returns messages from the server", func() { + messagesToSend <- marshalMessage(createMessage("test-message-0", 0)) + messagesToSend <- marshalMessage(createMessage("test-message-1", 0)) + perform() + + Expect(logMessages).To(HaveLen(2)) + Expect(logMessages[0].Message).To(Equal([]byte("test-message-0"))) + Expect(logMessages[1].Message).To(Equal([]byte("test-message-1"))) + }) + + It("calls the right path on the loggregator endpoint", func() { + appGuid = "app-guid" + perform() + + Expect(fakeHandler.getLastURL()).To(ContainSubstring("/dump/?app=app-guid")) + }) + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + }) + }) + }) + + Describe("SortRecent", func() { + var messages []*logmessage.LogMessage + + BeforeEach(func() { + messages = []*logmessage.LogMessage{createMessage("hello", 2), createMessage("konnichiha", 1)} + }) + + It("sorts messages", func() { + sortedMessages := consumer.SortRecent(messages) + + Expect(*sortedMessages[0].Timestamp).To(Equal(int64(1))) + Expect(*sortedMessages[1].Timestamp).To(Equal(int64(2))) + }) + + It("sorts using a stable algorithm", func() { + messages = append(messages, createMessage("guten tag", 1)) + + sortedMessages := consumer.SortRecent(messages) + + Expect(sortedMessages[0].Message).To(Equal([]byte("konnichiha"))) + Expect(sortedMessages[1].Message).To(Equal([]byte("guten tag"))) + Expect(sortedMessages[2].Message).To(Equal([]byte("hello"))) + }) + }) +}) + +func createMessage(message string, timestamp int64) *logmessage.LogMessage { + messageType := logmessage.LogMessage_OUT + sourceName := "DEA" + + if timestamp == 0 { + timestamp = time.Now().UnixNano() + } + + return &logmessage.LogMessage{ + Message: []byte(message), + AppId: proto.String("my-app-guid"), + MessageType: &messageType, + SourceName: &sourceName, + Timestamp: proto.Int64(timestamp), + } +} + +func marshalMessage(message *logmessage.LogMessage) []byte { + data, err := proto.Marshal(message) + if err != nil { + log.Println(err.Error()) + } + + return data +} + +type authFailer struct { + Message string +} + +func (failer authFailer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("WWW-Authenticate", "Basic") + rw.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(rw, "You are not authorized. %s", failer.Message) +} + +type messageCountingHandler struct { + msgCount int32 +} + +func (mch *messageCountingHandler) handle(conn *websocket.Conn) { + buffer := make([]byte, 1024) + var err error + for err == nil { + _, err = conn.Read(buffer) + if err == nil { + atomic.AddInt32(&mch.msgCount, 1) + } + } +} + +func (mch *messageCountingHandler) count() int32 { + return atomic.LoadInt32(&mch.msgCount) +} + +type FakeHandler struct { + innerHandler http.Handler + called bool + lastURL string + authHeader string + contentLen string + sync.RWMutex +} + +func (fh *FakeHandler) getAuthHeader() string { + fh.RLock() + defer fh.RUnlock() + return fh.authHeader +} + +func (fh *FakeHandler) setAuthHeader(authHeader string) { + fh.Lock() + defer fh.Unlock() + fh.authHeader = authHeader +} + +func (fh *FakeHandler) getLastURL() string { + fh.RLock() + defer fh.RUnlock() + return fh.lastURL +} + +func (fh *FakeHandler) setLastURL(url string) { + fh.Lock() + defer fh.Unlock() + fh.lastURL = url +} + +func (fh *FakeHandler) call() { + fh.Lock() + defer fh.Unlock() + fh.called = true +} + +func (fh *FakeHandler) wasCalled() bool { + fh.RLock() + defer fh.RUnlock() + return fh.called +} + +func (fh *FakeHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + fh.setLastURL(r.URL.String()) + fh.setAuthHeader(r.Header.Get("Authorization")) + fh.call() + if len(fh.contentLen) > 0 { + rw.Header().Set("Content-Length", fh.contentLen) + } + fh.innerHandler.ServeHTTP(rw, r) +} + +type fakeDebugPrinter struct { + Messages []*fakeDebugPrinterMessage +} + +type fakeDebugPrinterMessage struct { + Title, Body string +} + +func (p *fakeDebugPrinter) Print(title, body string) { + message := &fakeDebugPrinterMessage{title, body} + p.Messages = append(p.Messages, message) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer.go new file mode 100644 index 00000000000..03b8802de0f --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer.go @@ -0,0 +1,451 @@ +package dropsonde_consumer + +import ( + "bufio" + "bytes" + "code.google.com/p/gogoprotobuf/proto" + "crypto/tls" + "errors" + "fmt" + "github.com/cloudfoundry/dropsonde/dropsonde_unmarshaller" + "github.com/cloudfoundry/dropsonde/events" + "github.com/cloudfoundry/gosteno" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/gorilla/websocket" + "io/ioutil" + "mime/multipart" + "net" + "net/http" + "net/url" + "regexp" + "sort" + "strings" + "time" +) + +var ( + // KeepAlive sets the interval between keep-alive messages sent by the client to loggregator. + KeepAlive = 25 * time.Second + boundaryRegexp = regexp.MustCompile("boundary=(.*)") + ErrNotFound = errors.New("/recent path not found or has issues") + ErrBadResponse = errors.New("bad server response") + ErrBadRequest = errors.New("bad client request") +) + +/* DropsondeConsumer represents the actions that can be performed against a loggregator server. + */ +type DropsondeConsumer interface { + + // TailingLogs listens indefinitely for log messages. It returns two channels; the first is populated + // with log messages, while the second contains errors (e.g. from parsing messages). It returns + // immediately. Call Close() to terminate the connection when you are finished listening. + // + // Messages are presented in the order received from the loggregator server. Chronological or + // other ordering is not guaranteed. It is the responsibility of the consumer of these channels + // to provide any desired sorting mechanism. + TailingLogs(appGuid string, authToken string) (<-chan *events.Envelope, error) + + /* + Stream listens indefinitely for log and event messages. It returns two channels; the first is populated + with log and event messages, while the second contains errors (e.g. from parsing messages). It returns immediately. + Call Close() to terminate the connection when you are finished listening. + + Messages are presented in the order received from the loggregator server. Chronological or other ordering + is not guaranteed. It is the responsibility of the consumer of these channels to provide any desired sorting + mechanism. + */ + Stream(appGuid string, authToken string) (<-chan *events.Envelope, error) + + // Recent connects to loggregator via its 'recent' endpoint and returns a slice of recent messages. + // It does not guarantee any order of the messages; they are in the order returned by loggregator. + // + // The SortRecent method is provided to sort the data returned by this method. + RecentLogs(appGuid string, authToken string) ([]*events.Envelope, error) + + // Close terminates the websocket connection to loggregator. + Close() error + + // SetOnConnectCallback sets a callback function to be called with the websocket connection is established. + SetOnConnectCallback(func()) + + // SetDebugPrinter enables logging of the websocket handshake + SetDebugPrinter(DebugPrinter) +} + +type DebugPrinter interface { + Print(title, dump string) +} + +type nullDebugPrinter struct { +} + +func (nullDebugPrinter) Print(title, body string) { +} + +type consumer struct { + endpoint string + tlsConfig *tls.Config + ws *websocket.Conn + callback func() + proxy func(*http.Request) (*url.URL, error) + debugPrinter DebugPrinter +} + +/* New creates a new consumer to a loggregator endpoint. + */ +func NewDropsondeConsumer(endpoint string, tlsConfig *tls.Config, proxy func(*http.Request) (*url.URL, error)) DropsondeConsumer { + return &consumer{endpoint: endpoint, tlsConfig: tlsConfig, proxy: proxy, debugPrinter: nullDebugPrinter{}} +} + +/* SetDebugPrinter enables logging of the websocket handshake + */ +func (cnsmr *consumer) SetDebugPrinter(debugPrinter DebugPrinter) { + cnsmr.debugPrinter = debugPrinter +} + +/* +TailingLogs listens indefinitely for log messages. It returns two channels; the first is populated +with log messages, while the second contains errors (e.g. from parsing messages). It returns immediately. +Call Close() to terminate the connection when you are finished listening. + +Messages are presented in the order received from the loggregator server. Chronological or other ordering +is not guaranteed. It is the responsibility of the consumer of these channels to provide any desired sorting +mechanism. +*/ +func (cnsmr *consumer) TailingLogs(appGuid string, authToken string) (<-chan *events.Envelope, error) { + incomingChan := make(chan *events.Envelope) + var err error + + tailPath := fmt.Sprintf("/apps/%s/tailinglogs", appGuid) + cnsmr.ws, err = cnsmr.establishWebsocketConnection(tailPath, authToken) + + if err == nil { + go cnsmr.sendKeepAlive(KeepAlive) + + go func() { + defer close(incomingChan) + cnsmr.listenForMessages(incomingChan) + }() + } + + return incomingChan, err +} + +/* +Stream listens indefinitely for log and event messages. It returns two channels; the first is populated +with log and event messages, while the second contains errors (e.g. from parsing messages). It returns immediately. +Call Close() to terminate the connection when you are finished listening. + +Messages are presented in the order received from the loggregator server. Chronological or other ordering +is not guaranteed. It is the responsibility of the consumer of these channels to provide any desired sorting +mechanism. +*/ +func (cnsmr *consumer) Stream(appGuid string, authToken string) (<-chan *events.Envelope, error) { + incomingChan := make(chan *events.Envelope) + var err error + + streamPath := fmt.Sprintf("/apps/%s/stream", appGuid) + cnsmr.ws, err = cnsmr.establishWebsocketConnection(streamPath, authToken) + + if err == nil { + go cnsmr.sendKeepAlive(KeepAlive) + + go func() { + defer close(incomingChan) + cnsmr.listenForMessages(incomingChan) + }() + } + + return incomingChan, err +} + +/* +RecentLogs connects to loggregator via its 'recentlogs' http(s) endpoint and returns a slice of recent messages. +If the new http 'recentlogs' endpoint isn't supported (ie you are connecting to an older loggregator server), +we will fallback to the old Websocket 'dump' endpoint. + +It does not guarantee any order of the messages; they are in the order returned by loggregator. + +The SortRecent method is provided to sort the data returned by this method. +*/ +func (cnsmr *consumer) RecentLogs(appGuid string, authToken string) ([]*events.Envelope, error) { + messages, err := cnsmr.httpRecentLogs(appGuid, authToken) + if err != ErrBadRequest { + return messages, err + } else { + return cnsmr.dump(appGuid, authToken) + } +} + +/* +httpRecent connects to loggregator via its 'recentlogs' http(s) endpoint and returns a slice of recent messages. +It does not guarantee any order of the messages; they are in the order returned by loggregator. +*/ +func (cnsmr *consumer) httpRecentLogs(appGuid string, authToken string) ([]*events.Envelope, error) { + endpointUrl, err := url.ParseRequestURI(cnsmr.endpoint) + if err != nil { + return nil, err + } + + scheme := "https" + + if endpointUrl.Scheme == "ws" { + scheme = "http" + } + + recentPath := fmt.Sprintf("%s://%s/apps/%s/recentlogs", scheme, endpointUrl.Host, appGuid) + transport := &http.Transport{Proxy: cnsmr.proxy, TLSClientConfig: cnsmr.tlsConfig} + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", recentPath, nil) + req.Header.Set("Authorization", authToken) + + resp, err := client.Do(req) + if err != nil { + return nil, errors.New(fmt.Sprintf("Error dialing loggregator server: %s.\nPlease ask your Cloud Foundry Operator to check the platform configuration (loggregator endpoint is %s).", err.Error(), cnsmr.endpoint)) + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusUnauthorized { + data, _ := ioutil.ReadAll(resp.Body) + return nil, noaa_errors.NewUnauthorizedError(string(data)) + } + + if resp.StatusCode == http.StatusBadRequest { + return nil, ErrBadRequest + } + + if resp.StatusCode != http.StatusOK { + return nil, ErrNotFound + } + + contentType := resp.Header.Get("Content-Type") + + if len(strings.TrimSpace(contentType)) == 0 { + return nil, ErrBadResponse + } + + matches := boundaryRegexp.FindStringSubmatch(contentType) + + if len(matches) != 2 || len(strings.TrimSpace(matches[1])) == 0 { + return nil, ErrBadResponse + } + + reader := multipart.NewReader(resp.Body, matches[1]) + + var buffer bytes.Buffer + messages := make([]*events.Envelope, 0, 200) + + for part, loopErr := reader.NextPart(); loopErr == nil; part, loopErr = reader.NextPart() { + buffer.Reset() + + msg := new(events.Envelope) + _, err := buffer.ReadFrom(part) + if err != nil { + break + } + proto.Unmarshal(buffer.Bytes(), msg) + messages = append(messages, msg) + } + + return messages, err +} + +/* +dump connects to loggregator via its 'dump' ws(s) endpoint and returns a slice of recent messages. It does not +guarantee any order of the messages; they are in the order returned by loggregator. + +The SortRecent method is provided to sort the data returned by this method. +*/ +func (cnsmr *consumer) dump(appGuid string, authToken string) ([]*events.Envelope, error) { + var err error + + dumpPath := fmt.Sprintf("/dump/?app=%s", appGuid) + cnsmr.ws, err = cnsmr.establishWebsocketConnection(dumpPath, authToken) + + if err != nil { + return nil, err + } + + messages := []*events.Envelope{} + messageChan := make(chan *events.Envelope) + + go func() { + err = cnsmr.listenForMessages(messageChan) + close(messageChan) + }() + +drainLoop: + for { + select { + case msg, ok := <-messageChan: + if !ok { + break drainLoop + } + + messages = append(messages, msg) + } + } + + return messages, nil +} + +/* Close terminates the websocket connection to loggregator. + */ +func (cnsmr *consumer) Close() error { + if cnsmr.ws == nil { + return errors.New("connection does not exist") + } + + return cnsmr.ws.Close() +} + +func (cnsmr *consumer) SetOnConnectCallback(cb func()) { + cnsmr.callback = cb +} + +/* +SortRecent sorts a slice of LogMessages by timestamp. The sort is stable, so +messages with the same timestamp are sorted in the order that they are received. + +The input slice is sorted; the return value is simply a pointer to the same slice. +*/ +func SortRecent(messages []*events.Envelope) []*events.Envelope { + sort.Stable(logMessageSlice(messages)) + return messages +} + +type logMessageSlice []*events.Envelope + +func (lms logMessageSlice) Len() int { + return len(lms) +} + +func (lms logMessageSlice) Less(i, j int) bool { + return *(lms[i]).Timestamp < *(lms[j]).Timestamp +} + +func (lms logMessageSlice) Swap(i, j int) { + lms[i], lms[j] = lms[j], lms[i] +} + +func (cnsmr *consumer) sendKeepAlive(interval time.Duration) { + for { + err := cnsmr.ws.WriteMessage(websocket.TextMessage, []byte("I'm alive!")) + if err != nil { + return + } + time.Sleep(interval) + } +} + +func (cnsmr *consumer) listenForMessages(msgChan chan<- *events.Envelope) error { + defer cnsmr.ws.Close() + + unmarshaller := dropsonde_unmarshaller.NewDropsondeUnmarshaller(gosteno.NewLogger("")) + + for { + _, data, err := cnsmr.ws.ReadMessage() + if err != nil { + return err + } + + msg, err := unmarshaller.UnmarshallMessage(data) + if err != nil { + continue + } + + msgChan <- msg + } +} + +func headersString(header http.Header) string { + var result string + for name, values := range header { + result += name + ": " + strings.Join(values, ", ") + "\n" + } + return result +} + +func (cnsmr *consumer) establishWebsocketConnection(path string, authToken string) (*websocket.Conn, error) { + header := http.Header{"Origin": []string{"http://localhost"}, "Authorization": []string{authToken}} + + dialer := websocket.Dialer{NetDial: cnsmr.proxyDial, TLSClientConfig: cnsmr.tlsConfig} + + url := cnsmr.endpoint + path + + cnsmr.debugPrinter.Print("WEBSOCKET REQUEST:", + "GET "+path+" HTTP/1.1\n"+ + "Host: "+cnsmr.endpoint+"\n"+ + "Upgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Version: 13\nSec-WebSocket-Key: [HIDDEN]\n"+ + headersString(header)) + + ws, resp, err := dialer.Dial(url, header) + + if resp != nil { + cnsmr.debugPrinter.Print("WEBSOCKET RESPONSE:", + resp.Proto+" "+resp.Status+"\n"+ + headersString(resp.Header)) + } + + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + bodyData, _ := ioutil.ReadAll(resp.Body) + err = noaa_errors.NewUnauthorizedError(string(bodyData)) + return ws, err + } + + if err == nil && cnsmr.callback != nil { + cnsmr.callback() + } + + if err != nil { + return nil, errors.New(fmt.Sprintf("Error dialing loggregator server: %s.\nPlease ask your Cloud Foundry Operator to check the platform configuration (loggregator endpoint is %s).", err.Error(), cnsmr.endpoint)) + } + + return ws, err +} + +func (cnsmr *consumer) proxyDial(network, addr string) (net.Conn, error) { + targetUrl, err := url.Parse("http://" + addr) + if err != nil { + return nil, err + } + + proxy := cnsmr.proxy + if proxy == nil { + proxy = http.ProxyFromEnvironment + } + + proxyUrl, err := proxy(&http.Request{URL: targetUrl}) + if err != nil { + return nil, err + } + if proxyUrl == nil { + return net.Dial(network, addr) + } + + proxyConn, err := net.Dial(network, proxyUrl.Host) + if err != nil { + return nil, err + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: targetUrl, + Host: targetUrl.Host, + Header: make(http.Header), + } + connectReq.Write(proxyConn) + + connectResp, err := http.ReadResponse(bufio.NewReader(proxyConn), connectReq) + if err != nil { + proxyConn.Close() + return nil, err + } + if connectResp.StatusCode != http.StatusOK { + f := strings.SplitN(connectResp.Status, " ", 2) + proxyConn.Close() + return nil, errors.New(f[1]) + } + + return proxyConn, nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer_test.go new file mode 100644 index 00000000000..a019eaae376 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/consumer_test.go @@ -0,0 +1,832 @@ +package dropsonde_consumer_test + +import ( + "bytes" + "code.google.com/p/go.net/websocket" + "code.google.com/p/gogoprotobuf/proto" + "crypto/tls" + "fmt" + "github.com/cloudfoundry/dropsonde/events" + "github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/cloudfoundry/loggregatorlib/server/handlers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "log" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "sync/atomic" + "time" +) + +var _ = Describe("Dropsonde Consumer", func() { + var ( + connection dropsonde_consumer.DropsondeConsumer + endpoint string + testServer *httptest.Server + fakeHandler *FakeHandler + tlsSettings *tls.Config + consumerProxyFunc func(*http.Request) (*url.URL, error) + + appGuid string + authToken string + incomingChan <-chan *events.Envelope + messagesToSend chan []byte + + err error + ) + + BeforeSuite(func() { + buf := &bytes.Buffer{} + log.SetOutput(buf) + }) + + BeforeEach(func() { + messagesToSend = make(chan []byte, 256) + }) + + AfterEach(func() { + if testServer != nil { + testServer.Close() + } + }) + + Describe("SetOnConnectCallback", func() { + BeforeEach(func() { + testServer = httptest.NewServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "ws://" + testServer.Listener.Addr().String() + close(messagesToSend) + }) + + It("sets a callback and calls it when connecting", func() { + called := false + cb := func() { called = true } + + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.TailingLogs(appGuid, authToken) + + Eventually(func() bool { return called }).Should(BeTrue()) + }) + + Context("when the connection fails", func() { + It("does not call the callback", func() { + endpoint = "!!!bad-endpoint" + + called := false + cb := func() { called = true } + + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.TailingLogs(appGuid, authToken) + + Consistently(func() bool { return called }).Should(BeFalse()) + }) + }) + + Context("when authorization fails", func() { + var failer authFailer + var endpoint string + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("does not call the callback", func() { + called := false + cb := func() { called = true } + + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, nil) + connection.SetOnConnectCallback(cb) + connection.TailingLogs(appGuid, authToken) + + Consistently(func() bool { return called }).Should(BeFalse()) + }) + + }) + }) + + var startFakeTrafficController = func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + appGuid = "app-guid" + } + + Describe("Debug Printing", func() { + var debugPrinter *fakeDebugPrinter + + BeforeEach(func() { + startFakeTrafficController() + + debugPrinter = &fakeDebugPrinter{} + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, consumerProxyFunc) + connection.SetDebugPrinter(debugPrinter) + }) + + It("includes websocket handshake", func() { + close(messagesToSend) + connection.TailingLogs(appGuid, authToken) + + Expect(debugPrinter.Messages[0].Body).To(ContainSubstring("Sec-WebSocket-Version: 13")) + }) + + It("does not include messages sent or received", func() { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + + close(messagesToSend) + connection.TailingLogs(appGuid, authToken) + + Expect(debugPrinter.Messages[0].Body).ToNot(ContainSubstring("hello")) + }) + }) + + Describe("TailingLogs", func() { + perform := func() { + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, consumerProxyFunc) + incomingChan, err = connection.TailingLogs(appGuid, authToken) + } + + BeforeEach(func() { + startFakeTrafficController() + }) + + Context("when there is no TLS Config or consumerProxyFunc setting", func() { + Context("when the connection can be established", func() { + It("receives messages on the incoming channel", func(done Done) { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + + perform() + message := <-incomingChan + + Expect(message.GetLogMessage().GetMessage()).To(Equal([]byte("hello"))) + close(messagesToSend) + + close(done) + }) + + It("closes the channel after the server closes the connection", func(done Done) { + perform() + close(messagesToSend) + + Eventually(incomingChan).Should(BeClosed()) + + close(done) + }) + + It("sends a keepalive to the server", func() { + messageCountingServer := &messageCountingHandler{} + testServer := httptest.NewServer(websocket.Handler(messageCountingServer.handle)) + defer testServer.Close() + + dropsonde_consumer.KeepAlive = 10 * time.Millisecond + + connection = dropsonde_consumer.NewDropsondeConsumer("ws://"+testServer.Listener.Addr().String(), tlsSettings, consumerProxyFunc) + incomingChan, err = connection.TailingLogs(appGuid, authToken) + defer connection.Close() + + Eventually(messageCountingServer.count).Should(BeNumerically("~", 10, 2)) + }) + + It("sends messages for a specific app", func() { + appGuid = "the-app-guid" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getLastURL).Should(ContainSubstring("/apps/the-app-guid/tailinglogs")) + }) + + It("sends an Authorization header with an access token", func() { + authToken = "auth-token" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getAuthHeader).Should(Equal("auth-token")) + }) + + Context("when the message fails to parse", func() { + It("skips that message but continues to read messages", func(done Done) { + messagesToSend <- []byte{0} + messagesToSend <- marshalMessage(createMessage("hello", 0)) + perform() + close(messagesToSend) + + message := <-incomingChan + + Expect(message.GetLogMessage().GetMessage()).To(Equal([]byte("hello"))) + + close(done) + }) + }) + }) + + Context("when the connection cannot be established", func() { + BeforeEach(func() { + endpoint = "!!!bad-endpoint" + }) + + It("returns an error", func(done Done) { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Please ask your Cloud Foundry Operator")) + + close(done) + }) + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + Expect(err).To(BeAssignableToTypeOf(&noaa_errors.UnauthorizedError{})) + }) + }) + }) + + Context("when SSL settings are passed in", func() { + BeforeEach(func() { + // fakeHandler = &FakeHandler{innerHandler: } + testServer = httptest.NewTLSServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "wss://" + testServer.Listener.Addr().String() + + tlsSettings = &tls.Config{InsecureSkipVerify: true} + }) + + It("connects using those settings", func() { + perform() + close(messagesToSend) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("Stream", func() { + perform := func() { + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, tlsSettings, consumerProxyFunc) + incomingChan, err = connection.Stream(appGuid, authToken) + } + + BeforeEach(func() { + startFakeTrafficController() + }) + + Context("when there is no TLS Config or consumerProxyFunc setting", func() { + Context("when the connection can be established", func() { + It("receives messages on the incoming channel", func(done Done) { + messagesToSend <- marshalMessage(createMessage("hello", 0)) + + perform() + message := <-incomingChan + + Expect(message.GetLogMessage().GetMessage()).To(Equal([]byte("hello"))) + close(messagesToSend) + + close(done) + }) + + It("closes the channel after the server closes the connection", func(done Done) { + perform() + close(messagesToSend) + + Eventually(incomingChan).Should(BeClosed()) + + close(done) + }) + + It("sends a keepalive to the server", func() { + messageCountingServer := &messageCountingHandler{} + testServer := httptest.NewServer(websocket.Handler(messageCountingServer.handle)) + defer testServer.Close() + + dropsonde_consumer.KeepAlive = 10 * time.Millisecond + + connection = dropsonde_consumer.NewDropsondeConsumer("ws://"+testServer.Listener.Addr().String(), tlsSettings, consumerProxyFunc) + incomingChan, err = connection.Stream(appGuid, authToken) + defer connection.Close() + + Eventually(messageCountingServer.count).Should(BeNumerically("~", 10, 2)) + }) + + It("sends messages for a specific app", func() { + appGuid = "the-app-guid" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getLastURL).Should(ContainSubstring("/apps/the-app-guid/stream")) + }) + + It("sends an Authorization header with an access token", func() { + authToken = "auth-token" + perform() + close(messagesToSend) + + Eventually(fakeHandler.getAuthHeader).Should(Equal("auth-token")) + }) + + Context("when the message fails to parse", func() { + It("skips that message but continues to read messages", func(done Done) { + messagesToSend <- []byte{0} + messagesToSend <- marshalMessage(createMessage("hello", 0)) + perform() + close(messagesToSend) + + message := <-incomingChan + + Expect(message.GetLogMessage().GetMessage()).To(Equal([]byte("hello"))) + + close(done) + }) + }) + }) + + Context("when the connection cannot be established", func() { + BeforeEach(func() { + endpoint = "!!!bad-endpoint" + }) + + It("returns an error", func(done Done) { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Please ask your Cloud Foundry Operator")) + + close(done) + }) + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + Expect(err).To(BeAssignableToTypeOf(&noaa_errors.UnauthorizedError{})) + }) + }) + }) + + Context("when SSL settings are passed in", func() { + BeforeEach(func() { + // fakeHandler = &FakeHandler{innerHandler: } + testServer = httptest.NewTLSServer(handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)) + endpoint = "wss://" + testServer.Listener.Addr().String() + + tlsSettings = &tls.Config{InsecureSkipVerify: true} + }) + + It("connects using those settings", func() { + perform() + close(messagesToSend) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("Close", func() { + BeforeEach(func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + Context("when a connection is not open", func() { + It("returns an error", func() { + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, nil, nil) + err := connection.Close() + + Expect(err.Error()).To(Equal("connection does not exist")) + }) + }) + + Context("when a connection is open", func() { + It("closes any open channels", func(done Done) { + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, nil, nil) + incomingChan, err := connection.TailingLogs("app-guid", "auth-token") + close(messagesToSend) + + Eventually(fakeHandler.wasCalled).Should(BeTrue()) + + connection.Close() + + Expect(err).NotTo(HaveOccurred()) + Eventually(incomingChan).Should(BeClosed()) + + close(done) + }) + }) + }) + + Describe("RecentLogs with http", func() { + var ( + appGuid = "appGuid" + authToken = "authToken" + receivedLogMessages []*events.Envelope + recentError error + ) + + perform := func() { + close(messagesToSend) + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, nil, nil) + receivedLogMessages, recentError = connection.RecentLogs(appGuid, authToken) + } + + Context("when the connection cannot be established", func() { + It("invalid endpoints return error", func() { + endpoint = "invalid-endpoint" + perform() + + Expect(recentError).ToNot(BeNil()) + }) + }) + + Context("when the connection can be established", func() { + + BeforeEach(func() { + testServer = httptest.NewServer(handlers.NewHttpHandler(messagesToSend)) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("returns messages from the server", func() { + messagesToSend <- marshalMessage(createMessage("test-message-0", 0)) + messagesToSend <- marshalMessage(createMessage("test-message-1", 0)) + + perform() + + Expect(recentError).NotTo(HaveOccurred()) + Expect(receivedLogMessages).To(HaveLen(2)) + Expect(receivedLogMessages[0].GetLogMessage().GetMessage()).To(Equal([]byte("test-message-0"))) + Expect(receivedLogMessages[1].GetLogMessage().GetMessage()).To(Equal([]byte("test-message-1"))) + }) + }) + + Context("when the content type is missing", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/apps/appGuid/recentlogs", func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Content-Type", "") + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(dropsonde_consumer.ErrBadResponse)) + }) + + }) + + Context("when the content length is unknown", func() { + BeforeEach(func() { + fakeHandler = &FakeHandler{contentLen: "-1", innerHandler: handlers.NewHttpHandler(messagesToSend)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it handles that without throwing an error", func() { + messagesToSend <- marshalMessage(createMessage("bad-content-length", 0)) + perform() + + Expect(recentError).NotTo(HaveOccurred()) + Expect(receivedLogMessages).To(HaveLen(1)) + }) + + }) + + Context("when the content type doesn't have a boundary", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/apps/appGuid/recentlogs", func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(dropsonde_consumer.ErrBadResponse)) + }) + + }) + + Context("when the content type's boundary is blank", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/apps/appGuid/recentlogs", func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Content-Type", "boundary=") + resp.Write([]byte("OK")) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a bad reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(dropsonde_consumer.ErrBadResponse)) + }) + + }) + + Context("when the path is not found", func() { + BeforeEach(func() { + + serverMux := http.NewServeMux() + serverMux.HandleFunc("/apps/appGuid/recentlogs", func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusNotFound) + }) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a not found reponse error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError).To(Equal(dropsonde_consumer.ErrNotFound)) + }) + + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + serverMux := http.NewServeMux() + serverMux.Handle("/apps/appGuid/recentlogs", failer) + testServer = httptest.NewServer(serverMux) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + Expect(recentError).To(BeAssignableToTypeOf(&noaa_errors.UnauthorizedError{})) + }) + }) + }) + + Describe("RecentLogs", func() { + var ( + appGuid string + authToken string + logMessages []*events.Envelope + recentError error + ) + + perform := func() { + close(messagesToSend) + connection = dropsonde_consumer.NewDropsondeConsumer(endpoint, nil, nil) + logMessages, recentError = connection.RecentLogs(appGuid, authToken) + } + + BeforeEach(func() { + fakeHandler = &FakeHandler{innerHandler: handlers.NewWebsocketHandler(messagesToSend, 100*time.Millisecond)} + testServer = httptest.NewServer(fakeHandler) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + Context("when the connection cannot be established", func() { + It("returns an error", func() { + endpoint = "invalid-endpoint" + perform() + + Expect(recentError).ToNot(BeNil()) + }) + }) + + Context("when the connection can be established", func() { + It("connects to the loggregator server", func() { + perform() + + Expect(fakeHandler.wasCalled()).To(BeTrue()) + }) + + It("returns messages from the server", func() { + messagesToSend <- marshalMessage(createMessage("test-message-0", 0)) + messagesToSend <- marshalMessage(createMessage("test-message-1", 0)) + perform() + + Expect(logMessages).To(HaveLen(2)) + Expect(logMessages[0].GetLogMessage().GetMessage()).To(Equal([]byte("test-message-0"))) + Expect(logMessages[1].GetLogMessage().GetMessage()).To(Equal([]byte("test-message-1"))) + }) + + It("calls the right path on the loggregator endpoint", func() { + appGuid = "app-guid" + perform() + + Expect(fakeHandler.getLastURL()).To(ContainSubstring("/dump/?app=app-guid")) + }) + }) + + Context("when the authorization fails", func() { + var failer authFailer + + BeforeEach(func() { + failer = authFailer{Message: "Helpful message"} + testServer = httptest.NewServer(failer) + endpoint = "ws://" + testServer.Listener.Addr().String() + }) + + It("it returns a helpful error message", func() { + perform() + + Expect(recentError).To(HaveOccurred()) + Expect(recentError.Error()).To(ContainSubstring("You are not authorized. Helpful message")) + }) + }) + }) + + Describe("SortRecent", func() { + var messages []*events.Envelope + + BeforeEach(func() { + messages = []*events.Envelope{createMessage("hello", 2), createMessage("konnichiha", 1)} + }) + + It("sorts messages", func() { + sortedMessages := dropsonde_consumer.SortRecent(messages) + + Expect(*sortedMessages[0].Timestamp).To(Equal(int64(1))) + Expect(*sortedMessages[1].Timestamp).To(Equal(int64(2))) + }) + + It("sorts using a stable algorithm", func() { + messages = append(messages, createMessage("guten tag", 1)) + + sortedMessages := dropsonde_consumer.SortRecent(messages) + + Expect(sortedMessages[0].GetLogMessage().GetMessage()).To(Equal([]byte("konnichiha"))) + Expect(sortedMessages[1].GetLogMessage().GetMessage()).To(Equal([]byte("guten tag"))) + Expect(sortedMessages[2].GetLogMessage().GetMessage()).To(Equal([]byte("hello"))) + }) + }) +}) + +func createMessage(message string, timestamp int64) *events.Envelope { + if timestamp == 0 { + timestamp = time.Now().UnixNano() + } + + logMessageType := events.LogMessage_OUT + logMessage := &events.LogMessage{ + Message: []byte(message), + MessageType: &logMessageType, + AppId: proto.String("my-app-guid"), + SourceType: proto.String("DEA"), + Timestamp: proto.Int64(timestamp), + } + + eventType := events.Envelope_LogMessage + return &events.Envelope{ + LogMessage: logMessage, + EventType: &eventType, + Origin: proto.String("fake-origin-1"), + Timestamp: proto.Int64(timestamp), + } +} + +func marshalMessage(message *events.Envelope) []byte { + data, err := proto.Marshal(message) + if err != nil { + log.Println(err.Error()) + } + + return data +} + +type authFailer struct { + Message string +} + +func (failer authFailer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("WWW-Authenticate", "Basic") + rw.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(rw, "You are not authorized. %s", failer.Message) +} + +type messageCountingHandler struct { + msgCount int32 +} + +func (mch *messageCountingHandler) handle(conn *websocket.Conn) { + buffer := make([]byte, 1024) + var err error + for err == nil { + _, err = conn.Read(buffer) + if err == nil { + atomic.AddInt32(&mch.msgCount, 1) + } + } +} + +func (mch *messageCountingHandler) count() int32 { + return atomic.LoadInt32(&mch.msgCount) +} + +type FakeHandler struct { + innerHandler http.Handler + called bool + lastURL string + authHeader string + contentLen string + sync.RWMutex +} + +func (fh *FakeHandler) getAuthHeader() string { + fh.RLock() + defer fh.RUnlock() + return fh.authHeader +} + +func (fh *FakeHandler) setAuthHeader(authHeader string) { + fh.Lock() + defer fh.Unlock() + fh.authHeader = authHeader +} + +func (fh *FakeHandler) getLastURL() string { + fh.RLock() + defer fh.RUnlock() + return fh.lastURL +} + +func (fh *FakeHandler) setLastURL(url string) { + fh.Lock() + defer fh.Unlock() + fh.lastURL = url +} + +func (fh *FakeHandler) call() { + fh.Lock() + defer fh.Unlock() + fh.called = true +} + +func (fh *FakeHandler) wasCalled() bool { + fh.RLock() + defer fh.RUnlock() + return fh.called +} + +func (fh *FakeHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + fh.setLastURL(r.URL.String()) + fh.setAuthHeader(r.Header.Get("Authorization")) + fh.call() + if len(fh.contentLen) > 0 { + rw.Header().Set("Content-Length", fh.contentLen) + } + fh.innerHandler.ServeHTTP(rw, r) +} + +type fakeDebugPrinter struct { + Messages []*fakeDebugPrinterMessage +} + +type fakeDebugPrinterMessage struct { + Title, Body string +} + +func (p *fakeDebugPrinter) Print(title, body string) { + message := &fakeDebugPrinterMessage{title, body} + p.Messages = append(p.Messages, message) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/dropsonde_consumer_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/dropsonde_consumer_suite_test.go new file mode 100644 index 00000000000..5060f01d3a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer/dropsonde_consumer_suite_test.go @@ -0,0 +1,13 @@ +package dropsonde_consumer_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestDropsondeConsumer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "DropsondeConsumer Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/integration_test_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/integration_test_suite_test.go new file mode 100644 index 00000000000..b9c75608343 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/integration_test_suite_test.go @@ -0,0 +1,13 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIntegrationTest(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "IntegrationTest Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/loggregator_consumer_smoke_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/loggregator_consumer_smoke_test.go new file mode 100644 index 00000000000..1a73c27cd24 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/integration_test/loggregator_consumer_smoke_test.go @@ -0,0 +1,72 @@ +package integration_test + +import ( + "crypto/tls" + "encoding/json" + consumer "github.com/cloudfoundry/loggregator_consumer" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "os" + + "io/ioutil" + "strings" +) + +var _ = Describe("LoggregatorConsumer:", func() { + var appGuid, authToken string + var connection consumer.LoggregatorConsumer + + BeforeEach(func() { + var err error + appGuid = os.Getenv("TEST_APP_GUID") + loggregatorEndpoint := os.Getenv("LOGGREGATOR_ENDPOINT") + + connection = consumer.New(loggregatorEndpoint, &tls.Config{InsecureSkipVerify: true}, nil) + authToken, err = getAuthToken() + Expect(err).NotTo(HaveOccurred()) + + }) + + AfterEach(func() { + connection.Close() + }) + + It("should return data for recent", func() { + messages, err := connection.Recent(appGuid, authToken) + Expect(err).NotTo(HaveOccurred()) + Expect(messages).To(ContainElement(ContainSubstring("Tick"))) + }) + + It("should return data for tail", func(done Done) { + messagesChan, err := connection.Tail(appGuid, authToken) + Expect(err).NotTo(HaveOccurred()) + + for m := range messagesChan { + if strings.Contains(string(m.GetMessage()), "Tick") { + break + } + } + + close(done) + }, 2) + +}) + +type Config struct { + AccessToken string +} + +func getAuthToken() (string, error) { + bytes, err := ioutil.ReadFile(os.ExpandEnv("$HOME/.cf/config.json")) + if err != nil { + return "", err + } + + var config Config + err = json.Unmarshal(bytes, &config) + if err != nil { + return "", err + } + + return config.AccessToken, nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/loggregator_consumer_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/loggregator_consumer_suite_test.go new file mode 100644 index 00000000000..175f51e9605 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/loggregator_consumer_suite_test.go @@ -0,0 +1,13 @@ +package loggregator_consumer_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLoggregator_consumer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Loggregator_consumer Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/noaa_errors/unauthorized_error.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/noaa_errors/unauthorized_error.go new file mode 100644 index 00000000000..15e7f88ebfc --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/noaa_errors/unauthorized_error.go @@ -0,0 +1,13 @@ +package noaa_errors + +type UnauthorizedError struct { + description string +} + +func NewUnauthorizedError(description string) error { + return &UnauthorizedError{description: description} +} + +func (err *UnauthorizedError) Error() string { + return "Unauthorized error: " + err.description +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_consumer/main.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_consumer/main.go new file mode 100644 index 00000000000..39320ef1cf1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_consumer/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "crypto/tls" + "fmt" + consumer "github.com/cloudfoundry/loggregator_consumer" + "os" +) + +var LoggregatorAddress = "wss://loggregator.10.244.0.34.xip.io:443" +var appGuid = os.Getenv("APP_GUID") +var authToken = os.Getenv("CF_ACCESS_TOKEN") + +func main() { + connection := consumer.New(LoggregatorAddress, &tls.Config{InsecureSkipVerify: true}, nil) + + messages, err := connection.Recent(appGuid, authToken) + + if err != nil { + fmt.Printf("===== Error getting recent messages: %v\n", err) + } else { + fmt.Println("===== Recent messages") + for _, msg := range messages { + fmt.Println(msg) + } + } + + fmt.Println("===== Tailing messages") + msgChan, err := connection.Tail(appGuid, authToken) + + if err != nil { + fmt.Printf("===== Error tailing: %v\n", err) + } else { + for msg := range msgChan { + fmt.Printf("%v \n", msg) + } + } +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_dropsonde_consumer/main.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_dropsonde_consumer/main.go new file mode 100644 index 00000000000..628c02d02c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregator_consumer/sample_dropsonde_consumer/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "crypto/tls" + "fmt" + consumer "github.com/cloudfoundry/loggregator_consumer/dropsonde_consumer" + "os" +) + +var DopplerAddress = "wss://doppler.10.244.0.34.xip.io:443" +var appGuid = os.Getenv("APP_GUID") +var authToken = os.Getenv("CF_ACCESS_TOKEN") + +func main() { + connection := consumer.NewDropsondeConsumer(DopplerAddress, &tls.Config{InsecureSkipVerify: true}, nil) + + messages, err := connection.RecentLogs(appGuid, authToken) + + if err != nil { + fmt.Printf("===== Error getting recent messages: %v\n", err) + } else { + fmt.Println("===== Recent logs") + for _, msg := range messages { + fmt.Println(msg) + } + } + + fmt.Println("===== Streaming metrics") + msgChan, err := connection.Stream(appGuid, authToken) + + if err != nil { + fmt.Printf("===== Error streaming: %v\n", err) + } else { + for msg := range msgChan { + fmt.Printf("%v \n", msg) + } + } +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/dump.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/dump.go new file mode 100644 index 00000000000..cab400a44ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/dump.go @@ -0,0 +1,39 @@ +package logmessage + +import ( + "bytes" + "code.google.com/p/gogoprotobuf/proto" + "encoding/binary" +) + +func DumpMessage(msg Message, buffer *bytes.Buffer) { + binary.Write(buffer, binary.BigEndian, msg.GetRawMessageLength()) + buffer.Write(msg.GetRawMessage()) +} + +func ParseDumpedLogMessages(b []byte) (messages []*LogMessage, err error) { + buffer := bytes.NewBuffer(b) + var length uint32 + for buffer.Len() > 0 { + lengthBytes := bytes.NewBuffer(buffer.Next(4)) + err = binary.Read(lengthBytes, binary.BigEndian, &length) + if err != nil { + return + } + + msgBytes := buffer.Next(int(length)) + var msg *LogMessage + msg, err = parseLogMessage(msgBytes) + if err != nil { + return + } + messages = append(messages, msg) + } + return +} + +func parseLogMessage(data []byte) (logMessage *LogMessage, err error) { + logMessage = new(LogMessage) + err = proto.Unmarshal(data, logMessage) + return logMessage, err +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.pb.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.pb.go new file mode 100644 index 00000000000..942ff488d22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.pb.go @@ -0,0 +1,150 @@ +// Code generated by protoc-gen-gogo. +// source: log_message.proto +// DO NOT EDIT! + +package logmessage + +import proto "code.google.com/p/gogoprotobuf/proto" +import json "encoding/json" +import math "math" + +// Reference proto, json, and math imports to suppress error if they are not otherwise used. +var _ = proto.Marshal +var _ = &json.SyntaxError{} +var _ = math.Inf + +type LogMessage_MessageType int32 + +const ( + LogMessage_OUT LogMessage_MessageType = 1 + LogMessage_ERR LogMessage_MessageType = 2 +) + +var LogMessage_MessageType_name = map[int32]string{ + 1: "OUT", + 2: "ERR", +} +var LogMessage_MessageType_value = map[string]int32{ + "OUT": 1, + "ERR": 2, +} + +func (x LogMessage_MessageType) Enum() *LogMessage_MessageType { + p := new(LogMessage_MessageType) + *p = x + return p +} +func (x LogMessage_MessageType) String() string { + return proto.EnumName(LogMessage_MessageType_name, int32(x)) +} +func (x LogMessage_MessageType) MarshalJSON() ([]byte, error) { + return json.Marshal(x.String()) +} +func (x *LogMessage_MessageType) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(LogMessage_MessageType_value, data, "LogMessage_MessageType") + if err != nil { + return err + } + *x = LogMessage_MessageType(value) + return nil +} + +type LogMessage struct { + Message []byte `protobuf:"bytes,1,req,name=message" json:"message,omitempty"` + MessageType *LogMessage_MessageType `protobuf:"varint,2,req,name=message_type,enum=logmessage.LogMessage_MessageType" json:"message_type,omitempty"` + Timestamp *int64 `protobuf:"zigzag64,3,req,name=timestamp" json:"timestamp,omitempty"` + AppId *string `protobuf:"bytes,4,req,name=app_id" json:"app_id,omitempty"` + SourceId *string `protobuf:"bytes,6,opt,name=source_id" json:"source_id,omitempty"` + DrainUrls []string `protobuf:"bytes,7,rep,name=drain_urls" json:"drain_urls,omitempty"` + SourceName *string `protobuf:"bytes,8,opt,name=source_name" json:"source_name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogMessage) Reset() { *m = LogMessage{} } +func (m *LogMessage) String() string { return proto.CompactTextString(m) } +func (*LogMessage) ProtoMessage() {} + +func (m *LogMessage) GetMessage() []byte { + if m != nil { + return m.Message + } + return nil +} + +func (m *LogMessage) GetMessageType() LogMessage_MessageType { + if m != nil && m.MessageType != nil { + return *m.MessageType + } + return 0 +} + +func (m *LogMessage) GetTimestamp() int64 { + if m != nil && m.Timestamp != nil { + return *m.Timestamp + } + return 0 +} + +func (m *LogMessage) GetAppId() string { + if m != nil && m.AppId != nil { + return *m.AppId + } + return "" +} + +func (m *LogMessage) GetSourceId() string { + if m != nil && m.SourceId != nil { + return *m.SourceId + } + return "" +} + +func (m *LogMessage) GetDrainUrls() []string { + if m != nil { + return m.DrainUrls + } + return nil +} + +func (m *LogMessage) GetSourceName() string { + if m != nil && m.SourceName != nil { + return *m.SourceName + } + return "" +} + +type LogEnvelope struct { + RoutingKey *string `protobuf:"bytes,1,req,name=routing_key" json:"routing_key,omitempty"` + Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` + LogMessage *LogMessage `protobuf:"bytes,3,req,name=log_message" json:"log_message,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogEnvelope) Reset() { *m = LogEnvelope{} } +func (m *LogEnvelope) String() string { return proto.CompactTextString(m) } +func (*LogEnvelope) ProtoMessage() {} + +func (m *LogEnvelope) GetRoutingKey() string { + if m != nil && m.RoutingKey != nil { + return *m.RoutingKey + } + return "" +} + +func (m *LogEnvelope) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + +func (m *LogEnvelope) GetLogMessage() *LogMessage { + if m != nil { + return m.LogMessage + } + return nil +} + +func init() { + proto.RegisterEnum("logmessage.LogMessage_MessageType", LogMessage_MessageType_name, LogMessage_MessageType_value) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.proto b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.proto new file mode 100644 index 00000000000..58bae4e2e53 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/log_message.proto @@ -0,0 +1,22 @@ +package logmessage; + +message LogMessage { + enum MessageType { + OUT = 1; + ERR = 2; + } + + required bytes message = 1; + required MessageType message_type = 2; + required sint64 timestamp = 3; + required string app_id = 4; + optional string source_id = 6; + repeated string drain_urls = 7; + optional string source_name = 8; +} + +message LogEnvelope { + required string routing_key = 1; + required bytes signature = 2; + required LogMessage log_message = 3; +} \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message.go new file mode 100644 index 00000000000..ef8327864e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message.go @@ -0,0 +1,98 @@ +package logmessage + +import ( + "code.google.com/p/gogoprotobuf/proto" + "errors" + "github.com/cloudfoundry/loggregatorlib/signature" + "time" +) + +type Message struct { + logMessage *LogMessage + rawMessage []byte + rawMessageLength uint32 +} + +func NewMessage(logMessage *LogMessage, data []byte) *Message { + return &Message{logMessage, data, uint32(len(data))} +} + +func GenerateMessage(messageType LogMessage_MessageType, messageString, appId, sourceName string) (*Message, error) { + currentTime := time.Now() + logMessage := &LogMessage{ + Message: []byte(messageString), + AppId: &appId, + MessageType: &messageType, + SourceName: proto.String(sourceName), + Timestamp: proto.Int64(currentTime.UnixNano()), + } + + lmBytes, err := proto.Marshal(logMessage) + if err != nil { + return nil, err + } + return NewMessage(logMessage, lmBytes), nil +} + +func ParseMessage(data []byte) (*Message, error) { + logMessage, err := parseLogMessage(data) + return &Message{logMessage, data, uint32(len(data))}, err +} + +func ParseEnvelope(data []byte, secret string) (message *Message, err error) { + message = &Message{} + logEnvelope := &LogEnvelope{} + + if err := proto.Unmarshal(data, logEnvelope); err != nil { + return nil, err + } + if !logEnvelope.VerifySignature(secret) { + return nil, errors.New("Invalid Envelope Signature") + } + + //we pull out the LogMessage from the LogEnvelope and re-marshal it + //because the rawMessage should not contain the information in the logEnvelope + message.rawMessage, err = proto.Marshal(logEnvelope.LogMessage) + if err != nil { + return nil, err + } + + message.logMessage = logEnvelope.LogMessage + message.rawMessageLength = uint32(len(message.rawMessage)) + return message, nil +} + +func (m *Message) GetLogMessage() *LogMessage { + return m.logMessage +} + +func (m *Message) GetRawMessage() []byte { + return m.rawMessage +} + +func (m *Message) GetRawMessageLength() uint32 { + return m.rawMessageLength +} + +func (e *LogEnvelope) VerifySignature(sharedSecret string) bool { + messageDigest, err := signature.Decrypt(sharedSecret, e.GetSignature()) + if err != nil { + return false + } + + expectedDigest := e.logMessageDigest() + return string(messageDigest) == string(expectedDigest) +} + +func (e *LogEnvelope) SignEnvelope(sharedSecret string) error { + signature, err := signature.Encrypt(sharedSecret, e.logMessageDigest()) + if err == nil { + e.Signature = signature + } + + return err +} + +func (e *LogEnvelope) logMessageDigest() []byte { + return signature.DigestBytes(e.LogMessage.GetMessage()) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message_test.go new file mode 100644 index 00000000000..68fed9ce567 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/message_test.go @@ -0,0 +1,146 @@ +package logmessage + +import ( + "code.google.com/p/gogoprotobuf/proto" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestGenerateMessageReturnsMessage(t *testing.T) { + message, err := GenerateMessage(LogMessage_ERR, "myMessage", "123appID", "DEA") + assert.NoError(t, err) + + assert.Equal(t, *message.GetLogMessage().AppId, "123appID") + assert.Equal(t, *message.GetLogMessage().SourceName, "DEA") + assert.Equal(t, *message.GetLogMessage().MessageType, LogMessage_ERR) + assert.Equal(t, message.GetLogMessage().Message, []byte("myMessage")) +} + +func TestEnvelopeExtractionForMessageFails(t *testing.T) { + appMessageString := "AppMessage" + + unmarshalledMessage := NewLogMessageWithSourceName(t, appMessageString, "App", "myApp") + marshalledMessage := MarshallLogMessage(t, unmarshalledMessage) + + _, err := ParseEnvelope(marshalledMessage, "") + assert.Error(t, err) +} + +func TestExtractionFromEnvelopeWithValidSignature(t *testing.T) { + appMessageString := "AppMessage" + + unmarshalledMessage := NewLogMessageWithSourceName(t, appMessageString, "App", "myApp") + marshalledMessage := MarshallLogMessage(t, unmarshalledMessage) + marshalledEnvelope := MarshalledLogEnvelope(t, unmarshalledMessage, "some secret") + + message, err := ParseEnvelope(marshalledEnvelope, "some secret") + assert.NoError(t, err) + + assert.Equal(t, 36, len(message.GetRawMessage())) + assert.Equal(t, marshalledMessage, message.GetRawMessage()) + assert.Equal(t, unmarshalledMessage, message.GetLogMessage()) + assert.Equal(t, "App", message.logMessage.GetSourceName()) +} + +func TestExtractionFromEnvelopeWithInvalidSignature(t *testing.T) { + appMessageString := "AppMessage" + + unmarshalledMessage := NewLogMessageWithSourceName(t, appMessageString, "App", "myApp") + marshalledEnvelope := MarshalledLogEnvelope(t, unmarshalledMessage, "some secret") + + _, err := ParseEnvelope(marshalledEnvelope, "invalid secret") + assert.NotNil(t, err) + +} + +func TestExtractEnvelopeFromRawBytes(t *testing.T) { + //This allows us to verify that the same extraction can be done on the Ruby side + data := []uint8{10, 9, 109, 121, 95, 97, 112, 112, 95, 105, 100, 18, 64, 200, 50, 155, 229, 192, 81, 84, 207, 6, 73, 170, 77, 69, 0, 228, 210, 19, 158, 158, 196, 167, 164, 202, 189, 124, 54, 25, 26, 200, 250, 65, 64, 213, 183, 116, 76, 142, 82, 219, 61, 103, 39, 98, 171, 3, 123, 48, 162, 232, 216, 69, 38, 151, 75, 36, 40, 253, 162, 1, 9, 40, 219, 229, 55, 26, 43, 10, 12, 72, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101, 33, 16, 1, 24, 224, 151, 169, 222, 161, 217, 246, 177, 38, 34, 9, 109, 121, 95, 97, 112, 112, 95, 105, 100, 40, 1, 50, 2, 52, 50} + receivedEnvelope := &LogEnvelope{} + err := proto.Unmarshal(data, receivedEnvelope) + assert.NoError(t, err) + assert.Equal(t, receivedEnvelope.GetLogMessage().GetMessage(), []byte("Hello there!")) + assert.Equal(t, receivedEnvelope.GetLogMessage().GetAppId(), "my_app_id") + assert.Equal(t, receivedEnvelope.GetRoutingKey(), "my_app_id") + assert.Equal(t, receivedEnvelope.GetLogMessage().GetSourceId(), "42") + + assert.True(t, receivedEnvelope.VerifySignature("secret")) +} + +func TestThatSignatureValidatesWhenItMatches(t *testing.T) { + secret := "super-secret" + logMessage := NewLogMessageWithSourceName(t, "the logs", "App", "appid") + + envelope := &LogEnvelope{ + LogMessage: logMessage, + RoutingKey: proto.String(*logMessage.AppId), + } + envelope.SignEnvelope(secret) + + assert.True(t, envelope.VerifySignature(secret)) +} + +func TestThatSignatureDoesNotValidateWhenItDoesntMatch(t *testing.T) { + envelope := &LogEnvelope{ + LogMessage: &LogMessage{}, + RoutingKey: proto.String("app_id"), + Signature: []byte{0, 1, 2}, //some bad signature + } + + assert.False(t, envelope.VerifySignature("super-secret")) +} + +func TestThatSignatureDoesNotValidateWhenSecretIsIncorrect(t *testing.T) { + secret := "super-secret" + logMessage := NewLogMessageWithSourceName(t, "the logs", "App", "appid") + + envelope := &LogEnvelope{ + LogMessage: logMessage, + RoutingKey: proto.String(*logMessage.AppId), + } + envelope.SignEnvelope(secret) + + assert.False(t, envelope.VerifySignature(secret+"not the right secret")) +} + +func NewLogMessageWithSourceName(t *testing.T, messageString, sourceName, appId string) *LogMessage { + currentTime := time.Now() + + messageType := LogMessage_OUT + protoMessage := &LogMessage{ + Message: []byte(messageString), + AppId: proto.String(appId), + MessageType: &messageType, + Timestamp: proto.Int64(currentTime.UnixNano()), + SourceName: proto.String(sourceName), + } + return protoMessage +} + +func MarshallLogMessage(t *testing.T, unmarshalledMessage *LogMessage) []byte { + message, err := proto.Marshal(unmarshalledMessage) + assert.NoError(t, err) + + return message +} + +func MarshalledLogEnvelope(t *testing.T, unmarshalledMessage *LogMessage, secret string) []byte { + envelope := &LogEnvelope{ + LogMessage: unmarshalledMessage, + RoutingKey: proto.String(*unmarshalledMessage.AppId), + } + envelope.SignEnvelope(secret) + + marshalledEnvelope, err := proto.Marshal(envelope) + assert.NoError(t, err) + + return marshalledEnvelope +} + +func UnmarshalLogEnvelope(t *testing.T, data []byte) *LogEnvelope { + logEnvelope := new(LogEnvelope) + err := proto.Unmarshal(data, logEnvelope) + assert.NoError(t, err) + return logEnvelope +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/testhelpers/logmessage_testhelper.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/testhelpers/logmessage_testhelper.go new file mode 100644 index 00000000000..bd43a205839 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/logmessage/testhelpers/logmessage_testhelper.go @@ -0,0 +1,156 @@ +package testhelpers + +import ( + "code.google.com/p/gogoprotobuf/proto" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func MarshalledErrorLogMessage(t *testing.T, messageString string, appId, sourceId string) []byte { + messageType := logmessage.LogMessage_ERR + sourceName := "DEA" + protoMessage := generateLogMessage(messageString, appId, messageType, sourceName, sourceId) + + return marshalProtoBuf(t, protoMessage) +} + +func MarshalledLogMessage(t *testing.T, messageString string, appId string) []byte { + messageType := logmessage.LogMessage_OUT + sourceName := "DEA" + protoMessage := generateLogMessage(messageString, appId, messageType, sourceName, "") + + return marshalProtoBuf(t, protoMessage) +} + +func MarshalledDrainedLogMessage(t *testing.T, messageString string, appId string, drainUrls ...string) []byte { + messageType := logmessage.LogMessage_OUT + sourceName := "App" + protoMessage := generateLogMessage(messageString, appId, messageType, sourceName, "") + protoMessage.DrainUrls = drainUrls + + return marshalProtoBuf(t, protoMessage) +} + +func MarshalledDrainedNonWardenLogMessage(t *testing.T, messageString string, appId string, drainUrls ...string) []byte { + messageType := logmessage.LogMessage_OUT + sourceName := "DEA" + protoMessage := generateLogMessage(messageString, appId, messageType, sourceName, "") + protoMessage.DrainUrls = drainUrls + + return marshalProtoBuf(t, protoMessage) +} + +func NewLogMessage(messageString, appId string) *logmessage.LogMessage { + messageType := logmessage.LogMessage_OUT + sourceName := "App" + + return generateLogMessage(messageString, appId, messageType, sourceName, "") +} + +func NewMessageWithError(messageString, appId string) (*logmessage.Message, error) { + logMessage := generateLogMessage(messageString, appId, logmessage.LogMessage_OUT, "App", "") + + marshalledLogMessage, err := proto.Marshal(logMessage) + + return logmessage.NewMessage(logMessage, marshalledLogMessage), err +} + +func NewMessage(t *testing.T, messageString, appId string) *logmessage.Message { + logMessage := generateLogMessage(messageString, appId, logmessage.LogMessage_OUT, "App", "") + + marshalledLogMessage, err := proto.Marshal(logMessage) + assert.NoError(t, err) + + return logmessage.NewMessage(logMessage, marshalledLogMessage) +} + +func NewMessageFromLogMessage(t *testing.T, logMessage *logmessage.LogMessage) *logmessage.Message { + marshalledLogMessage, err := proto.Marshal(logMessage) + assert.NoError(t, err) + + return logmessage.NewMessage(logMessage, marshalledLogMessage) +} + +func NewMessageWithSyslogDrain(t *testing.T, messageString, appId string, syslogDrains ...string) *logmessage.Message { + logMessage := generateLogMessage(messageString, appId, logmessage.LogMessage_OUT, "App", "") + logMessage.DrainUrls = syslogDrains + + marshalledLogMessage, err := proto.Marshal(logMessage) + assert.NoError(t, err) + + return logmessage.NewMessage(logMessage, marshalledLogMessage) +} + +func NewMessageWithSourceId(t *testing.T, messageString, appId, sourceId string) *logmessage.Message { + logMessage := generateLogMessage(messageString, appId, logmessage.LogMessage_OUT, "App", sourceId) + + marshalledLogMessage, err := proto.Marshal(logMessage) + assert.NoError(t, err) + + return logmessage.NewMessage(logMessage, marshalledLogMessage) +} + +func NewErrMessageWithSourceId(t *testing.T, messageString, appId, sourceId string) *logmessage.Message { + logMessage := generateLogMessage(messageString, appId, logmessage.LogMessage_ERR, "App", sourceId) + + marshalledLogMessage, err := proto.Marshal(logMessage) + assert.NoError(t, err) + + return logmessage.NewMessage(logMessage, marshalledLogMessage) +} + +func MarshalledLogEnvelopeForMessage(t *testing.T, msg, appName, secret string, drainUrls ...string) []byte { + logMessage := NewLogMessage(msg, appName) + logMessage.DrainUrls = drainUrls + return MarshalledLogEnvelope(t, logMessage, secret) +} + +func MarshalledLogEnvelope(t *testing.T, unmarshalledMessage *logmessage.LogMessage, secret string) []byte { + envelope := &logmessage.LogEnvelope{ + LogMessage: unmarshalledMessage, + RoutingKey: proto.String(*unmarshalledMessage.AppId), + } + envelope.SignEnvelope(secret) + + return marshalProtoBuf(t, envelope) +} + +func AssertProtoBufferMessageEquals(t *testing.T, expectedMessage string, actual []byte) { + receivedMessage := assertProtoBufferMessageNoError(t, actual) + assert.Equal(t, receivedMessage, expectedMessage) +} + +func AssertProtoBufferMessageContains(t *testing.T, expectedMessage string, actual []byte) { + receivedMessage := assertProtoBufferMessageNoError(t, actual) + assert.Contains(t, receivedMessage, expectedMessage) +} + +func assertProtoBufferMessageNoError(t *testing.T, actual []byte) string { + receivedMessage := &logmessage.LogMessage{} + err := proto.Unmarshal(actual, receivedMessage) + assert.NoError(t, err) + return string(receivedMessage.GetMessage()) +} + +func generateLogMessage(messageString, appId string, messageType logmessage.LogMessage_MessageType, sourceName, sourceId string) *logmessage.LogMessage { + currentTime := time.Now() + logMessage := &logmessage.LogMessage{ + Message: []byte(messageString), + AppId: proto.String(appId), + MessageType: &messageType, + SourceName: proto.String(sourceName), + SourceId: proto.String(sourceId), + Timestamp: proto.Int64(currentTime.UnixNano()), + } + + return logMessage +} + +func marshalProtoBuf(t *testing.T, pb proto.Message) []byte { + marshalledProtoBuf, err := proto.Marshal(pb) + assert.NoError(t, err) + + return marshalledProtoBuf +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric.go new file mode 100644 index 00000000000..9fa0309e167 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric.go @@ -0,0 +1,131 @@ +//Original source: https://github.com/gokyle/marchat + +package signature + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "fmt" + "io" +) + +const KeySize = 16 + +var ( + ErrPadding = fmt.Errorf("invalid padding") + ErrRandomFailure = fmt.Errorf("failed to read enough random data") + ErrInvalidIV = fmt.Errorf("invalid IV") +) + +func Decrypt(key string, encryptedMessage []byte) ([]byte, error) { + cypher, err := aes.NewCipher(getEncryptionKey(key)) + if err != nil { + return nil, err + } + + clonedEncryptedMessage := make([]byte, len(encryptedMessage)) + copy(clonedEncryptedMessage, encryptedMessage) + + if len(clonedEncryptedMessage) < aes.BlockSize { + return nil, ErrInvalidIV + } + + iv := clonedEncryptedMessage[:aes.BlockSize] + message := clonedEncryptedMessage[aes.BlockSize:] + cbc := cipher.NewCBCDecrypter(cypher, iv) + cbc.CryptBlocks(message, message) + return unpadBuffer(message) +} + +func DigestBytes(message []byte) []byte { + hasher := sha256.New() + hasher.Write(message) + hashedKey := hasher.Sum(nil) + + return hashedKey +} + +func Encrypt(key string, message []byte) ([]byte, error) { + cypher, err := aes.NewCipher(getEncryptionKey(key)) + if err != nil { + return nil, err + } + + iv, err := generateIV() + if err != nil { + return nil, err + } + + paddedMessage, err := padBuffer(message) + if err != nil { + return nil, err + } + + cbc := cipher.NewCBCEncrypter(cypher, iv) + cbc.CryptBlocks(paddedMessage, paddedMessage) + encryptedMessage := append(iv, paddedMessage...) + + return encryptedMessage, nil +} + +func getEncryptionKey(key string) []byte { + hasher := sha256.New() + io.WriteString(hasher, key) + hashedKey := hasher.Sum(nil) + return []byte(hashedKey)[:16] +} + +func generateIV() ([]byte, error) { + return random(aes.BlockSize) +} + +func random(size int) ([]byte, error) { + randomBytes := make([]byte, size) + n, err := rand.Read(randomBytes) + if err != nil { + return []byte{}, err + } else if size != n { + err = ErrRandomFailure + } + return randomBytes, err +} + +func padBuffer(message []byte) ([]byte, error) { + messageLen := len(message) + + paddedMessage := make([]byte, messageLen) + copy(paddedMessage, message) + + if len(paddedMessage) != messageLen { + return paddedMessage, ErrPadding + } + + bytesToPad := aes.BlockSize - messageLen%aes.BlockSize + + paddedMessage = append(paddedMessage, 0x80) + for i := 1; i < bytesToPad; i++ { + paddedMessage = append(paddedMessage, 0x0) + } + return paddedMessage, nil +} + +func unpadBuffer(paddedMessage []byte) ([]byte, error) { + message := paddedMessage + var paddedMessageLen int + origLen := len(message) + + for paddedMessageLen = origLen - 1; paddedMessageLen >= 0; paddedMessageLen-- { + if message[paddedMessageLen] == 0x80 { + break + } + + if message[paddedMessageLen] != 0x0 || (origLen-paddedMessageLen) > aes.BlockSize { + err := ErrPadding + return nil, err + } + } + message = message[:paddedMessageLen] + return message, nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric_test.go new file mode 100644 index 00000000000..b34404a0869 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/loggregatorlib/signature/symmetric_test.go @@ -0,0 +1,65 @@ +package signature + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSimpleEncryption(t *testing.T) { + key := "aaaaaaaaaaaaaaaa" + message := []byte("Super secret message that no one should read") + encrypted, err := Encrypt(key, message) + assert.NoError(t, err) + + decrypted, err := Decrypt(key, encrypted) + assert.NoError(t, err) + + assert.Equal(t, decrypted, message) + assert.NotEqual(t, encrypted, message) +} + +func TestEncryptionWithAShortKey(t *testing.T) { + key := "short key" + message := []byte("Super secret message that no one should read") + encrypted, err := Encrypt(key, message) + assert.NoError(t, err) + + decrypted, err := Decrypt(key, encrypted) + assert.NoError(t, err) + + assert.Equal(t, decrypted, message) + assert.NotEqual(t, encrypted, message) +} + +func TestDecryptionWithWrongKey(t *testing.T) { + key := "short key" + message := []byte("Super secret message that no one should read") + encrypted, err := Encrypt(key, message) + assert.NoError(t, err) + + _, err = Decrypt("wrong key", encrypted) + assert.Error(t, err) +} + +func TestThatEncryptionIsNonDeterministic(t *testing.T) { + key := "aaaaaaaaaaaaaaaa" + message := []byte("Super secret message that no one should read") + encrypted1, err := Encrypt(key, message) + assert.NoError(t, err) + + encrypted2, err := Encrypt(key, message) + assert.NoError(t, err) + + assert.NotEqual(t, encrypted1, encrypted2) +} + +//Test so that we are able to test that Ruby encrypts/signs the same way +func TestDigest(t *testing.T) { + assert.Equal(t, DigestBytes([]byte("some-key")), []byte{0x68, 0x2f, 0x66, 0x97, 0xfa, 0x93, 0xec, 0xa6, 0xc8, 0x1, 0xa2, 0x32, 0x51, 0x9a, 0x9, 0xe3, 0xfe, 0xc, 0x5c, 0x33, 0x94, 0x65, 0xee, 0x53, 0xc3, 0xf9, 0xed, 0xf9, 0x2f, 0xd0, 0x1f, 0x35}) +} + +//Test so that we are able to test that Ruby encrypts/signs the same way +func TestForKeyCreation(t *testing.T) { + key := "12345" + assert.Equal(t, getEncryptionKey(key), []byte{0x59, 0x94, 0x47, 0x1a, 0xbb, 0x1, 0x11, 0x2a, 0xfc, 0xc1, 0x81, 0x59, 0xf6, 0xcc, 0x74, 0xb4}) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml b/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml new file mode 100644 index 00000000000..baf46abc6f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml @@ -0,0 +1,6 @@ +language: go +go: 1.1 + +script: +- go vet ./... +- go test -v ./... diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE b/Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE new file mode 100644 index 00000000000..5515ccfb716 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2013 Jeremy Saenz +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/README.md b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md new file mode 100644 index 00000000000..5c83df6cc20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md @@ -0,0 +1,287 @@ +[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli) + +# cli.go +cli.go is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. + +You can view the API docs here: +http://godoc.org/github.com/codegangsta/cli + +## Overview +Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. + +**This is where cli.go comes into play.** cli.go makes command line programming fun, organized, and expressive! + +## Installation +Make sure you have a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html). + +To install `cli.go`, simply run: +``` +$ go get github.com/codegangsta/cli +``` + +Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: +``` +export PATH=$PATH:$GOPATH/bin +``` + +## Getting Started +One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`. + +``` go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + cli.NewApp().Run(os.Args) +} +``` + +This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: + +``` go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "boom" + app.Usage = "make an explosive entrance" + app.Action = func(c *cli.Context) { + println("boom! I say!") + } + + app.Run(os.Args) +} +``` + +Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below. + +## Example + +Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! + +``` go +/* greet.go */ +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "greet" + app.Usage = "fight the loneliness!" + app.Action = func(c *cli.Context) { + println("Hello friend!") + } + + app.Run(os.Args) +} +``` + +Install our command to the `$GOPATH/bin` directory: + +``` +$ go install +``` + +Finally run our new command: + +``` +$ greet +Hello friend! +``` + +cli.go also generates some bitchass help text: +``` +$ greet help +NAME: + greet - fight the loneliness! + +USAGE: + greet [global options] command [command options] [arguments...] + +VERSION: + 0.0.0 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS + --version Shows version information +``` + +### Arguments +You can lookup arguments by calling the `Args` function on `cli.Context`. + +``` go +... +app.Action = func(c *cli.Context) { + println("Hello", c.Args()[0]) +} +... +``` + +### Flags +Setting and querying flags is simple. +``` go +... +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, +} +app.Action = func(c *cli.Context) { + name := "someone" + if len(c.Args()) > 0 { + name = c.Args()[0] + } + if c.String("lang") == "spanish" { + println("Hola", name) + } else { + println("Hello", name) + } +} +... +``` + +#### Alternate Names + +You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, +} +``` + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVar`. e.g. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, +} +``` + +That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. + +### Subcommands + +Subcommands can be defined for a more git-like command line app. +```go +... +app.Commands = []cli.Command{ + { + Name: "add", + ShortName: "a", + Usage: "add a task to the list", + Action: func(c *cli.Context) { + println("added task: ", c.Args().First()) + }, + }, + { + Name: "complete", + ShortName: "c", + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + }, + { + Name: "template", + ShortName: "r", + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) { + println("new task template: ", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) { + println("removed task template: ", c.Args().First()) + }, + }, + }, + }, +} +... +``` + +### Bash Completion + +You can enable completion commands by setting the `EnableBashCompletion` +flag on the `App` object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. +```go +... +var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} +app := cli.NewApp() +app.EnableBashCompletion = true +app.Commands = []cli.Command{ + { + Name: "complete", + ShortName: "c", + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if len(c.Args()) > 0 { + return + } + for _, t := range tasks { + println(t) + } + }, + } +} +... +``` + +#### To Enable + +Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while +setting the `PROG` variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + + +## Contribution Guidelines +Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. + +If you are have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. + +If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. + +## About +cli.go is written by none other than the [Code Gangsta](http://codegangsta.io) diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go new file mode 100644 index 00000000000..66e541c7f86 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go @@ -0,0 +1,246 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "os" + "time" +) + +// App is the main structure of a cli application. It is recomended that +// and app be created with the cli.NewApp() function +type App struct { + // The name of the program. Defaults to os.Args[0] + Name string + // Description of the program. + Usage string + // Version of the program + Version string + // List of commands to execute + Commands []Command + // List of flags to parse + Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command + HideHelp bool + // An action to execute when the bash-completion flag is set + BashComplete func(context *Context) + // An action to execute before any subcommands are run, but after the context is ready + // If a non-nil error is returned, no subcommands are run + Before func(context *Context) error + // The action to execute when no subcommands are specified + Action func(context *Context) + // Execute this function if the proper command cannot be found + CommandNotFound func(context *Context, command string) + // Compilation date + Compiled time.Time + // Author + Author string + // Author e-mail + Email string +} + +// Tries to find out when this binary was compiled. +// Returns the current time if it fails to find it. +func compileTime() time.Time { + info, err := os.Stat(os.Args[0]) + if err != nil { + return time.Now() + } + return info.ModTime() +} + +// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. +func NewApp() *App { + return &App{ + Name: os.Args[0], + Usage: "A new cli application", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + } +} + +// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination +func (a *App) Run(arguments []string) error { + // append help to commands + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + a.appendFlag(HelpFlag) + } + + //append version/help flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + a.appendFlag(VersionFlag) + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err := set.Parse(arguments[1:]) + nerr := normalizeFlags(a.Flags, set) + if nerr != nil { + fmt.Println(nerr) + context := NewContext(a, set, set) + ShowAppHelp(context) + fmt.Println("") + return nerr + } + context := NewContext(a, set, set) + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowAppHelp(context) + fmt.Println("") + return err + } + + if checkCompletions(context) { + return nil + } + + if checkHelp(context) { + return nil + } + + if checkVersion(context) { + return nil + } + + if a.Before != nil { + err := a.Before(context) + if err != nil { + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + a.Action(context) + return nil +} + +// Another entry point to the cli app, takes care of passing arguments and error handling +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + os.Stderr.WriteString(fmt.Sprintln(err)) + os.Exit(1) + } +} + +// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags +func (a *App) RunAsSubcommand(ctx *Context) error { + // append help to commands + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + a.appendFlag(HelpFlag) + } + } + + // append flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err := set.Parse(ctx.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, ctx.globalSet) + + if nerr != nil { + fmt.Println(nerr) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } + fmt.Println("") + return nerr + } + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowSubcommandHelp(context) + return err + } + + if checkCompletions(context) { + return nil + } + + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } + } + + if a.Before != nil { + err := a.Before(context) + if err != nil { + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + if len(a.Commands) > 0 { + a.Action(context) + } else { + a.Action(ctx) + } + + return nil +} + +// Returns the named command on App. Returns nil if the command does not exist +func (a *App) Command(name string) *Command { + for _, c := range a.Commands { + if c.HasName(name) { + return &c + } + } + + return nil +} + +func (a *App) hasFlag(flag Flag) bool { + for _, f := range a.Flags { + if flag == f { + return true + } + } + + return false +} + +func (a *App) appendFlag(flag Flag) { + if !a.hasFlag(flag) { + a.Flags = append(a.Flags, flag) + } +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go new file mode 100644 index 00000000000..81d11743e31 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go @@ -0,0 +1,423 @@ +package cli_test + +import ( + "fmt" + "os" + "testing" + + "github.com/codegangsta/cli" +) + +func ExampleApp() { + // set args for examples sake + os.Args = []string{"greet", "--name", "Jeremy"} + + app := cli.NewApp() + app.Name = "greet" + app.Flags = []cli.Flag{ + cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, + } + app.Action = func(c *cli.Context) { + fmt.Printf("Hello %v\n", c.String("name")) + } + app.Run(os.Args) + // Output: + // Hello Jeremy +} + +func ExampleAppSubcommand() { + // set args for examples sake + os.Args = []string{"say", "hi", "english", "--name", "Jeremy"} + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Value: "Bob", + Usage: "Name of the person to greet", + }, + }, + Action: func(c *cli.Context) { + fmt.Println("Hello,", c.String("name")) + }, + }, + }, + }, + } + + app.Run(os.Args) + // Output: + // Hello, Jeremy +} + +func ExampleAppHelp() { + // set args for examples sake + os.Args = []string{"greet", "h", "describeit"} + + app := cli.NewApp() + app.Name = "greet" + app.Flags = []cli.Flag{ + cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, + } + app.Commands = []cli.Command{ + { + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + Action: func(c *cli.Context) { + fmt.Printf("i like to describe things") + }, + }, + } + app.Run(os.Args) + // Output: + // NAME: + // describeit - use it to see a description + // + // USAGE: + // command describeit [arguments...] + // + // DESCRIPTION: + // This is how we describe describeit the function +} + +func ExampleAppBashComplete() { + // set args for examples sake + os.Args = []string{"greet", "--generate-bash-completion"} + + app := cli.NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + Action: func(c *cli.Context) { + fmt.Printf("i like to describe things") + }, + }, { + Name: "next", + Usage: "next example", + Description: "more stuff to see when generating bash completion", + Action: func(c *cli.Context) { + fmt.Printf("the next example") + }, + }, + } + + app.Run(os.Args) + // Output: + // describeit + // d + // next + // help + // h +} + +func TestApp_Run(t *testing.T) { + s := "" + + app := cli.NewApp() + app.Action = func(c *cli.Context) { + s = s + c.Args().First() + } + + err := app.Run([]string{"command", "foo"}) + expect(t, err, nil) + err = app.Run([]string{"command", "bar"}) + expect(t, err, nil) + expect(t, s, "foobar") +} + +var commandAppTests = []struct { + name string + expected bool +}{ + {"foobar", true}, + {"batbaz", true}, + {"b", true}, + {"f", true}, + {"bat", false}, + {"nothing", false}, +} + +func TestApp_Command(t *testing.T) { + app := cli.NewApp() + fooCommand := cli.Command{Name: "foobar", ShortName: "f"} + batCommand := cli.Command{Name: "batbaz", ShortName: "b"} + app.Commands = []cli.Command{ + fooCommand, + batCommand, + } + + for _, test := range commandAppTests { + expect(t, app.Command(test.name) != nil, test.expected) + } +} + +func TestApp_CommandWithArgBeforeFlags(t *testing.T) { + var parsedOption, firstArg string + + app := cli.NewApp() + command := cli.Command{ + Name: "cmd", + Flags: []cli.Flag{ + cli.StringFlag{Name: "option", Value: "", Usage: "some option"}, + }, + Action: func(c *cli.Context) { + parsedOption = c.String("option") + firstArg = c.Args().First() + }, + } + app.Commands = []cli.Command{command} + + app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"}) + + expect(t, parsedOption, "my-option") + expect(t, firstArg, "my-arg") +} + +func TestApp_Float64Flag(t *testing.T) { + var meters float64 + + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, + } + app.Action = func(c *cli.Context) { + meters = c.Float64("height") + } + + app.Run([]string{"", "--height", "1.93"}) + expect(t, meters, 1.93) +} + +func TestApp_ParseSliceFlags(t *testing.T) { + var parsedOption, firstArg string + var parsedIntSlice []int + var parsedStringSlice []string + + app := cli.NewApp() + command := cli.Command{ + Name: "cmd", + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "p", Value: &cli.IntSlice{}, Usage: "set one or more ip addr"}, + cli.StringSliceFlag{Name: "ip", Value: &cli.StringSlice{}, Usage: "set one or more ports to open"}, + }, + Action: func(c *cli.Context) { + parsedIntSlice = c.IntSlice("p") + parsedStringSlice = c.StringSlice("ip") + parsedOption = c.String("option") + firstArg = c.Args().First() + }, + } + app.Commands = []cli.Command{command} + + app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"}) + + IntsEquals := func(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true + } + + StrsEquals := func(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true + } + var expectedIntSlice = []int{22, 80} + var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"} + + if !IntsEquals(parsedIntSlice, expectedIntSlice) { + t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice) + } + + if !StrsEquals(parsedStringSlice, expectedStringSlice) { + t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice) + } +} + +func TestApp_BeforeFunc(t *testing.T) { + beforeRun, subcommandRun := false, false + beforeError := fmt.Errorf("fail") + var err error + + app := cli.NewApp() + + app.Before = func(c *cli.Context) error { + beforeRun = true + s := c.String("opt") + if s == "fail" { + return beforeError + } + + return nil + } + + app.Commands = []cli.Command{ + cli.Command{ + Name: "sub", + Action: func(c *cli.Context) { + subcommandRun = true + }, + }, + } + + app.Flags = []cli.Flag{ + cli.StringFlag{Name: "opt"}, + } + + // run with the Before() func succeeding + err = app.Run([]string{"command", "--opt", "succeed", "sub"}) + + if err != nil { + t.Fatalf("Run error: %s", err) + } + + if beforeRun == false { + t.Errorf("Before() not executed when expected") + } + + if subcommandRun == false { + t.Errorf("Subcommand not executed when expected") + } + + // reset + beforeRun, subcommandRun = false, false + + // run with the Before() func failing + err = app.Run([]string{"command", "--opt", "fail", "sub"}) + + // should be the same error produced by the Before func + if err != beforeError { + t.Errorf("Run error expected, but not received") + } + + if beforeRun == false { + t.Errorf("Before() not executed when expected") + } + + if subcommandRun == true { + t.Errorf("Subcommand executed when NOT expected") + } + +} + +func TestAppHelpPrinter(t *testing.T) { + oldPrinter := cli.HelpPrinter + defer func() { + cli.HelpPrinter = oldPrinter + }() + + var wasCalled = false + cli.HelpPrinter = func(template string, data interface{}) { + wasCalled = true + } + + app := cli.NewApp() + app.Run([]string{"-h"}) + + if wasCalled == false { + t.Errorf("Help printer expected to be called, but was not") + } +} + +func TestAppVersionPrinter(t *testing.T) { + oldPrinter := cli.VersionPrinter + defer func() { + cli.VersionPrinter = oldPrinter + }() + + var wasCalled = false + cli.VersionPrinter = func(c *cli.Context) { + wasCalled = true + } + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + cli.ShowVersion(ctx) + + if wasCalled == false { + t.Errorf("Version printer expected to be called, but was not") + } +} + +func TestAppCommandNotFound(t *testing.T) { + beforeRun, subcommandRun := false, false + app := cli.NewApp() + + app.CommandNotFound = func(c *cli.Context, command string) { + beforeRun = true + } + + app.Commands = []cli.Command{ + cli.Command{ + Name: "bar", + Action: func(c *cli.Context) { + subcommandRun = true + }, + }, + } + + app.Run([]string{"command", "foo"}) + + expect(t, beforeRun, true) + expect(t, subcommandRun, false) +} + +func TestGlobalFlagsInSubcommands(t *testing.T) { + subcommandRun := false + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "debug, d", Usage: "Enable debugging"}, + } + + app.Commands = []cli.Command{ + cli.Command{ + Name: "foo", + Subcommands: []cli.Command{ + { + Name: "bar", + Action: func(c *cli.Context) { + if c.GlobalBool("debug") { + subcommandRun = true + } + }, + }, + }, + }, + } + + app.Run([]string{"command", "-d", "foo", "bar"}) + + expect(t, subcommandRun, true) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete new file mode 100644 index 00000000000..9b55dd990cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete @@ -0,0 +1,13 @@ +#! /bin/bash + +_cli_bash_autocomplete() { + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + } + + complete -F _cli_bash_autocomplete $PROG \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete new file mode 100644 index 00000000000..5430a18f957 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete @@ -0,0 +1,5 @@ +autoload -U compinit && compinit +autoload -U bashcompinit && bashcompinit + +script_dir=$(dirname $0) +source ${script_dir}/bash_autocomplete diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/cli.go b/Godeps/_workspace/src/github.com/codegangsta/cli/cli.go new file mode 100644 index 00000000000..b7425458123 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/cli.go @@ -0,0 +1,19 @@ +// Package cli provides a minimal framework for creating and organizing command line +// Go applications. cli is designed to be easy to understand and write, the most simple +// cli application can be written as follows: +// func main() { +// cli.NewApp().Run(os.Args) +// } +// +// Of course this application does not do much, so let's make this an actual application: +// func main() { +// app := cli.NewApp() +// app.Name = "greet" +// app.Usage = "say a greeting" +// app.Action = func(c *cli.Context) { +// println("Greetings") +// } +// +// app.Run(os.Args) +// } +package cli diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go new file mode 100644 index 00000000000..879a793dc22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go @@ -0,0 +1,100 @@ +package cli_test + +import ( + "os" + + "github.com/codegangsta/cli" +) + +func Example() { + app := cli.NewApp() + app.Name = "todo" + app.Usage = "task list on the command line" + app.Commands = []cli.Command{ + { + Name: "add", + ShortName: "a", + Usage: "add a task to the list", + Action: func(c *cli.Context) { + println("added task: ", c.Args().First()) + }, + }, + { + Name: "complete", + ShortName: "c", + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + }, + } + + app.Run(os.Args) +} + +func ExampleSubcommand() { + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Value: "Bob", + Usage: "Name of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Hello, ", c.String("name")) + }, + }, { + Name: "spanish", + ShortName: "sp", + Usage: "sends a greeting in spanish", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "surname", + Value: "Jones", + Usage: "Surname of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Hola, ", c.String("surname")) + }, + }, { + Name: "french", + ShortName: "fr", + Usage: "sends a greeting in french", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "nickname", + Value: "Stevie", + Usage: "Nickname of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Bonjour, ", c.String("nickname")) + }, + }, + }, + }, { + Name: "bye", + Usage: "says goodbye", + Action: func(c *cli.Context) { + println("bye") + }, + }, + } + + app.Run(os.Args) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go new file mode 100644 index 00000000000..5622b38f75c --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go @@ -0,0 +1,144 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "strings" +) + +// Command is a subcommand for a cli.App. +type Command struct { + // The name of the command + Name string + // short name of the command. Typically one character + ShortName string + // A short description of the usage of this command + Usage string + // A longer explanation of how the command works + Description string + // The function to call when checking for bash command completions + BashComplete func(context *Context) + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before func(context *Context) error + // The function to call when this command is invoked + Action func(context *Context) + // List of child commands + Subcommands []Command + // List of flags to parse + Flags []Flag + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Boolean to hide built-in help command + HideHelp bool +} + +// Invokes the command given the context, parses ctx.Args() to generate command-specific flags +func (c Command) Run(ctx *Context) error { + + if len(c.Subcommands) > 0 || c.Before != nil { + return c.startApp(ctx) + } + + if !c.HideHelp { + // append help to flags + c.Flags = append( + c.Flags, + HelpFlag, + ) + } + + if ctx.App.EnableBashCompletion { + c.Flags = append(c.Flags, BashCompletionFlag) + } + + set := flagSet(c.Name, c.Flags) + set.SetOutput(ioutil.Discard) + + firstFlagIndex := -1 + for index, arg := range ctx.Args() { + if strings.HasPrefix(arg, "-") { + firstFlagIndex = index + break + } + } + + var err error + if firstFlagIndex > -1 && !c.SkipFlagParsing { + args := ctx.Args() + regularArgs := args[1:firstFlagIndex] + flagArgs := args[firstFlagIndex:] + err = set.Parse(append(flagArgs, regularArgs...)) + } else { + err = set.Parse(ctx.Args().Tail()) + } + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowCommandHelp(ctx, c.Name) + fmt.Println("") + return err + } + + nerr := normalizeFlags(c.Flags, set) + if nerr != nil { + fmt.Println(nerr) + fmt.Println("") + ShowCommandHelp(ctx, c.Name) + fmt.Println("") + return nerr + } + context := NewContext(ctx.App, set, ctx.globalSet) + + if checkCommandCompletions(context, c.Name) { + return nil + } + + if checkCommandHelp(context, c.Name) { + return nil + } + context.Command = c + c.Action(context) + return nil +} + +// Returns true if Command.Name or Command.ShortName matches given name +func (c Command) HasName(name string) bool { + return c.Name == name || c.ShortName == name +} + +func (c Command) startApp(ctx *Context) error { + app := NewApp() + + // set the name and usage + app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + if c.Description != "" { + app.Usage = c.Description + } else { + app.Usage = c.Usage + } + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + app.HideHelp = c.HideHelp + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the actions + app.Before = c.Before + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + + return app.RunAsSubcommand(ctx) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go new file mode 100644 index 00000000000..c0f556ad242 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go @@ -0,0 +1,49 @@ +package cli_test + +import ( + "flag" + "testing" + + "github.com/codegangsta/cli" +) + +func TestCommandDoNotIgnoreFlags(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + test := []string{"blah", "blah", "-break"} + set.Parse(test) + + c := cli.NewContext(app, set, set) + + command := cli.Command{ + Name: "test-cmd", + ShortName: "tc", + Usage: "this is for testing", + Description: "testing", + Action: func(_ *cli.Context) {}, + } + err := command.Run(c) + + expect(t, err.Error(), "flag provided but not defined: -break") +} + +func TestCommandIgnoreFlags(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + test := []string{"blah", "blah"} + set.Parse(test) + + c := cli.NewContext(app, set, set) + + command := cli.Command{ + Name: "test-cmd", + ShortName: "tc", + Usage: "this is for testing", + Description: "testing", + Action: func(_ *cli.Context) {}, + SkipFlagParsing: true, + } + err := command.Run(c) + + expect(t, err, nil) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go new file mode 100644 index 00000000000..8b44148ec28 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go @@ -0,0 +1,315 @@ +package cli + +import ( + "errors" + "flag" + "strconv" + "strings" + "time" +) + +// Context is a type that is passed through to +// each Handler action in a cli application. Context +// can be used to retrieve context-specific Args and +// parsed command-line options. +type Context struct { + App *App + Command Command + flagSet *flag.FlagSet + globalSet *flag.FlagSet + setFlags map[string]bool +} + +// Creates a new context. For use in when invoking an App or Command action. +func NewContext(app *App, set *flag.FlagSet, globalSet *flag.FlagSet) *Context { + return &Context{App: app, flagSet: set, globalSet: globalSet} +} + +// Looks up the value of a local int flag, returns 0 if no int flag exists +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + +// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists +func (c *Context) Float64(name string) float64 { + return lookupFloat64(name, c.flagSet) +} + +// Looks up the value of a local bool flag, returns false if no bool flag exists +func (c *Context) Bool(name string) bool { + return lookupBool(name, c.flagSet) +} + +// Looks up the value of a local boolT flag, returns false if no bool flag exists +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + +// Looks up the value of a local string flag, returns "" if no string flag exists +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// Looks up the value of a local string slice flag, returns nil if no string slice flag exists +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// Looks up the value of a local int slice flag, returns nil if no int slice flag exists +func (c *Context) IntSlice(name string) []int { + return lookupIntSlice(name, c.flagSet) +} + +// Looks up the value of a local generic flag, returns nil if no generic flag exists +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// Looks up the value of a global int flag, returns 0 if no int flag exists +func (c *Context) GlobalInt(name string) int { + return lookupInt(name, c.globalSet) +} + +// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) GlobalDuration(name string) time.Duration { + return lookupDuration(name, c.globalSet) +} + +// Looks up the value of a global bool flag, returns false if no bool flag exists +func (c *Context) GlobalBool(name string) bool { + return lookupBool(name, c.globalSet) +} + +// Looks up the value of a global string flag, returns "" if no string flag exists +func (c *Context) GlobalString(name string) string { + return lookupString(name, c.globalSet) +} + +// Looks up the value of a global string slice flag, returns nil if no string slice flag exists +func (c *Context) GlobalStringSlice(name string) []string { + return lookupStringSlice(name, c.globalSet) +} + +// Looks up the value of a global int slice flag, returns nil if no int slice flag exists +func (c *Context) GlobalIntSlice(name string) []int { + return lookupIntSlice(name, c.globalSet) +} + +// Looks up the value of a global generic flag, returns nil if no generic flag exists +func (c *Context) GlobalGeneric(name string) interface{} { + return lookupGeneric(name, c.globalSet) +} + +// Determines if the flag was actually set exists +func (c *Context) IsSet(name string) bool { + if c.setFlags == nil { + c.setFlags = make(map[string]bool) + c.flagSet.Visit(func(f *flag.Flag) { + c.setFlags[f.Name] = true + }) + } + return c.setFlags[name] == true +} + +// Returns a slice of flag names used in this context. +func (c *Context) FlagNames() (names []string) { + for _, flag := range c.Command.Flags { + name := strings.Split(flag.getName(), ",")[0] + if name == "help" { + continue + } + names = append(names, name) + } + return +} + +type Args []string + +// Returns the command line arguments associated with the context. +func (c *Context) Args() Args { + args := Args(c.flagSet.Args()) + return args +} + +// Returns the nth argument, or else a blank string +func (a Args) Get(n int) string { + if len(a) > n { + return a[n] + } + return "" +} + +// Returns the first argument, or else a blank string +func (a Args) First() string { + return a.Get(0) +} + +// Return the rest of the arguments (not the first one) +// or else an empty string slice +func (a Args) Tail() []string { + if len(a) >= 2 { + return []string(a)[1:] + } + return []string{} +} + +// Checks if there are any arguments present +func (a Args) Present() bool { + return len(a) != 0 +} + +// Swaps arguments at the given indexes +func (a Args) Swap(from, to int) error { + if from >= len(a) || to >= len(a) { + return errors.New("index out of range") + } + a[from], a[to] = a[to], a[from] + return nil +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + val, err := strconv.Atoi(f.Value.String()) + if err != nil { + return 0 + } + return val + } + + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + val, err := time.ParseDuration(f.Value.String()) + if err == nil { + return val + } + } + + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return val + } + + return 0 +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + return f.Value.String() + } + + return "" +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*StringSlice)).Value() + + } + + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*IntSlice)).Value() + + } + + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + return f.Value + } + return nil +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return val + } + + return false +} + +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return true + } + return val + } + + return false +} + +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case *StringSlice: + default: + set.Set(name, ff.Value.String()) + } +} + +func normalizeFlags(flags []Flag, set *flag.FlagSet) error { + visited := make(map[string]bool) + set.Visit(func(f *flag.Flag) { + visited[f.Name] = true + }) + for _, f := range flags { + parts := strings.Split(f.getName(), ",") + if len(parts) == 1 { + continue + } + var ff *flag.Flag + for _, name := range parts { + name = strings.Trim(name, " ") + if visited[name] { + if ff != nil { + return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) + } + ff = set.Lookup(name) + } + } + if ff == nil { + continue + } + for _, name := range parts { + name = strings.Trim(name, " ") + if !visited[name] { + copyFlag(name, ff, set) + } + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go new file mode 100644 index 00000000000..b2d24121104 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go @@ -0,0 +1,77 @@ +package cli_test + +import ( + "flag" + "testing" + "time" + + "github.com/codegangsta/cli" +) + +func TestNewContext(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int("myflag", 12, "doc") + globalSet := flag.NewFlagSet("test", 0) + globalSet.Int("myflag", 42, "doc") + command := cli.Command{Name: "mycommand"} + c := cli.NewContext(nil, set, globalSet) + c.Command = command + expect(t, c.Int("myflag"), 12) + expect(t, c.GlobalInt("myflag"), 42) + expect(t, c.Command.Name, "mycommand") +} + +func TestContext_Int(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int("myflag", 12, "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.Int("myflag"), 12) +} + +func TestContext_Duration(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Duration("myflag", time.Duration(12*time.Second), "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.Duration("myflag"), time.Duration(12*time.Second)) +} + +func TestContext_String(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.String("myflag", "hello world", "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.String("myflag"), "hello world") +} + +func TestContext_Bool(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.Bool("myflag"), false) +} + +func TestContext_BoolT(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", true, "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.BoolT("myflag"), true) +} + +func TestContext_Args(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + c := cli.NewContext(nil, set, set) + set.Parse([]string{"--myflag", "bat", "baz"}) + expect(t, len(c.Args()), 2) + expect(t, c.Bool("myflag"), true) +} + +func TestContext_IsSet(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + set.String("otherflag", "hello world", "doc") + c := cli.NewContext(nil, set, set) + set.Parse([]string{"--myflag", "bat", "baz"}) + expect(t, c.IsSet("myflag"), true) + expect(t, c.IsSet("otherflag"), false) + expect(t, c.IsSet("bogusflag"), false) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go b/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go new file mode 100644 index 00000000000..b30bca3019b --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go @@ -0,0 +1,410 @@ +package cli + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +// This flag enables bash-completion for all commands and subcommands +var BashCompletionFlag = BoolFlag{ + Name: "generate-bash-completion", +} + +// This flag prints the version for the application +var VersionFlag = BoolFlag{ + Name: "version, v", + Usage: "print the version", +} + +// This flag prints the help for all commands and subcommands +var HelpFlag = BoolFlag{ + Name: "help, h", + Usage: "show help", +} + +// Flag is a common interface related to parsing flags in cli. +// For more advanced flag parsing techniques, it is recomended that +// this interface be implemented. +type Flag interface { + fmt.Stringer + // Apply Flag settings to the given flag set + Apply(*flag.FlagSet) + getName() string +} + +func flagSet(name string, flags []Flag) *flag.FlagSet { + set := flag.NewFlagSet(name, flag.ContinueOnError) + + for _, f := range flags { + f.Apply(set) + } + return set +} + +func eachName(longName string, fn func(string)) { + parts := strings.Split(longName, ",") + for _, name := range parts { + name = strings.Trim(name, " ") + fn(name) + } +} + +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// GenericFlag is the flag type for types implementing Generic +type GenericFlag struct { + Name string + Value Generic + Usage string + EnvVar string +} + +func (f GenericFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage)) +} + +func (f GenericFlag) Apply(set *flag.FlagSet) { + val := f.Value + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + val.Set(envVal) + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) +} + +func (f GenericFlag) getName() string { + return f.Name +} + +type StringSlice []string + +func (f *StringSlice) Set(value string) error { + *f = append(*f, value) + return nil +} + +func (f *StringSlice) String() string { + return fmt.Sprintf("%s", *f) +} + +func (f *StringSlice) Value() []string { + return *f +} + +type StringSliceFlag struct { + Name string + Value *StringSlice + Usage string + EnvVar string +} + +func (f StringSliceFlag) String() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) +} + +func (f StringSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + newVal.Set(s) + } + f.Value = newVal + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) +} + +func (f StringSliceFlag) getName() string { + return f.Name +} + +type IntSlice []int + +func (f *IntSlice) Set(value string) error { + + tmp, err := strconv.Atoi(value) + if err != nil { + return err + } else { + *f = append(*f, tmp) + } + return nil +} + +func (f *IntSlice) String() string { + return fmt.Sprintf("%d", *f) +} + +func (f *IntSlice) Value() []int { + return *f +} + +type IntSliceFlag struct { + Name string + Value *IntSlice + Usage string + EnvVar string +} + +func (f IntSliceFlag) String() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) +} + +func (f IntSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + err := newVal.Set(s) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + } + } + f.Value = newVal + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) +} + +func (f IntSliceFlag) getName() string { + return f.Name +} + +type BoolFlag struct { + Name string + Usage string + EnvVar string +} + +func (f BoolFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) +} + +func (f BoolFlag) Apply(set *flag.FlagSet) { + val := false + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + } + } + + eachName(f.Name, func(name string) { + set.Bool(name, val, f.Usage) + }) +} + +func (f BoolFlag) getName() string { + return f.Name +} + +type BoolTFlag struct { + Name string + Usage string + EnvVar string +} + +func (f BoolTFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) +} + +func (f BoolTFlag) Apply(set *flag.FlagSet) { + val := true + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + } + } + + eachName(f.Name, func(name string) { + set.Bool(name, val, f.Usage) + }) +} + +func (f BoolTFlag) getName() string { + return f.Name +} + +type StringFlag struct { + Name string + Value string + Usage string + EnvVar string +} + +func (f StringFlag) String() string { + var fmtString string + fmtString = "%s %v\t%v" + + if len(f.Value) > 0 { + fmtString = "%s '%v'\t%v" + } else { + fmtString = "%s %v\t%v" + } + + return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) +} + +func (f StringFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + f.Value = envVal + } + } + + eachName(f.Name, func(name string) { + set.String(name, f.Value, f.Usage) + }) +} + +func (f StringFlag) getName() string { + return f.Name +} + +type IntFlag struct { + Name string + Value int + Usage string + EnvVar string +} + +func (f IntFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +func (f IntFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValInt, err := strconv.ParseUint(envVal, 10, 64) + if err == nil { + f.Value = int(envValInt) + } + } + } + + eachName(f.Name, func(name string) { + set.Int(name, f.Value, f.Usage) + }) +} + +func (f IntFlag) getName() string { + return f.Name +} + +type DurationFlag struct { + Name string + Value time.Duration + Usage string + EnvVar string +} + +func (f DurationFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +func (f DurationFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValDuration, err := time.ParseDuration(envVal) + if err == nil { + f.Value = envValDuration + } + } + } + + eachName(f.Name, func(name string) { + set.Duration(name, f.Value, f.Usage) + }) +} + +func (f DurationFlag) getName() string { + return f.Name +} + +type Float64Flag struct { + Name string + Value float64 + Usage string + EnvVar string +} + +func (f Float64Flag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +func (f Float64Flag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err == nil { + f.Value = float64(envValFloat) + } + } + } + + eachName(f.Name, func(name string) { + set.Float64(name, f.Value, f.Usage) + }) +} + +func (f Float64Flag) getName() string { + return f.Name +} + +func prefixFor(name string) (prefix string) { + if len(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} + +func prefixedNames(fullName string) (prefixed string) { + parts := strings.Split(fullName, ",") + for i, name := range parts { + name = strings.Trim(name, " ") + prefixed += prefixFor(name) + name + if i < len(parts)-1 { + prefixed += ", " + } + } + return +} + +func withEnvHint(envVar, str string) string { + envText := "" + if envVar != "" { + envText = fmt.Sprintf(" [$%s]", envVar) + } + return str + envText +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go new file mode 100644 index 00000000000..bc5059ca17e --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go @@ -0,0 +1,587 @@ +package cli_test + +import ( + "fmt" + "os" + "reflect" + "strings" + "testing" + + "github.com/codegangsta/cli" +) + +var boolFlagTests = []struct { + name string + expected string +}{ + {"help", "--help\t"}, + {"h", "-h\t"}, +} + +func TestBoolFlagHelpOutput(t *testing.T) { + + for _, test := range boolFlagTests { + flag := cli.BoolFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +var stringFlagTests = []struct { + name string + value string + expected string +}{ + {"help", "", "--help \t"}, + {"h", "", "-h \t"}, + {"h", "", "-h \t"}, + {"test", "Something", "--test 'Something'\t"}, +} + +func TestStringFlagHelpOutput(t *testing.T) { + + for _, test := range stringFlagTests { + flag := cli.StringFlag{Name: test.name, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_FOO", "derp") + for _, test := range stringFlagTests { + flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_FOO]") { + t.Errorf("%s does not end with [$APP_FOO]", output) + } + } +} + +var stringSliceFlagTests = []struct { + name string + value *cli.StringSlice + expected string +}{ + {"help", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "--help '--help option --help option'\t"}, + {"h", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "-h '-h option -h option'\t"}, + {"h", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "-h '-h option -h option'\t"}, + {"test", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("Something") + return s + }(), "--test '--test option --test option'\t"}, +} + +func TestStringSliceFlagHelpOutput(t *testing.T) { + + for _, test := range stringSliceFlagTests { + flag := cli.StringSliceFlag{Name: test.name, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_QWWX", "11,4") + for _, test := range stringSliceFlagTests { + flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_QWWX]") { + t.Errorf("%q does not end with [$APP_QWWX]", output) + } + } +} + +var intFlagTests = []struct { + name string + expected string +}{ + {"help", "--help '0'\t"}, + {"h", "-h '0'\t"}, +} + +func TestIntFlagHelpOutput(t *testing.T) { + + for _, test := range intFlagTests { + flag := cli.IntFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAR", "2") + for _, test := range intFlagTests { + flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAR]") { + t.Errorf("%s does not end with [$APP_BAR]", output) + } + } +} + +var durationFlagTests = []struct { + name string + expected string +}{ + {"help", "--help '0'\t"}, + {"h", "-h '0'\t"}, +} + +func TestDurationFlagHelpOutput(t *testing.T) { + + for _, test := range durationFlagTests { + flag := cli.DurationFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAR", "2h3m6s") + for _, test := range durationFlagTests { + flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAR]") { + t.Errorf("%s does not end with [$APP_BAR]", output) + } + } +} + +var intSliceFlagTests = []struct { + name string + value *cli.IntSlice + expected string +}{ + {"help", &cli.IntSlice{}, "--help '--help option --help option'\t"}, + {"h", &cli.IntSlice{}, "-h '-h option -h option'\t"}, + {"h", &cli.IntSlice{}, "-h '-h option -h option'\t"}, + {"test", func() *cli.IntSlice { + i := &cli.IntSlice{} + i.Set("9") + return i + }(), "--test '--test option --test option'\t"}, +} + +func TestIntSliceFlagHelpOutput(t *testing.T) { + + for _, test := range intSliceFlagTests { + flag := cli.IntSliceFlag{Name: test.name, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_SMURF", "42,3") + for _, test := range intSliceFlagTests { + flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_SMURF]") { + t.Errorf("%q does not end with [$APP_SMURF]", output) + } + } +} + +var float64FlagTests = []struct { + name string + expected string +}{ + {"help", "--help '0'\t"}, + {"h", "-h '0'\t"}, +} + +func TestFloat64FlagHelpOutput(t *testing.T) { + + for _, test := range float64FlagTests { + flag := cli.Float64Flag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAZ", "99.4") + for _, test := range float64FlagTests { + flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAZ]") { + t.Errorf("%s does not end with [$APP_BAZ]", output) + } + } +} + +var genericFlagTests = []struct { + name string + value cli.Generic + expected string +}{ + {"help", &Parser{}, "--help \t`-help option -help option` "}, + {"h", &Parser{}, "-h \t`-h option -h option` "}, + {"test", &Parser{}, "--test \t`-test option -test option` "}, +} + +func TestGenericFlagHelpOutput(t *testing.T) { + + for _, test := range genericFlagTests { + flag := cli.GenericFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_ZAP", "3") + for _, test := range genericFlagTests { + flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_ZAP]") { + t.Errorf("%s does not end with [$APP_ZAP]", output) + } + } +} + +func TestParseMultiString(t *testing.T) { + (&cli.App{ + Flags: []cli.Flag{ + cli.StringFlag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.String("serve") != "10" { + t.Errorf("main name not set") + } + if ctx.String("s") != "10" { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run", "-s", "10"}) +} + +func TestParseMultiStringFromEnv(t *testing.T) { + os.Setenv("APP_COUNT", "20") + (&cli.App{ + Flags: []cli.Flag{ + cli.StringFlag{Name: "count, c", EnvVar: "APP_COUNT"}, + }, + Action: func(ctx *cli.Context) { + if ctx.String("count") != "20" { + t.Errorf("main name not set") + } + if ctx.String("c") != "20" { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run"}) +} + +func TestParseMultiStringSlice(t *testing.T) { + (&cli.App{ + Flags: []cli.Flag{ + cli.StringSliceFlag{Name: "serve, s", Value: &cli.StringSlice{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiStringSliceFromEnv(t *testing.T) { + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + +func TestParseMultiInt(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.IntFlag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Int("serve") != 10 { + t.Errorf("main name not set") + } + if ctx.Int("s") != 10 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "-s", "10"}) +} + +func TestParseMultiIntFromEnv(t *testing.T) { + os.Setenv("APP_TIMEOUT_SECONDS", "10") + a := cli.App{ + Flags: []cli.Flag{ + cli.IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Int("timeout") != 10 { + t.Errorf("main name not set") + } + if ctx.Int("t") != 10 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + +func TestParseMultiIntSlice(t *testing.T) { + (&cli.App{ + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "serve, s", Value: &cli.IntSlice{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiIntSliceFromEnv(t *testing.T) { + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + +func TestParseMultiFloat64(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.Float64Flag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Float64("serve") != 10.2 { + t.Errorf("main name not set") + } + if ctx.Float64("s") != 10.2 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "-s", "10.2"}) +} + +func TestParseMultiFloat64FromEnv(t *testing.T) { + os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + a := cli.App{ + Flags: []cli.Flag{ + cli.Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Float64("timeout") != 15.5 { + t.Errorf("main name not set") + } + if ctx.Float64("t") != 15.5 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + +func TestParseMultiBool(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolFlag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Bool("serve") != true { + t.Errorf("main name not set") + } + if ctx.Bool("s") != true { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "--serve"}) +} + +func TestParseMultiBoolFromEnv(t *testing.T) { + os.Setenv("APP_DEBUG", "1") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Bool("debug") != true { + t.Errorf("main name not set from env") + } + if ctx.Bool("d") != true { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + +func TestParseMultiBoolT(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolTFlag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.BoolT("serve") != true { + t.Errorf("main name not set") + } + if ctx.BoolT("s") != true { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "--serve"}) +} + +func TestParseMultiBoolTFromEnv(t *testing.T) { + os.Setenv("APP_DEBUG", "0") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.BoolT("debug") != false { + t.Errorf("main name not set from env") + } + if ctx.BoolT("d") != false { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + +type Parser [2]string + +func (p *Parser) Set(value string) error { + parts := strings.Split(value, ",") + if len(parts) != 2 { + return fmt.Errorf("invalid format") + } + + (*p)[0] = parts[0] + (*p)[1] = parts[1] + + return nil +} + +func (p *Parser) String() string { + return fmt.Sprintf("%s,%s", p[0], p[1]) +} + +func TestParseGeneric(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.GenericFlag{Name: "serve, s", Value: &Parser{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "-s", "10,20"}) +} + +func TestParseGenericFromEnv(t *testing.T) { + os.Setenv("APP_SERVE", "20,30") + a := cli.App{ + Flags: []cli.Flag{ + cli.GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/help.go b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go new file mode 100644 index 00000000000..5020cb6f3ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go @@ -0,0 +1,224 @@ +package cli + +import ( + "fmt" + "os" + "text/tabwriter" + "text/template" +) + +// The text template for the Default help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...] + +VERSION: + {{.Version}}{{if or .Author .Email}} + +AUTHOR:{{if .Author}} + {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} + {{.Email}}{{end}}{{end}} + +COMMANDS: + {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}}{{if .Flags}} +GLOBAL OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{end}} +` + +// The text template for the command help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var CommandHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .Flags}} + +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{ end }} +` + +// The text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] + +COMMANDS: + {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}}{{if .Flags}} +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{end}} +` + +var helpCommand = Command{ + Name: "help", + ShortName: "h", + Usage: "Shows a list of commands or help for one command", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowAppHelp(c) + } + }, +} + +var helpSubcommand = Command{ + Name: "help", + ShortName: "h", + Usage: "Shows a list of commands or help for one command", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowSubcommandHelp(c) + } + }, +} + +// Prints help for the App +var HelpPrinter = printHelp + +// Prints version for the App +var VersionPrinter = printVersion + +func ShowAppHelp(c *Context) { + HelpPrinter(AppHelpTemplate, c.App) +} + +// Prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + for _, command := range c.App.Commands { + fmt.Println(command.Name) + if command.ShortName != "" { + fmt.Println(command.ShortName) + } + } +} + +// Prints help for the given command +func ShowCommandHelp(c *Context, command string) { + for _, c := range c.App.Commands { + if c.HasName(command) { + HelpPrinter(CommandHelpTemplate, c) + return + } + } + + if c.App.CommandNotFound != nil { + c.App.CommandNotFound(c, command) + } else { + fmt.Printf("No help topic for '%v'\n", command) + } +} + +// Prints help for the given subcommand +func ShowSubcommandHelp(c *Context) { + HelpPrinter(SubcommandHelpTemplate, c.App) +} + +// Prints the version number of the App +func ShowVersion(c *Context) { + VersionPrinter(c) +} + +func printVersion(c *Context) { + fmt.Printf("%v version %v\n", c.App.Name, c.App.Version) +} + +// Prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// Prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil && c.BashComplete != nil { + c.BashComplete(ctx) + } +} + +func printHelp(templ string, data interface{}) { + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + t := template.Must(template.New("help").Parse(templ)) + err := t.Execute(w, data) + if err != nil { + panic(err) + } + w.Flush() +} + +func checkVersion(c *Context) bool { + if c.GlobalBool("version") { + ShowVersion(c) + return true + } + + return false +} + +func checkHelp(c *Context) bool { + if c.GlobalBool("h") || c.GlobalBool("help") { + ShowAppHelp(c) + return true + } + + return false +} + +func checkCommandHelp(c *Context, name string) bool { + if c.Bool("h") || c.Bool("help") { + ShowCommandHelp(c, name) + return true + } + + return false +} + +func checkSubcommandHelp(c *Context) bool { + if c.GlobalBool("h") || c.GlobalBool("help") { + ShowSubcommandHelp(c) + return true + } + + return false +} + +func checkCompletions(c *Context) bool { + if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCompletions(c) + return true + } + + return false +} + +func checkCommandCompletions(c *Context, name string) bool { + if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCommandCompletions(c, name) + return true + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go new file mode 100644 index 00000000000..cdc4feb2fcd --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go @@ -0,0 +1,19 @@ +package cli_test + +import ( + "reflect" + "testing" +) + +/* Test Helpers */ +func expect(t *testing.T, a interface{}, b interface{}) { + if a != b { + t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} + +func refute(t *testing.T, a interface{}, b interface{}) { + if a == b { + t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore new file mode 100644 index 00000000000..00268614f04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 00000000000..8687342e9d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.1 + - 1.2 + - tip diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 00000000000..b003eca0ca1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Joachim Bauch + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE new file mode 100644 index 00000000000..9171c972252 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md new file mode 100644 index 00000000000..9ca3ca8eb7f --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md @@ -0,0 +1,59 @@ +# Gorilla WebSocket + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +### Documentation + +* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
gorillago.net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Limit size of received messageYesNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
+ +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/code.google.com/p/go.net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered, Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/bench_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/bench_test.go new file mode 100644 index 00000000000..f66fc36bc87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/bench_test.go @@ -0,0 +1,19 @@ +// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "testing" +) + +func BenchmarkMaskBytes(b *testing.B) { + var key [4]byte + data := make([]byte, 1024) + pos := 0 + for i := 0; i < b.N; i++ { + pos = maskBytes(key, pos, data) + } + b.SetBytes(int64(len(data))) +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/client.go b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go new file mode 100644 index 00000000000..3b5cac4612c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go @@ -0,0 +1,245 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/tls" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. Use requestHeader to specify +// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies +// (Cookie). Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + acceptKey := computeAcceptKey(challengeKey) + + c = newConn(netConn, false, readBufSize, writeBufSize) + p := c.writeBuf[:0] + p = append(p, "GET "...) + p = append(p, u.RequestURI()...) + p = append(p, " HTTP/1.1\r\nHost: "...) + p = append(p, u.Host...) + // "Upgrade" is capitalized for servers that do not use case insensitive + // comparisons on header tokens. + p = append(p, "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...) + p = append(p, challengeKey...) + p = append(p, "\r\n"...) + for k, vs := range requestHeader { + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + p = append(p, v...) + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + if _, err := netConn.Write(p); err != nil { + return nil, nil, err + } + + resp, err := http.ReadResponse(c.br, &http.Request{Method: "GET", URL: u}) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != acceptKey { + return nil, resp, ErrBadHandshake + } + c.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + return c, resp, nil +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // Input and output buffer sizes. If the buffer size is zero, then a + // default value of 4096 is used. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +// parseURL parses the URL. The url.Parse function is not used here because +// url.Parse mangles the path. +func parseURL(s string) (*url.URL, error) { + // From the RFC: + // + // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] + // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] + // + // We don't use the net/url parser here because the dialer interface does + // not provide a way for applications to work around percent deocding in + // the net/url parser. + + var u url.URL + switch { + case strings.HasPrefix(s, "ws://"): + u.Scheme = "ws" + s = s[len("ws://"):] + case strings.HasPrefix(s, "wss://"): + u.Scheme = "wss" + s = s[len("wss://"):] + default: + return nil, errMalformedURL + } + + u.Host = s + u.Opaque = "/" + if i := strings.Index(s, "/"); i >= 0 { + u.Host = s[:i] + u.Opaque = s[i:] + } + + return &u, nil +} + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + if u.Scheme == "wss" { + hostPort += ":443" + } else { + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default zero values. +var DefaultDialer *Dialer + +// Dial creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + u, err := parseURL(urlStr) + if err != nil { + return nil, nil, err + } + + hostPort, hostNoPort := hostPortNoPort(u) + + if d == nil { + d = &Dialer{} + } + + var deadline time.Time + if d.HandshakeTimeout != 0 { + deadline = time.Now().Add(d.HandshakeTimeout) + } + + netDial := d.NetDial + if netDial == nil { + netDialer := &net.Dialer{Deadline: deadline} + netDial = netDialer.Dial + } + + netConn, err := netDial("tcp", hostPort) + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if err := netConn.SetDeadline(deadline); err != nil { + return nil, nil, err + } + + if u.Scheme == "wss" { + cfg := d.TLSClientConfig + if cfg == nil { + cfg = &tls.Config{ServerName: hostNoPort} + } else if cfg.ServerName == "" { + shallowCopy := *cfg + cfg = &shallowCopy + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + if err := tlsConn.Handshake(); err != nil { + return nil, nil, err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return nil, nil, err + } + } + } + + readBufferSize := d.ReadBufferSize + if readBufferSize == 0 { + readBufferSize = 4096 + } + + writeBufferSize := d.WriteBufferSize + if writeBufferSize == 0 { + writeBufferSize = 4096 + } + + if len(d.Subprotocols) > 0 { + h := http.Header{} + for k, v := range requestHeader { + h[k] = v + } + h.Set("Sec-Websocket-Protocol", strings.Join(d.Subprotocols, ", ")) + requestHeader = h + } + + conn, resp, err := NewClient(netConn, u, requestHeader, readBufferSize, writeBufferSize) + if err != nil { + return nil, resp, err + } + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/client_server_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/client_server_test.go new file mode 100644 index 00000000000..8c608f68c4b --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/client_server_test.go @@ -0,0 +1,249 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/tls" + "crypto/x509" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" +) + +var cstUpgrader = Upgrader{ + Subprotocols: []string{"p0", "p1"}, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { + http.Error(w, reason.Error(), status) + }, +} + +var cstDialer = Dialer{ + Subprotocols: []string{"p1", "p2"}, + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +type cstHandler struct{ *testing.T } + +type Server struct { + *httptest.Server + URL string +} + +func newServer(t *testing.T) *Server { + var s Server + s.Server = httptest.NewServer(cstHandler{t}) + s.URL = "ws" + s.Server.URL[len("http"):] + return &s +} + +func newTLSServer(t *testing.T) *Server { + var s Server + s.Server = httptest.NewTLSServer(cstHandler{t}) + s.URL = "ws" + s.Server.URL[len("http"):] + return &s +} + +func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Logf("method %s not allowed", r.Method) + http.Error(w, "method not allowed", 405) + return + } + subprotos := Subprotocols(r) + if !reflect.DeepEqual(subprotos, cstDialer.Subprotocols) { + t.Logf("subprotols=%v, want %v", subprotos, cstDialer.Subprotocols) + http.Error(w, "bad protocol", 400) + return + } + ws, err := cstUpgrader.Upgrade(w, r, http.Header{"Set-Cookie": {"sessionID=1234"}}) + if err != nil { + t.Logf("Upgrade: %v", err) + return + } + defer ws.Close() + + if ws.Subprotocol() != "p1" { + t.Logf("Subprotocol() = %s, want p1", ws.Subprotocol()) + ws.Close() + return + } + op, rd, err := ws.NextReader() + if err != nil { + t.Logf("NextReader: %v", err) + return + } + wr, err := ws.NextWriter(op) + if err != nil { + t.Logf("NextWriter: %v", err) + return + } + if _, err = io.Copy(wr, rd); err != nil { + t.Logf("NextWriter: %v", err) + return + } + if err := wr.Close(); err != nil { + t.Logf("Close: %v", err) + return + } +} + +func sendRecv(t *testing.T, ws *Conn) { + const message = "Hello World!" + if err := ws.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf("SetWriteDeadline: %v", err) + } + if err := ws.WriteMessage(TextMessage, []byte(message)); err != nil { + t.Fatalf("WriteMessage: %v", err) + } + if err := ws.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + _, p, err := ws.ReadMessage() + if err != nil { + t.Fatalf("ReadMessage: %v", err) + } + if string(p) != message { + t.Fatalf("message=%s, want %s", p, message) + } +} + +func TestDial(t *testing.T) { + s := newServer(t) + defer s.Close() + + ws, _, err := cstDialer.Dial(s.URL, nil) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer ws.Close() + sendRecv(t, ws) +} + +func TestDialTLS(t *testing.T) { + s := newTLSServer(t) + defer s.Close() + + certs := x509.NewCertPool() + for _, c := range s.TLS.Certificates { + roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1]) + if err != nil { + t.Fatalf("error parsing server's root cert: %v", err) + } + for _, root := range roots { + certs.AddCert(root) + } + } + + u, _ := url.Parse(s.URL) + d := cstDialer + d.NetDial = func(network, addr string) (net.Conn, error) { return net.Dial(network, u.Host) } + d.TLSClientConfig = &tls.Config{RootCAs: certs} + ws, _, err := d.Dial("wss://example.com/", nil) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer ws.Close() + sendRecv(t, ws) +} + +func xTestDialTLSBadCert(t *testing.T) { + s := newTLSServer(t) + defer s.Close() + + ws, _, err := cstDialer.Dial(s.URL, nil) + if err == nil { + ws.Close() + t.Fatalf("Dial: nil") + } +} + +func xTestDialTLSNoVerify(t *testing.T) { + s := newTLSServer(t) + defer s.Close() + + d := cstDialer + d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + ws, _, err := d.Dial(s.URL, nil) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer ws.Close() + sendRecv(t, ws) +} + +func TestDialTimeout(t *testing.T) { + s := newServer(t) + defer s.Close() + + d := cstDialer + d.HandshakeTimeout = -1 + ws, _, err := d.Dial(s.URL, nil) + if err == nil { + ws.Close() + t.Fatalf("Dial: nil") + } +} + +func TestDialBadScheme(t *testing.T) { + s := newServer(t) + defer s.Close() + + ws, _, err := cstDialer.Dial(s.Server.URL, nil) + if err == nil { + ws.Close() + t.Fatalf("Dial: nil") + } +} + +func TestDialBadOrigin(t *testing.T) { + s := newServer(t) + defer s.Close() + + ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {"bad"}}) + if err == nil { + ws.Close() + t.Fatalf("Dial: nil") + } + if resp == nil { + t.Fatalf("resp=nil, err=%v", err) + } + if resp.StatusCode != http.StatusForbidden { + t.Fatalf("status=%d, want %d", resp.StatusCode, http.StatusForbidden) + } +} + +func TestHandshake(t *testing.T) { + s := newServer(t) + defer s.Close() + + ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {s.URL}}) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer ws.Close() + + var sessionID string + for _, c := range resp.Cookies() { + if c.Name == "sessionID" { + sessionID = c.Value + } + } + if sessionID != "1234" { + t.Error("Set-Cookie not received from the server.") + } + + if ws.Subprotocol() != "p1" { + t.Errorf("ws.Subprotocol() = %s, want p1", ws.Subprotocol()) + } + sendRecv(t, ws) +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/client_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/client_test.go new file mode 100644 index 00000000000..d2f2ebd798b --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/client_test.go @@ -0,0 +1,63 @@ +// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "net/url" + "reflect" + "testing" +) + +var parseURLTests = []struct { + s string + u *url.URL +}{ + {"ws://example.com/", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}}, + {"ws://example.com", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}}, + {"ws://example.com:7777/", &url.URL{Scheme: "ws", Host: "example.com:7777", Opaque: "/"}}, + {"wss://example.com/", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/"}}, + {"wss://example.com/a/b", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/a/b"}}, + {"ss://example.com/a/b", nil}, +} + +func TestParseURL(t *testing.T) { + for _, tt := range parseURLTests { + u, err := parseURL(tt.s) + if tt.u != nil && err != nil { + t.Errorf("parseURL(%q) returned error %v", tt.s, err) + continue + } + if tt.u == nil && err == nil { + t.Errorf("parseURL(%q) did not return error", tt.s) + continue + } + if !reflect.DeepEqual(u, tt.u) { + t.Errorf("parseURL(%q) returned %v, want %v", tt.s, u, tt.u) + continue + } + } +} + +var hostPortNoPortTests = []struct { + u *url.URL + hostPort, hostNoPort string +}{ + {&url.URL{Scheme: "ws", Host: "example.com"}, "example.com:80", "example.com"}, + {&url.URL{Scheme: "wss", Host: "example.com"}, "example.com:443", "example.com"}, + {&url.URL{Scheme: "ws", Host: "example.com:7777"}, "example.com:7777", "example.com"}, + {&url.URL{Scheme: "wss", Host: "example.com:7777"}, "example.com:7777", "example.com"}, +} + +func TestHostPortNoPort(t *testing.T) { + for _, tt := range hostPortNoPortTests { + hostPort, hostNoPort := hostPortNoPort(tt.u) + if hostPort != tt.hostPort { + t.Errorf("hostPortNoPort(%v) returned hostPort %q, want %q", tt.u, hostPort, tt.hostPort) + } + if hostNoPort != tt.hostNoPort { + t.Errorf("hostPortNoPort(%v) returned hostNoPort %q, want %q", tt.u, hostNoPort, tt.hostNoPort) + } + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go new file mode 100644 index 00000000000..270114285ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go @@ -0,0 +1,807 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "time" +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +var ( + continuationFrame = 0 + noFrame = -1 +) + +var ( + // ErrCloseSent is returned when the application writes a message to the + // connection after sending a close message. + ErrCloseSent = errors.New("websocket: close sent") + + // ErrReadLimit is returned when reading a message that is larger than the + // read limit set for the connection. + ErrReadLimit = errors.New("websocket: read limit exceeded") +) + +type websocketError struct { + msg string + temporary bool + timeout bool +} + +func (e *websocketError) Error() string { return e.msg } +func (e *websocketError) Temporary() bool { return e.temporary } +func (e *websocketError) Timeout() bool { return e.timeout } + +var ( + errWriteTimeout = &websocketError{msg: "websocket: write timeout", timeout: true} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +const ( + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + finalBit = 1 << 7 + maskBit = 1 << 7 + writeWait = time.Second +) + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = struct{ error }{err} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +// Conn represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan bool // used as mutex to protect write to conn and closeSent + closeSent bool // true if close message was sent + + // Message writer fields. + writeErr error + writeBuf []byte // frame is constructed in this buffer. + writePos int // end of data in writeBuf. + writeFrameType int // type of the current frame. + writeSeq int // incremented to invalidate message writers. + writeDeadline time.Time + + // Read fields + readErr error + br *bufio.Reader + readRemaining int64 // bytes remaining in current frame. + readFinal bool // true the current message has more frames. + readSeq int // incremented to invalidate message readers. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error +} + +func newConn(conn net.Conn, isServer bool, readBufSize, writeBufSize int) *Conn { + mu := make(chan bool, 1) + mu <- true + + c := &Conn{ + isServer: isServer, + br: bufio.NewReaderSize(conn, readBufSize), + conn: conn, + mu: mu, + readFinal: true, + writeBuf: make([]byte, writeBufSize+maxFrameHeaderSize), + writeFrameType: noFrame, + writePos: maxFrameHeaderSize, + } + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting for a close frame. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { + <-c.mu + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if frameType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + for _, buf := range bufs { + if len(buf) > 0 { + n, err := c.conn.Write(buf) + if n != len(buf) { + // Close on partial write. + c.conn.Close() + } + if err != nil { + return err + } + } + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := time.Hour * 1000 + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if messageType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + n, err := c.conn.Write(buf) + if n != 0 && n != len(buf) { + c.conn.Close() + } + return err +} + +// NextWriter returns a writer for the next message to send. The writer's +// Close method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +// +// The NextWriter method and the writers returned from the method cannot be +// accessed by more than one goroutine at a time. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + if c.writeErr != nil { + return nil, c.writeErr + } + + if c.writeFrameType != noFrame { + if err := c.flushFrame(true, nil); err != nil { + return nil, err + } + } + + if !isControl(messageType) && !isData(messageType) { + return nil, errBadWriteOpCode + } + + c.writeFrameType = messageType + return messageWriter{c, c.writeSeq}, nil +} + +func (c *Conn) flushFrame(final bool, extra []byte) error { + length := c.writePos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(c.writeFrameType) && + (!final || length > maxControlFramePayloadSize) { + c.writeSeq++ + c.writeFrameType = noFrame + c.writePos = maxFrameHeaderSize + return errInvalidControlFrame + } + + b0 := byte(c.writeFrameType) + if final { + b0 |= finalBit + } + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos]) + if len(extra) > 0 { + c.writeErr = errors.New("websocket: internal error, extra used in client mode") + return c.writeErr + } + } + + // Write the buffers to the connection. + c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra) + + // Setup for next frame. + c.writePos = maxFrameHeaderSize + c.writeFrameType = continuationFrame + if final { + c.writeSeq++ + c.writeFrameType = noFrame + } + return c.writeErr +} + +type messageWriter struct { + c *Conn + seq int +} + +func (w messageWriter) err() error { + c := w.c + if c.writeSeq != w.seq { + return errWriteClosed + } + if c.writeErr != nil { + return c.writeErr + } + return nil +} + +func (w messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.c.writePos + if n <= 0 { + if err := w.c.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.c.writePos + } + if n > max { + n = max + } + return n, nil +} + +func (w messageWriter) write(final bool, p []byte) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.c.flushFrame(final, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) Write(p []byte) (int, error) { + return w.write(false, p) +} + +func (w messageWriter) WriteString(p string) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if err := w.err(); err != nil { + return 0, err + } + for { + if w.c.writePos == len(w.c.writeBuf) { + err = w.c.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.c.writePos:]) + w.c.writePos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w messageWriter) Close() error { + if err := w.err(); err != nil { + return err + } + return w.c.flushFrame(true, nil) +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + wr, err := c.NextWriter(messageType) + if err != nil { + return err + } + w := wr.(messageWriter) + if _, err := w.write(true, data); err != nil { + return err + } + if c.writeSeq == w.seq { + if err := c.flushFrame(true, nil); err != nil { + return err + } + } + return nil +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +// readFull is like io.ReadFull except that io.EOF is never returned. +func (c *Conn) readFull(p []byte) (err error) { + var n int + for n < len(p) && err == nil { + var nn int + nn, err = c.br.Read(p[n:]) + n += nn + } + if n == len(p) { + err = nil + } else if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return +} + +func (c *Conn) advanceFrame() (int, error) { + + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + var b [8]byte + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + + final := b[0]&finalBit != 0 + frameType := int(b[0] & 0xf) + reserved := int((b[0] >> 4) & 0x7) + mask := b[1]&maskBit != 0 + c.readRemaining = int64(b[1] & 0x7f) + + if reserved != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length. + + switch c.readRemaining { + case 126: + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) + case 127: + if err := c.readFull(b[:8]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + if err := c.readFull(c.readMaskKey[:]); err != nil { + return noFrame, err + } + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload = make([]byte, c.readRemaining) + c.readRemaining = 0 + if err := c.readFull(payload); err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + c.WriteControl(CloseMessage, []byte{}, time.Now().Add(writeWait)) + if len(payload) < 2 { + return noFrame, io.EOF + } + closeCode := binary.BigEndian.Uint16(payload) + switch closeCode { + case CloseNormalClosure, CloseGoingAway: + return noFrame, io.EOF + default: + return noFrame, errors.New("websocket: close " + + strconv.Itoa(int(closeCode)) + " " + + string(payload[2:])) + } + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// The NextReader method and the readers returned from the method cannot be +// accessed by more than one goroutine at a time. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + + c.readSeq++ + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + if frameType == TextMessage || frameType == BinaryMessage { + return frameType, messageReader{c, c.readSeq}, nil + } + } + return noFrame, nil, c.readErr +} + +type messageReader struct { + c *Conn + seq int +} + +func (r messageReader) Read(b []byte) (int, error) { + + if r.seq != r.c.readSeq { + return 0, io.EOF + } + + for r.c.readErr == nil { + + if r.c.readRemaining > 0 { + if int64(len(b)) > r.c.readRemaining { + b = b[:r.c.readRemaining] + } + n, err := r.c.br.Read(b) + r.c.readErr = hideTempErr(err) + if r.c.isServer { + r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) + } + r.c.readRemaining -= int64(n) + return n, r.c.readErr + } + + if r.c.readFinal { + r.c.readSeq++ + return 0, io.EOF + } + + frameType, err := r.c.advanceFrame() + switch { + case err != nil: + r.c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := r.c.readErr + if err == io.EOF && r.seq == r.c.readSeq { + err = io.ErrUnexpectedEOF + } + return 0, err +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size for a message read from the peer. If a +// message exceeds the limit, the connection sends a close frame to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The default ping handler sends a pong to the peer. +func (c *Conn) SetPingHandler(h func(string) error) { + if h == nil { + h = func(message string) error { + c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + return nil + } + } + c.handlePing = h +} + +// SetPongHandler sets then handler for pong messages received from the peer. +// The default pong handler does nothing. +func (c *Conn) SetPongHandler(h func(string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +func FormatCloseMessage(closeCode int, text string) []byte { + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/conn_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/conn_test.go new file mode 100644 index 00000000000..632725a170e --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/conn_test.go @@ -0,0 +1,238 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "testing" + "testing/iotest" + "time" +) + +var _ net.Error = errWriteTimeout + +type fakeNetConn struct { + io.Reader + io.Writer +} + +func (c fakeNetConn) Close() error { return nil } +func (c fakeNetConn) LocalAddr() net.Addr { return nil } +func (c fakeNetConn) RemoteAddr() net.Addr { return nil } +func (c fakeNetConn) SetDeadline(t time.Time) error { return nil } +func (c fakeNetConn) SetReadDeadline(t time.Time) error { return nil } +func (c fakeNetConn) SetWriteDeadline(t time.Time) error { return nil } + +func TestFraming(t *testing.T) { + frameSizes := []int{0, 1, 2, 124, 125, 126, 127, 128, 129, 65534, 65535, 65536, 65537} + var readChunkers = []struct { + name string + f func(io.Reader) io.Reader + }{ + {"half", iotest.HalfReader}, + {"one", iotest.OneByteReader}, + {"asis", func(r io.Reader) io.Reader { return r }}, + } + + writeBuf := make([]byte, 65537) + for i := range writeBuf { + writeBuf[i] = byte(i) + } + + for _, isServer := range []bool{true, false} { + for _, chunker := range readChunkers { + + var connBuf bytes.Buffer + wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024) + rc := newConn(fakeNetConn{Reader: chunker.f(&connBuf), Writer: nil}, !isServer, 1024, 1024) + + for _, n := range frameSizes { + for _, iocopy := range []bool{true, false} { + name := fmt.Sprintf("s:%v, r:%s, n:%d c:%v", isServer, chunker.name, n, iocopy) + + w, err := wc.NextWriter(TextMessage) + if err != nil { + t.Errorf("%s: wc.NextWriter() returned %v", name, err) + continue + } + var nn int + if iocopy { + var n64 int64 + n64, err = io.Copy(w, bytes.NewReader(writeBuf[:n])) + nn = int(n64) + } else { + nn, err = w.Write(writeBuf[:n]) + } + if err != nil || nn != n { + t.Errorf("%s: w.Write(writeBuf[:n]) returned %d, %v", name, nn, err) + continue + } + err = w.Close() + if err != nil { + t.Errorf("%s: w.Close() returned %v", name, err) + continue + } + + opCode, r, err := rc.NextReader() + if err != nil || opCode != TextMessage { + t.Errorf("%s: NextReader() returned %d, r, %v", name, opCode, err) + continue + } + rbuf, err := ioutil.ReadAll(r) + if err != nil { + t.Errorf("%s: ReadFull() returned rbuf, %v", name, err) + continue + } + + if len(rbuf) != n { + t.Errorf("%s: len(rbuf) is %d, want %d", name, len(rbuf), n) + continue + } + + for i, b := range rbuf { + if byte(i) != b { + t.Errorf("%s: bad byte at offset %d", name, i) + break + } + } + } + } + } + } +} + +func TestControl(t *testing.T) { + const message = "this is a ping/pong messsage" + for _, isServer := range []bool{true, false} { + for _, isWriteControl := range []bool{true, false} { + name := fmt.Sprintf("s:%v, wc:%v", isServer, isWriteControl) + var connBuf bytes.Buffer + wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024) + rc := newConn(fakeNetConn{Reader: &connBuf, Writer: nil}, !isServer, 1024, 1024) + if isWriteControl { + wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second)) + } else { + w, err := wc.NextWriter(PongMessage) + if err != nil { + t.Errorf("%s: wc.NextWriter() returned %v", name, err) + continue + } + if _, err := w.Write([]byte(message)); err != nil { + t.Errorf("%s: w.Write() returned %v", name, err) + continue + } + if err := w.Close(); err != nil { + t.Errorf("%s: w.Close() returned %v", name, err) + continue + } + var actualMessage string + rc.SetPongHandler(func(s string) error { actualMessage = s; return nil }) + rc.NextReader() + if actualMessage != message { + t.Errorf("%s: pong=%q, want %q", name, actualMessage, message) + continue + } + } + } + } +} + +func TestCloseBeforeFinalFrame(t *testing.T) { + const bufSize = 512 + + var b1, b2 bytes.Buffer + wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize) + rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024) + + w, _ := wc.NextWriter(BinaryMessage) + w.Write(make([]byte, bufSize+bufSize/2)) + wc.WriteControl(CloseMessage, []byte{}, time.Now().Add(10*time.Second)) + w.Close() + + op, r, err := rc.NextReader() + if op != BinaryMessage || err != nil { + t.Fatalf("NextReader() returned %d, %v", op, err) + } + _, err = io.Copy(ioutil.Discard, r) + if err != io.ErrUnexpectedEOF { + t.Fatalf("io.Copy() returned %v, want %v", err, io.ErrUnexpectedEOF) + } + _, _, err = rc.NextReader() + if err != io.EOF { + t.Fatalf("NextReader() returned %v, want %v", err, io.EOF) + } +} + +func TestEOFBeforeFinalFrame(t *testing.T) { + const bufSize = 512 + + var b1, b2 bytes.Buffer + wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize) + rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024) + + w, _ := wc.NextWriter(BinaryMessage) + w.Write(make([]byte, bufSize+bufSize/2)) + + op, r, err := rc.NextReader() + if op != BinaryMessage || err != nil { + t.Fatalf("NextReader() returned %d, %v", op, err) + } + _, err = io.Copy(ioutil.Discard, r) + if err != io.ErrUnexpectedEOF { + t.Fatalf("io.Copy() returned %v, want %v", err, io.ErrUnexpectedEOF) + } + _, _, err = rc.NextReader() + if err != io.ErrUnexpectedEOF { + t.Fatalf("NextReader() returned %v, want %v", err, io.ErrUnexpectedEOF) + } +} + +func TestReadLimit(t *testing.T) { + + const readLimit = 512 + message := make([]byte, readLimit+1) + + var b1, b2 bytes.Buffer + wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, readLimit-2) + rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024) + rc.SetReadLimit(readLimit) + + // Send message at the limit with interleaved pong. + w, _ := wc.NextWriter(BinaryMessage) + w.Write(message[:readLimit-1]) + wc.WriteControl(PongMessage, []byte("this is a pong"), time.Now().Add(10*time.Second)) + w.Write(message[:1]) + w.Close() + + // Send message larger than the limit. + wc.WriteMessage(BinaryMessage, message[:readLimit+1]) + + op, _, err := rc.NextReader() + if op != BinaryMessage || err != nil { + t.Fatalf("1: NextReader() returned %d, %v", op, err) + } + op, r, err := rc.NextReader() + if op != BinaryMessage || err != nil { + t.Fatalf("2: NextReader() returned %d, %v", op, err) + } + _, err = io.Copy(ioutil.Discard, r) + if err != ErrReadLimit { + t.Fatalf("io.Copy() returned %v", err) + } +} + +func TestUnderlyingConn(t *testing.T) { + var b1, b2 bytes.Buffer + fc := fakeNetConn{Reader: &b1, Writer: &b2} + c := newConn(fc, true, 1024, 1024) + ul := c.UnderlyingConn() + if ul != fc { + t.Fatalf("Underlying conn is not what it should be.") + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go new file mode 100644 index 00000000000..efde3dc26b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go @@ -0,0 +1,120 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application uses +// the Upgrade function from an Upgrader object with a HTTP request handler +// to get a pointer to a Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection WriteMessage and ReadMessages methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// return +// } +// if err = conn.WriteMessage(messageType, p); err != nil { +// return err +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// snippet shows how to echo messages using the NextWriter and NextReader +// methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received ping and pong messages by invoking a callback +// function set with SetPingHandler and SetPongHandler methods. These callback +// functions can be invoked from the ReadMessage method, the NextReader method +// or from a call to the data message reader returned from NextReader. +// +// Connections handle received close messages by returning an error from the +// ReadMessage method, the NextReader method or from a call to the data message +// reader returned from NextReader. +// +// Concurrency +// +// A Conn supports a single concurrent caller to the write methods (NextWriter, +// SetWriteDeadline, WriteMessage) and a single concurrent caller to the read +// methods (NextReader, SetReadDeadline, ReadMessage). The Close and +// WriteControl methods can be called concurrently with all other methods. +// +// Read is Required +// +// The application must read the connection to process ping and close messages +// sent from the peer. If the application is not otherwise interested in +// messages from the peer, then the application should start a goroutine to read +// and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +package websocket diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/README.md new file mode 100644 index 00000000000..075ac1530a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/README.md @@ -0,0 +1,13 @@ +# Test Server + +This package contains a server for the [Autobahn WebSockets Test Suite](http://autobahn.ws/testsuite). + +To test the server, run + + go run server.go + +and start the client test driver + + wstest -m fuzzingclient -s fuzzingclient.json + +When the client completes, it writes a report to reports/clients/index.html. diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/fuzzingclient.json b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/fuzzingclient.json new file mode 100644 index 00000000000..27d5a5b146d --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/fuzzingclient.json @@ -0,0 +1,14 @@ + +{ + "options": {"failByDrop": false}, + "outdir": "./reports/clients", + "servers": [ + {"agent": "ReadAllWriteMessage", "url": "ws://localhost:9000/m", "options": {"version": 18}}, + {"agent": "ReadAllWrite", "url": "ws://localhost:9000/r", "options": {"version": 18}}, + {"agent": "CopyFull", "url": "ws://localhost:9000/f", "options": {"version": 18}}, + {"agent": "CopyWriterOnly", "url": "ws://localhost:9000/c", "options": {"version": 18}} + ], + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/server.go b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/server.go new file mode 100644 index 00000000000..c483cb658a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/server.go @@ -0,0 +1,246 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Command server is a test server for the Autobahn WebSockets Test Suite. +package main + +import ( + "errors" + "flag" + "github.com/gorilla/websocket" + "io" + "log" + "net/http" + "time" + "unicode/utf8" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// echoCopy echoes messages from the client using io.Copy. +func echoCopy(w http.ResponseWriter, r *http.Request, writerOnly bool) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("Upgrade:", err) + return + } + defer conn.Close() + for { + mt, r, err := conn.NextReader() + if err != nil { + if err != io.EOF { + log.Println("NextReader:", err) + } + return + } + if mt == websocket.TextMessage { + r = &validator{r: r} + } + w, err := conn.NextWriter(mt) + if err != nil { + log.Println("NextWriter:", err) + return + } + if mt == websocket.TextMessage { + r = &validator{r: r} + } + if writerOnly { + _, err = io.Copy(struct{ io.Writer }{w}, r) + } else { + _, err = io.Copy(w, r) + } + if err != nil { + if err == errInvalidUTF8 { + conn.WriteControl(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""), + time.Time{}) + } + log.Println("Copy:", err) + return + } + err = w.Close() + if err != nil { + log.Println("Close:", err) + return + } + } +} + +func echoCopyWriterOnly(w http.ResponseWriter, r *http.Request) { + echoCopy(w, r, true) +} + +func echoCopyFull(w http.ResponseWriter, r *http.Request) { + echoCopy(w, r, false) +} + +// echoReadAll echoes messages from the client by reading the entire message +// with ioutil.ReadAll. +func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("Upgrade:", err) + return + } + defer conn.Close() + for { + mt, b, err := conn.ReadMessage() + if err != nil { + if err != io.EOF { + log.Println("NextReader:", err) + } + return + } + if mt == websocket.TextMessage { + if !utf8.Valid(b) { + conn.WriteControl(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""), + time.Time{}) + log.Println("ReadAll: invalid utf8") + } + } + if writeMessage { + err = conn.WriteMessage(mt, b) + if err != nil { + log.Println("WriteMessage:", err) + } + } else { + w, err := conn.NextWriter(mt) + if err != nil { + log.Println("NextWriter:", err) + return + } + if _, err := w.Write(b); err != nil { + log.Println("Writer:", err) + return + } + if err := w.Close(); err != nil { + log.Println("Close:", err) + return + } + } + } +} + +func echoReadAllWriter(w http.ResponseWriter, r *http.Request) { + echoReadAll(w, r, false) +} + +func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) { + echoReadAll(w, r, true) +} + +func serveHome(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.Error(w, "Not found.", 404) + return + } + if r.Method != "GET" { + http.Error(w, "Method not allowed", 405) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + io.WriteString(w, "Echo Server +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +var utf8d = [...]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df + 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef + 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8 +} + +const ( + utf8Accept = 0 + utf8Reject = 1 +) + +func decode(state int, x rune, b byte) (int, rune) { + t := utf8d[b] + if state != utf8Accept { + x = rune(b&0x3f) | (x << 6) + } else { + x = rune((0xff >> t) & b) + } + state = int(utf8d[256+state*16+int(t)]) + return state, x +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/README.md new file mode 100644 index 00000000000..08fc3e65c65 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/README.md @@ -0,0 +1,19 @@ +# Chat Example + +This application shows how to use use the +[websocket](https://github.com/gorilla/websocket) package and +[jQuery](http://jquery.com) to implement a simple web chat application. + +## Running the example + +The example requires a working Go development environment. The [Getting +Started](http://golang.org/doc/install) page describes how to install the +development environment. + +Once you have Go up and running, you can download, build and run the example +using the following commands. + + $ go get github.com/gorilla/websocket + $ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat` + $ go run *.go + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/conn.go b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/conn.go new file mode 100644 index 00000000000..7cc0496c3e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/conn.go @@ -0,0 +1,106 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/gorilla/websocket" + "log" + "net/http" + "time" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// connection is an middleman between the websocket connection and the hub. +type connection struct { + // The websocket connection. + ws *websocket.Conn + + // Buffered channel of outbound messages. + send chan []byte +} + +// readPump pumps messages from the websocket connection to the hub. +func (c *connection) readPump() { + defer func() { + h.unregister <- c + c.ws.Close() + }() + c.ws.SetReadLimit(maxMessageSize) + c.ws.SetReadDeadline(time.Now().Add(pongWait)) + c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.ws.ReadMessage() + if err != nil { + break + } + h.broadcast <- message + } +} + +// write writes a message with the given message type and payload. +func (c *connection) write(mt int, payload []byte) error { + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + return c.ws.WriteMessage(mt, payload) +} + +// writePump pumps messages from the hub to the websocket connection. +func (c *connection) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.ws.Close() + }() + for { + select { + case message, ok := <-c.send: + if !ok { + c.write(websocket.CloseMessage, []byte{}) + return + } + if err := c.write(websocket.TextMessage, message); err != nil { + return + } + case <-ticker.C: + if err := c.write(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +// serverWs handles websocket requests from the peer. +func serveWs(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method not allowed", 405) + return + } + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + c := &connection{send: make(chan []byte, 256), ws: ws} + h.register <- c + go c.writePump() + c.readPump() +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/home.html b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/home.html new file mode 100644 index 00000000000..29599225cb1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/home.html @@ -0,0 +1,92 @@ + + + +Chat Example + + + + + +
+
+ + +
+ + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/hub.go b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/hub.go new file mode 100644 index 00000000000..449ba753d82 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/hub.go @@ -0,0 +1,51 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// hub maintains the set of active connections and broadcasts messages to the +// connections. +type hub struct { + // Registered connections. + connections map[*connection]bool + + // Inbound messages from the connections. + broadcast chan []byte + + // Register requests from the connections. + register chan *connection + + // Unregister requests from connections. + unregister chan *connection +} + +var h = hub{ + broadcast: make(chan []byte), + register: make(chan *connection), + unregister: make(chan *connection), + connections: make(map[*connection]bool), +} + +func (h *hub) run() { + for { + select { + case c := <-h.register: + h.connections[c] = true + case c := <-h.unregister: + if _, ok := h.connections[c]; ok { + delete(h.connections, c) + close(c.send) + } + case m := <-h.broadcast: + for c := range h.connections { + select { + case c.send <- m: + default: + close(c.send) + delete(h.connections, c) + } + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/main.go b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/main.go new file mode 100644 index 00000000000..3c4448d72d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/main.go @@ -0,0 +1,39 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "log" + "net/http" + "text/template" +) + +var addr = flag.String("addr", ":8080", "http service address") +var homeTempl = template.Must(template.ParseFiles("home.html")) + +func serveHome(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.Error(w, "Not found", 404) + return + } + if r.Method != "GET" { + http.Error(w, "Method not allowed", 405) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + homeTempl.Execute(w, r.Host) +} + +func main() { + flag.Parse() + go h.run() + http.HandleFunc("/", serveHome) + http.HandleFunc("/ws", serveWs) + err := http.ListenAndServe(*addr, nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/README.md new file mode 100644 index 00000000000..ca4931f3baf --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/README.md @@ -0,0 +1,9 @@ +# File Watch example. + +This example sends a file to the browser client for display whenever the file is modified. + + $ go get github.com/gorilla/websocket + $ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/filewatch` + $ go run main.go + # Open http://localhost:8080/ . + # Modify the file to see it update in the browser. diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/main.go b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/main.go new file mode 100644 index 00000000000..a2c7b85fab3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/main.go @@ -0,0 +1,193 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "io/ioutil" + "log" + "net/http" + "os" + "strconv" + "text/template" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // Time allowed to write the file to the client. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the client. + pongWait = 60 * time.Second + + // Send pings to client with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Poll file for changes with this period. + filePeriod = 10 * time.Second +) + +var ( + addr = flag.String("addr", ":8080", "http service address") + homeTempl = template.Must(template.New("").Parse(homeHTML)) + filename string + upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } +) + +func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) { + fi, err := os.Stat(filename) + if err != nil { + return nil, lastMod, err + } + if !fi.ModTime().After(lastMod) { + return nil, lastMod, nil + } + p, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fi.ModTime(), err + } + return p, fi.ModTime(), nil +} + +func reader(ws *websocket.Conn) { + defer ws.Close() + ws.SetReadLimit(512) + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, _, err := ws.ReadMessage() + if err != nil { + break + } + } +} + +func writer(ws *websocket.Conn, lastMod time.Time) { + lastError := "" + pingTicker := time.NewTicker(pingPeriod) + fileTicker := time.NewTicker(filePeriod) + defer func() { + pingTicker.Stop() + fileTicker.Stop() + ws.Close() + }() + for { + select { + case <-fileTicker.C: + var p []byte + var err error + + p, lastMod, err = readFileIfModified(lastMod) + + if err != nil { + if s := err.Error(); s != lastError { + lastError = s + p = []byte(lastError) + } + } else { + lastError = "" + } + + if p != nil { + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.TextMessage, p); err != nil { + return + } + } + case <-pingTicker.C: + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func serveWs(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + if _, ok := err.(websocket.HandshakeError); !ok { + log.Println(err) + } + return + } + + var lastMod time.Time + if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil { + lastMod = time.Unix(0, n) + } + + go writer(ws, lastMod) + reader(ws) +} + +func serveHome(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.Error(w, "Not found", 404) + return + } + if r.Method != "GET" { + http.Error(w, "Method not allowed", 405) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + p, lastMod, err := readFileIfModified(time.Time{}) + if err != nil { + p = []byte(err.Error()) + lastMod = time.Unix(0, 0) + } + var v = struct { + Host string + Data string + LastMod string + }{ + r.Host, + string(p), + strconv.FormatInt(lastMod.UnixNano(), 16), + } + homeTempl.Execute(w, &v) +} + +func main() { + flag.Parse() + if flag.NArg() != 1 { + log.Fatal("filename not specified") + } + filename = flag.Args()[0] + http.HandleFunc("/", serveHome) + http.HandleFunc("/ws", serveWs) + if err := http.ListenAndServe(*addr, nil); err != nil { + log.Fatal(err) + } +} + +const homeHTML = ` + + + WebSocket Example + + +
{{.Data}}
+ + + +` diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/json.go b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go new file mode 100644 index 00000000000..e0668f25e15 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go @@ -0,0 +1,49 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "encoding/json" +) + +// WriteJSON is deprecated, use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v to the connection. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON is deprecated, use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + return json.NewDecoder(r).Decode(v) +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/json_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/json_test.go new file mode 100644 index 00000000000..2edb28d2f87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/json_test.go @@ -0,0 +1,63 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "reflect" + "testing" +) + +func TestJSON(t *testing.T) { + var buf bytes.Buffer + c := fakeNetConn{&buf, &buf} + wc := newConn(c, true, 1024, 1024) + rc := newConn(c, false, 1024, 1024) + + var actual, expect struct { + A int + B string + } + expect.A = 1 + expect.B = "hello" + + if err := wc.WriteJSON(&expect); err != nil { + t.Fatal("write", err) + } + + if err := rc.ReadJSON(&actual); err != nil { + t.Fatal("read", err) + } + + if !reflect.DeepEqual(&actual, &expect) { + t.Fatal("equal", actual, expect) + } +} + +func TestDeprecatedJSON(t *testing.T) { + var buf bytes.Buffer + c := fakeNetConn{&buf, &buf} + wc := newConn(c, true, 1024, 1024) + rc := newConn(c, false, 1024, 1024) + + var actual, expect struct { + A int + B string + } + expect.A = 1 + expect.B = "hello" + + if err := WriteJSON(wc, &expect); err != nil { + t.Fatal("write", err) + } + + if err := ReadJSON(rc, &actual); err != nil { + t.Fatal("read", err) + } + + if !reflect.DeepEqual(&actual, &expect) { + t.Fatal("equal", actual, expect) + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/server.go b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go new file mode 100644 index 00000000000..c24c4103714 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go @@ -0,0 +1,260 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +const ( + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 +) + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then a default value of 4096 is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is set, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, the host in the Origin header must not be set or + // must match the host of the request. + CheckOrigin func(r *http.Request) bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return u.Host == r.Host +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-Websocket-Protocol). +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") + } + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: connection header != upgrade") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: upgrade != websocket") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank") + } + + subprotocol := u.selectSubprotocol(r, responseHeader) + + var ( + netConn net.Conn + br *bufio.Reader + err error + ) + + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var rw *bufio.ReadWriter + netConn, rw, err = h.Hijack() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + br = rw.Reader + + if br.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + readBufSize := u.ReadBufferSize + if readBufSize == 0 { + readBufSize = defaultReadBufferSize + } + writeBufSize := u.WriteBufferSize + if writeBufSize == 0 { + writeBufSize = defaultWriteBufferSize + } + c := newConn(netConn, true, readBufSize, writeBufSize) + c.subprotocol = subprotocol + + p := c.writeBuf[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + if c.subprotocol != "" { + p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// This function is deprecated, use websocket.Upgrader instead. +// +// The application is responsible for checking the request origin before +// calling Upgrade. An example implementation of the same origin policy is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", 403) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/server_test.go b/Godeps/_workspace/src/github.com/gorilla/websocket/server_test.go new file mode 100644 index 00000000000..ead0776aff9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/server_test.go @@ -0,0 +1,33 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "net/http" + "reflect" + "testing" +) + +var subprotocolTests = []struct { + h string + protocols []string +}{ + {"", nil}, + {"foo", []string{"foo"}}, + {"foo,bar", []string{"foo", "bar"}}, + {"foo, bar", []string{"foo", "bar"}}, + {" foo, bar", []string{"foo", "bar"}}, + {" foo, bar ", []string{"foo", "bar"}}, +} + +func TestSubprotocols(t *testing.T) { + for _, st := range subprotocolTests { + r := http.Request{Header: http.Header{"Sec-Websocket-Protocol": {st.h}}} + protocols := Subprotocols(&r) + if !reflect.DeepEqual(st.protocols, protocols) { + t.Errorf("SubProtocols(%q) returned %#v, want %#v", st.h, protocols, st.protocols) + } + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/util.go b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go new file mode 100644 index 00000000000..ffdc265ed78 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go @@ -0,0 +1,44 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" +) + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains token. +func tokenListContainsValue(header http.Header, name string, value string) bool { + for _, v := range header[name] { + for _, s := range strings.Split(v, ",") { + if strings.EqualFold(value, strings.TrimSpace(s)) { + return true + } + } + } + return false +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go new file mode 100644 index 00000000000..8f6062fee0f --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go @@ -0,0 +1,213 @@ +// Package bundle manages translations for multiple languages. +package bundle + +import ( + "encoding/json" + "fmt" + "io/ioutil" + // "launchpad.net/goyaml" + + "path/filepath" + + "github.com/nicksnyder/go-i18n/i18n/language" + "github.com/nicksnyder/go-i18n/i18n/translation" +) + +// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency. +type TranslateFunc func(translationID string, args ...interface{}) string + +// Bundle stores the translations for multiple languages. +type Bundle struct { + translations map[string]map[string]translation.Translation +} + +// New returns an empty bundle. +func New() *Bundle { + return &Bundle{ + translations: make(map[string]map[string]translation.Translation), + } +} + +// MustLoadTranslationFile is similar to LoadTranslationFile +// except it panics if an error happens. +func (b *Bundle) MustLoadTranslationFile(filename string) { + if err := b.LoadTranslationFile(filename); err != nil { + panic(err) + } +} + +// LoadTranslationFile loads the translations from filename into memory. +// +// The language that the translations are associated with is parsed from the filename (e.g. en-US.json). +// +// Generally you should load translation files once during your program's initialization. +func (b *Bundle) LoadTranslationFile(filename string) error { + basename := filepath.Base(filename) + langs := language.Parse(basename) + switch l := len(langs); { + case l == 0: + return fmt.Errorf("no language found in %q", basename) + case l > 1: + return fmt.Errorf("multiple languages found in filename %q: %v; expected one", basename, langs) + } + translations, err := parseTranslationFile(filename) + if err != nil { + return err + } + b.AddTranslation(langs[0], translations...) + return nil +} + +func parseTranslationFile(filename string) ([]translation.Translation, error) { + var unmarshalFunc func([]byte, interface{}) error + switch format := filepath.Ext(filename); format { + case ".json": + unmarshalFunc = json.Unmarshal + /* + case ".yaml": + unmarshalFunc = goyaml.Unmarshal + */ + default: + return nil, fmt.Errorf("unsupported file extension %s", format) + } + fileBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var translationsData []map[string]interface{} + if len(fileBytes) > 0 { + if err := unmarshalFunc(fileBytes, &translationsData); err != nil { + return nil, err + } + } + + translations := make([]translation.Translation, 0, len(translationsData)) + for i, translationData := range translationsData { + t, err := translation.NewTranslation(translationData) + if err != nil { + return nil, fmt.Errorf("unable to parse translation #%d in %s because %s\n%v", i, filename, err, translationData) + } + translations = append(translations, t) + } + return translations, nil +} + +// AddTranslation adds translations for a language. +// +// It is useful if your translations are in a format not supported by LoadTranslationFile. +func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) { + if b.translations[lang.Tag] == nil { + b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations)) + } + currentTranslations := b.translations[lang.Tag] + for _, newTranslation := range translations { + if currentTranslation := currentTranslations[newTranslation.ID()]; currentTranslation != nil { + currentTranslations[newTranslation.ID()] = currentTranslation.Merge(newTranslation) + } else { + currentTranslations[newTranslation.ID()] = newTranslation + } + } +} + +// Translations returns all translations in the bundle. +func (b *Bundle) Translations() map[string]map[string]translation.Translation { + return b.translations +} + +// MustTfunc is similar to Tfunc except it panics if an error happens. +func (b *Bundle) MustTfunc(languageSource string, languageSources ...string) TranslateFunc { + tf, err := b.Tfunc(languageSource, languageSources...) + if err != nil { + panic(err) + } + return tf +} + +// Tfunc returns a TranslateFunc that will be bound to the first language which +// has a non-zero number of translations in the bundle. +// +// It can parse languages from Accept-Language headers (RFC 2616). +func (b *Bundle) Tfunc(src string, srcs ...string) (TranslateFunc, error) { + lang := b.translatedLanguage(src) + if lang == nil { + for _, src := range srcs { + lang = b.translatedLanguage(src) + if lang != nil { + break + } + } + } + var err error + if lang == nil { + err = fmt.Errorf("no supported languages found %#v", append(srcs, src)) + } + return func(translationID string, args ...interface{}) string { + return b.translate(lang, translationID, args...) + }, err +} + +func (b *Bundle) translatedLanguage(src string) *language.Language { + langs := language.Parse(src) + for _, lang := range langs { + if len(b.translations[lang.Tag]) > 0 { + return lang + } + } + return nil +} + +func (b *Bundle) translate(lang *language.Language, translationID string, args ...interface{}) string { + if lang == nil { + return translationID + } + + translations := b.translations[lang.Tag] + if translations == nil { + return translationID + } + + translation := translations[translationID] + if translation == nil { + return translationID + } + + var count interface{} + if len(args) > 0 && isNumber(args[0]) { + count = args[0] + args = args[1:] + } + + plural, _ := lang.Plural(count) + template := translation.Template(plural) + if template == nil { + return translationID + } + + var data map[string]interface{} + if len(args) > 0 { + data, _ = args[0].(map[string]interface{}) + } + + if isNumber(count) { + if data == nil { + data = map[string]interface{}{"Count": count} + } else { + data["Count"] = count + } + } + + s := template.Execute(data) + if s == "" { + return translationID + } + return s +} + +func isNumber(n interface{}) bool { + switch n.(type) { + case int, int8, int16, int32, int64, string: + return true + } + return false +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle_test.go new file mode 100644 index 00000000000..abcc742bf89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/bundle/bundle_test.go @@ -0,0 +1,152 @@ +package bundle + +import ( + "testing" + + "github.com/nicksnyder/go-i18n/i18n/language" + "github.com/nicksnyder/go-i18n/i18n/translation" +) + +func TestMustLoadTranslationFile(t *testing.T) { + t.Skipf("not implemented") +} + +func TestLoadTranslationFile(t *testing.T) { + t.Skipf("not implemented") +} + +func TestAddTranslation(t *testing.T) { + t.Skipf("not implemented") +} + +func TestMustTfunc(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected MustTfunc to panic") + } + }() + New().MustTfunc("invalid") +} + +func TestTfunc(t *testing.T) { + b := New() + translationID := "translation_id" + englishTranslation := "en-US(translation_id)" + b.AddTranslation(language.MustParse("en-US")[0], testNewTranslation(t, map[string]interface{}{ + "id": translationID, + "translation": englishTranslation, + })) + frenchTranslation := "fr-FR(translation_id)" + b.AddTranslation(language.MustParse("fr-FR")[0], testNewTranslation(t, map[string]interface{}{ + "id": translationID, + "translation": frenchTranslation, + })) + spanishTranslation := "es(translation_id)" + b.AddTranslation(language.MustParse("es")[0], testNewTranslation(t, map[string]interface{}{ + "id": translationID, + "translation": spanishTranslation, + })) + + tests := []struct { + languageIDs []string + valid bool + result string + }{ + { + []string{"invalid"}, + false, + translationID, + }, + { + []string{"invalid", "invalid2"}, + false, + translationID, + }, + { + []string{"invalid", "en-US"}, + true, + englishTranslation, + }, + { + []string{"en-US", "invalid"}, + true, + englishTranslation, + }, + { + []string{"en-US", "fr-FR"}, + true, + englishTranslation, + }, + { + []string{"invalid", "es"}, + true, + spanishTranslation, + }, + { + []string{"zh-CN,fr-XX,es"}, + true, + spanishTranslation, + }, + } + + for i, test := range tests { + tf, err := b.Tfunc(test.languageIDs[0], test.languageIDs[1:]...) + if err != nil && test.valid { + t.Errorf("Tfunc(%v) = error{%q}; expected no error", test.languageIDs, err) + } + if err == nil && !test.valid { + t.Errorf("Tfunc(%v) = nil error; expected error", test.languageIDs) + } + if result := tf(translationID); result != test.result { + t.Errorf("translation %d was %s; expected %s", i, result, test.result) + } + } +} + +func testNewTranslation(t *testing.T, data map[string]interface{}) translation.Translation { + translation, err := translation.NewTranslation(data) + if err != nil { + t.Fatal(err) + } + return translation +} + +/* + +func bundleFixture(t *testing.T) *Bundle { + l, err := NewLocaleFromString("ar-EG") + if err != nil { + t.Errorf(err.Error()) + } + return &Bundle{ + Locale: l, + localizedStrings: map[string]*LocalizedString{ + "a": &LocalizedString{ + ID: "a", + }, + "b": &LocalizedString{ + ID: "b", + Translation: "translation(b)", + }, + "c": &LocalizedString{ + ID: "c", + Translations: map[PluralCategory]*PluralTranslation{ + Zero: NewPluralTranslation("zero(c)"), + One: NewPluralTranslation("one(c)"), + Two: NewPluralTranslation("two(c)"), + Few: NewPluralTranslation("few(c)"), + Many: NewPluralTranslation("many(c)"), + Other: NewPluralTranslation("other(c)"), + }, + }, + "d": &LocalizedString{ + ID: "d", + Translations: map[PluralCategory]*PluralTranslation{ + Zero: NewPluralTranslation("zero(d)"), + One: NewPluralTranslation("one(d)"), + }, + }, + }, + } +} +*/ diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/example_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/example_test.go new file mode 100644 index 00000000000..c933f4aa804 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/example_test.go @@ -0,0 +1,59 @@ +package i18n_test + +import ( + "fmt" + "github.com/nicksnyder/go-i18n/i18n" +) + +func Example() { + i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json") + + T, _ := i18n.Tfunc("en-US") + + fmt.Println(T("program_greeting")) + fmt.Println(T("person_greeting", map[string]interface{}{ + "Person": "Bob", + })) + + fmt.Println(T("your_unread_email_count", 0)) + fmt.Println(T("your_unread_email_count", 1)) + fmt.Println(T("your_unread_email_count", 2)) + fmt.Println(T("my_height_in_meters", "1.7")) + + fmt.Println(T("person_unread_email_count", 0, map[string]interface{}{ + "Person": "Bob", + })) + fmt.Println(T("person_unread_email_count", 1, map[string]interface{}{ + "Person": "Bob", + })) + fmt.Println(T("person_unread_email_count", 2, map[string]interface{}{ + "Person": "Bob", + })) + + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 0), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 1), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 2), + })) + + // Output: + // Hello world + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // I am 1.7 meters tall. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // Bob has 3 unread emails in the past 0 days. + // Bob has 3 unread emails in the past 1 day. + // Bob has 3 unread emails in the past 2 days. +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/exampletemplate_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/exampletemplate_test.go new file mode 100644 index 00000000000..b96894519e9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/exampletemplate_test.go @@ -0,0 +1,46 @@ +package i18n_test + +import ( + "github.com/nicksnyder/go-i18n/i18n" + "os" + "text/template" +) + +var funcMap = map[string]interface{}{ + "T": i18n.IdentityTfunc, +} + +var tmpl = template.Must(template.New("").Funcs(funcMap).Parse(` +{{T "program_greeting"}} +{{T "person_greeting" .}} +{{T "your_unread_email_count" 0}} +{{T "your_unread_email_count" 1}} +{{T "your_unread_email_count" 2}} +{{T "person_unread_email_count" 0 .}} +{{T "person_unread_email_count" 1 .}} +{{T "person_unread_email_count" 2 .}} +`)) + +func Example_template() { + i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json") + + T, _ := i18n.Tfunc("en-US") + tmpl.Funcs(map[string]interface{}{ + "T": T, + }) + + tmpl.Execute(os.Stdout, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 1), + }) + + // Output: + // Hello world + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/i18n.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/i18n.go new file mode 100644 index 00000000000..83b16151690 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/i18n.go @@ -0,0 +1,120 @@ +// Package i18n supports string translations with variable substitution and CLDR pluralization. +// It is intended to be used in conjunction with the goi18n command, although that is not strictly required. +// +// Initialization +// +// Your Go program should load translations during its initialization. +// i18n.MustLoadTranslationFile("path/to/fr-FR.all.json") +// If your translations are in a file format not supported by (Must)?LoadTranslationFile, +// then you can use the AddTranslation function to manually add translations. +// +// Fetching a translation +// +// Use Tfunc or MustTfunc to fetch a TranslateFunc that will return the translated string for a specific language. +// func handleRequest(w http.ResponseWriter, r *http.Request) { +// cookieLang := r.Cookie("lang") +// acceptLang := r.Header.Get("Accept-Language") +// defaultLang = "en-US" // known valid language +// T, err := i18n.Tfunc(cookieLang, acceptLang, defaultLang) +// fmt.Println(T("Hello world")) +// } +// +// Usually it is a good idea to identify strings by a generic id rather than the English translation, +// but the rest of this documentation will continue to use the English translation for readability. +// T("Hello world") // ok +// T("programGreeting") // better! +// +// Variables +// +// TranslateFunc supports strings that have variables using the text/template syntax. +// T("Hello {{.Person}}", map[string]interface{}{ +// "Person": "Bob", +// }) +// +// Pluralization +// +// TranslateFunc supports the pluralization of strings using the CLDR pluralization rules defined here: +// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +// T("You have {{.Count}} unread emails.", 2) +// T("I am {{.Count}} meters tall.", "1.7") +// +// Plural strings may also have variables. +// T("{{.Person}} has {{.Count}} unread emails", 2, map[string]interface{}{ +// "Person": "Bob", +// }) +// +// Sentences with multiple plural components can be supported with nesting. +// T("{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", 3, map[string]interface{}{ +// "Person": "Bob", +// "Timeframe": T("{{.Count}} days", 2), +// }) +// +// Templates +// +// You can use the .Funcs() method of a text/template or html/template to register a TranslateFunc +// for usage inside of that template. +package i18n + +import ( + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/nicksnyder/go-i18n/i18n/language" + "github.com/nicksnyder/go-i18n/i18n/translation" +) + +// TranslateFunc returns the translation of the string identified by translationID. +// +// If translationID is a non-plural form, then the first variadic argument may be a map[string]interface{} +// that contains template data. +// +// If translationID is a plural form, then the first variadic argument must be an integer type +// (int, int8, int16, int32, int64) or a float formatted as a string (e.g. "123.45"). +// The second variadic argument may be a map[string]interface{} that contains template data. +type TranslateFunc func(translationID string, args ...interface{}) string + +// IdentityTfunc returns a TranslateFunc that always returns the translationID passed to it. +// +// It is a useful placeholder when parsing a text/template or html/template +// before the actual Tfunc is available. +func IdentityTfunc() TranslateFunc { + return func(translationID string, args ...interface{}) string { + return translationID + } +} + +var defaultBundle = bundle.New() + +// MustLoadTranslationFile is similar to LoadTranslationFile +// except it panics if an error happens. +func MustLoadTranslationFile(filename string) { + defaultBundle.MustLoadTranslationFile(filename) +} + +// LoadTranslationFile loads the translations from filename into memory. +// +// The language that the translations are associated with is parsed from the filename (e.g. en-US.json). +// +// Generally you should load translation files once during your program's initialization. +func LoadTranslationFile(filename string) error { + return defaultBundle.LoadTranslationFile(filename) +} + +// AddTranslation adds translations for a language. +// +// It is useful if your translations are in a format not supported by LoadTranslationFile. +func AddTranslation(lang *language.Language, translations ...translation.Translation) { + defaultBundle.AddTranslation(lang, translations...) +} + +// MustTfunc is similar to Tfunc except it panics if an error happens. +func MustTfunc(languageSource string, languageSources ...string) TranslateFunc { + return TranslateFunc(defaultBundle.MustTfunc(languageSource, languageSources...)) +} + +// Tfunc returns a TranslateFunc that will be bound to the first language which +// has a non-zero number of translations. +// +// It can parse languages from Accept-Language headers (RFC 2616). +func Tfunc(languageSource string, languageSources ...string) (TranslateFunc, error) { + tf, err := defaultBundle.Tfunc(languageSource, languageSources...) + return TranslateFunc(tf), err +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language.go new file mode 100644 index 00000000000..321c6814fc0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language.go @@ -0,0 +1,84 @@ +// Package language defines languages that implement CLDR pluralization. +package language + +import ( + "fmt" + "strings" +) + +// Language is a written human language. +type Language struct { + // Tag uniquely identifies the language as defined by RFC 5646. + // + // Most language tags are a two character language code (ISO 639-1) + // optionally followed by a dash and a two character country code (ISO 3166-1). + // (e.g. en, pt-br) + Tag string + *PluralSpec +} + +func (l *Language) String() string { + return l.Tag +} + +// Parse returns a slice of supported languages found in src or nil if none are found. +// It can parse language tags and Accept-Language headers. +func Parse(src string) []*Language { + var langs []*Language + start := 0 + for end, chr := range src { + switch chr { + case ',', ';', '.': + tag := strings.TrimSpace(src[start:end]) + if spec := getPluralSpec(tag); spec != nil { + langs = append(langs, &Language{NormalizeTag(tag), spec}) + } + start = end + 1 + } + } + if start > 0 { + tag := strings.TrimSpace(src[start:]) + if spec := getPluralSpec(tag); spec != nil { + langs = append(langs, &Language{NormalizeTag(tag), spec}) + } + return dedupe(langs) + } + if spec := getPluralSpec(src); spec != nil { + langs = append(langs, &Language{NormalizeTag(src), spec}) + } + return langs +} + +func dedupe(langs []*Language) []*Language { + found := make(map[string]struct{}, len(langs)) + deduped := make([]*Language, 0, len(langs)) + for _, lang := range langs { + if _, ok := found[lang.Tag]; !ok { + found[lang.Tag] = struct{}{} + deduped = append(deduped, lang) + } + } + return deduped +} + +// MustParse is similar to Parse except it panics instead of retuning a nil Language. +func MustParse(src string) []*Language { + langs := Parse(src) + if len(langs) == 0 { + panic(fmt.Errorf("unable to parse language from %q", src)) + } + return langs +} + +// Add adds support for a new language. +func Add(l *Language) { + tag := NormalizeTag(l.Tag) + pluralSpecs[tag] = l.PluralSpec +} + +// NormalizeTag returns a language tag with all lower-case characters +// and dashes "-" instead of underscores "_" +func NormalizeTag(tag string) string { + tag = strings.ToLower(tag) + return strings.Replace(tag, "_", "-", -1) +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language_test.go new file mode 100644 index 00000000000..7f4a3722417 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/language_test.go @@ -0,0 +1,70 @@ +package language + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + src string + lang []*Language + }{ + {"en", []*Language{{"en", pluralSpecs["en"]}}}, + {"en-US", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en_US", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-GB", []*Language{{"en-gb", pluralSpecs["en"]}}}, + {"zh-CN", []*Language{{"zh-cn", pluralSpecs["zh"]}}}, + {"zh-TW", []*Language{{"zh-tw", pluralSpecs["zh"]}}}, + {"pt-BR", []*Language{{"pt-br", pluralSpecs["pt-br"]}}}, + {"pt_BR", []*Language{{"pt-br", pluralSpecs["pt-br"]}}}, + {"pt-PT", []*Language{{"pt-pt", pluralSpecs["pt"]}}}, + {"pt_PT", []*Language{{"pt-pt", pluralSpecs["pt"]}}}, + {"zh-Hans-CN", []*Language{{"zh-hans-cn", pluralSpecs["zh"]}}}, + {"zh-Hant-TW", []*Language{{"zh-hant-tw", pluralSpecs["zh"]}}}, + {"en-US-en-US", []*Language{{"en-us-en-us", pluralSpecs["en"]}}}, + {".en-US..en-US.", []*Language{{"en-us", pluralSpecs["en"]}}}, + { + "it, xx-zz, xx-ZZ, zh, en-gb;q=0.8, en;q=0.7, es-ES;q=0.6, de-xx", + []*Language{ + {"it", pluralSpecs["it"]}, + {"zh", pluralSpecs["zh"]}, + {"en-gb", pluralSpecs["en"]}, + {"en", pluralSpecs["en"]}, + {"es-es", pluralSpecs["es"]}, + {"de-xx", pluralSpecs["de"]}, + }, + }, + { + "it-qq,xx,xx-zz,xx-ZZ,zh,en-gb;q=0.8,en;q=0.7,es-ES;q=0.6,de-xx", + []*Language{ + {"it-qq", pluralSpecs["it"]}, + {"zh", pluralSpecs["zh"]}, + {"en-gb", pluralSpecs["en"]}, + {"en", pluralSpecs["en"]}, + {"es-es", pluralSpecs["es"]}, + {"de-xx", pluralSpecs["de"]}, + }, + }, + {"en.json", []*Language{{"en", pluralSpecs["en"]}}}, + {"en-US.json", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-us.json", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-xx.json", []*Language{{"en-xx", pluralSpecs["en"]}}}, + {"xx-Yyen-US", nil}, + {"en US", nil}, + {"", nil}, + {"-", nil}, + {"_", nil}, + {"-en", nil}, + {"_en", nil}, + {"-en-", nil}, + {"_en_", nil}, + {"xx", nil}, + } + for _, test := range tests { + lang := Parse(test.src) + if !reflect.DeepEqual(lang, test.lang) { + t.Errorf("Parse(%q) = %q expected %q", test.src, lang, test.lang) + } + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands.go new file mode 100644 index 00000000000..e56534bae91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands.go @@ -0,0 +1,87 @@ +package language + +import ( + "fmt" + "strconv" + "strings" +) + +// http://unicode.org/reports/tr35/tr35-numbers.html#Operands +type operands struct { + N float64 // absolute value of the source number (integer and decimals) + I int64 // integer digits of n + V int // number of visible fraction digits in n, with trailing zeros + W int // number of visible fraction digits in n, without trailing zeros + F int // visible fractional digits in n, with trailing zeros + T int // visible fractional digits in n, without trailing zeros +} + +func newOperands(v interface{}) (*operands, error) { + switch v := v.(type) { + case int: + return newOperandsInt64(int64(v)), nil + case int8: + return newOperandsInt64(int64(v)), nil + case int16: + return newOperandsInt64(int64(v)), nil + case int32: + return newOperandsInt64(int64(v)), nil + case int64: + return newOperandsInt64(v), nil + case string: + return newOperandsString(v) + case float32, float64: + return nil, fmt.Errorf("floats should be formatted into a string") + default: + return nil, fmt.Errorf("invalid type %T; expected integer or string", v) + } +} + +func newOperandsInt64(i int64) *operands { + if i < 0 { + i = -i + } + return &operands{float64(i), i, 0, 0, 0, 0} +} + +func newOperandsString(s string) (*operands, error) { + if s[0] == '-' { + s = s[1:] + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, err + } + ops := &operands{N: n} + parts := strings.SplitN(s, ".", 2) + ops.I, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, err + } + if len(parts) == 1 { + return ops, nil + } + fraction := parts[1] + ops.V = len(fraction) + for i := ops.V - 1; i >= 0; i-- { + if fraction[i] != '0' { + ops.W = i + 1 + break + } + } + if ops.V > 0 { + f, err := strconv.ParseInt(fraction, 10, 0) + if err != nil { + return nil, err + } + ops.F = int(f) + } + if ops.W > 0 { + t, err := strconv.ParseInt(fraction[:ops.W], 10, 0) + if err != nil { + return nil, err + } + ops.T = int(t) + } + return ops, nil +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands_test.go new file mode 100644 index 00000000000..29030876af3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/operands_test.go @@ -0,0 +1,45 @@ +package language + +import ( + "reflect" + "testing" +) + +func TestNewOperands(t *testing.T) { + tests := []struct { + input interface{} + ops *operands + err bool + }{ + {int64(0), &operands{0.0, 0, 0, 0, 0, 0}, false}, + {int64(1), &operands{1.0, 1, 0, 0, 0, 0}, false}, + {"0", &operands{0.0, 0, 0, 0, 0, 0}, false}, + {"1", &operands{1.0, 1, 0, 0, 0, 0}, false}, + {"1.0", &operands{1.0, 1, 1, 0, 0, 0}, false}, + {"1.00", &operands{1.0, 1, 2, 0, 0, 0}, false}, + {"1.3", &operands{1.3, 1, 1, 1, 3, 3}, false}, + {"1.30", &operands{1.3, 1, 2, 1, 30, 3}, false}, + {"1.03", &operands{1.03, 1, 2, 2, 3, 3}, false}, + {"1.230", &operands{1.23, 1, 3, 2, 230, 23}, false}, + {"20.0230", &operands{20.023, 20, 4, 3, 230, 23}, false}, + {20.0230, nil, true}, + } + for _, test := range tests { + ops, err := newOperands(test.input) + if err != nil && !test.err { + t.Errorf("newOperands(%#v) unexpected error: %s", test.input, err) + } else if err == nil && test.err { + t.Errorf("newOperands(%#v) returned %#v; expected error", test.input, ops) + } else if !reflect.DeepEqual(ops, test.ops) { + t.Errorf("newOperands(%#v) returned %#v; expected %#v", test.input, ops, test.ops) + } + } +} + +func BenchmarkNewOperand(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := newOperands("1234.56780000"); err != nil { + b.Fatal(err) + } + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural.go new file mode 100644 index 00000000000..1f3ea5c69b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural.go @@ -0,0 +1,40 @@ +package language + +import ( + "fmt" +) + +// Plural represents a language pluralization form as defined here: +// http://cldr.unicode.org/index/cldr-spec/plural-rules +type Plural string + +// All defined plural categories. +const ( + Invalid Plural = "invalid" + Zero = "zero" + One = "one" + Two = "two" + Few = "few" + Many = "many" + Other = "other" +) + +// NewPlural returns src as a Plural +// or Invalid and a non-nil error if src is not a valid Plural. +func NewPlural(src string) (Plural, error) { + switch src { + case "zero": + return Zero, nil + case "one": + return One, nil + case "two": + return Two, nil + case "few": + return Few, nil + case "many": + return Many, nil + case "other": + return Other, nil + } + return Invalid, fmt.Errorf("invalid plural category %s", src) +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural_test.go new file mode 100644 index 00000000000..6336d29b20d --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/plural_test.go @@ -0,0 +1,28 @@ +package language + +import ( + "testing" +) + +func TestNewPlural(t *testing.T) { + tests := []struct { + src string + plural Plural + err bool + }{ + {"zero", Zero, false}, + {"one", One, false}, + {"two", Two, false}, + {"few", Few, false}, + {"many", Many, false}, + {"other", Other, false}, + {"asdf", Invalid, true}, + } + for _, test := range tests { + plural, err := NewPlural(test.src) + wrongErr := (err != nil && !test.err) || (err == nil && test.err) + if plural != test.plural || wrongErr { + t.Errorf("NewPlural(%#v) returned %#v,%#v; expected %#v", test.src, plural, err, test.plural) + } + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec.go new file mode 100644 index 00000000000..f5011885fd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec.go @@ -0,0 +1,362 @@ +package language + +import ( + "strings" + "math" +) + +// PluralSpec defines the CLDR plural rules for a language. +// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +// http://unicode.org/reports/tr35/tr35-numbers.html#Operands +type PluralSpec struct { + Plurals map[Plural]struct{} + PluralFunc func(*operands) Plural +} + +// Plural returns the plural category for number as defined by +// the language's CLDR plural rules. +func (ps *PluralSpec) Plural(number interface{}) (Plural, error) { + ops, err := newOperands(number) + if err != nil { + return Invalid, err + } + return ps.PluralFunc(ops), nil +} + +// getPluralSpec returns the PluralSpec that matches the longest prefix of tag. +// It returns nil if no PluralSpec matches tag. +func getPluralSpec(tag string) *PluralSpec { + tag = NormalizeTag(tag) + subtag := tag + for { + if spec := pluralSpecs[subtag]; spec != nil { + return spec + } + end := strings.LastIndex(subtag, "-") + if end == -1 { + return nil + } + subtag = subtag[:end] + } +} + +// Alphabetical by English name. +var pluralSpecs = map[string]*PluralSpec{ + // Arabic + "ar": &PluralSpec{ + Plurals: newPluralSet(Zero, One, Two, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + if ops.W == 0 { + switch ops.I { + case 0: + return Zero + case 1: + return One + case 2: + return Two + default: + mod100 := ops.I % 100 + if mod100 >= 3 && mod100 <= 10 { + return Few + } + if mod100 >= 11 { + return Many + } + } + } + return Other + }, + }, + + // Belarusian + "be": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + mod10 := math.Mod(ops.N, 10) + mod100 := math.Mod(ops.N, 100) + if ops.T == 0 && mod10 == 1 && mod100 != 11 { + return One + } + if ops.T == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14) { + return Few + } + if (ops.T == 0 && mod10 == 0) || + (ops.T == 0 && mod10 >= 5 && mod10 <= 9) || + (ops.T == 0 && mod100 >= 11 && mod100 <= 14) { + return Many + } + return Other + }, + }, + + // Catalan + "ca": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // Chinese + // There is no need to distinguish between simplified and traditional + // since they have the same pluralization. + "zh": &PluralSpec{ + Plurals: newPluralSet(Other), + PluralFunc: func(ops *operands) Plural { + return Other + }, + }, + + // Czech + "cs": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + if ops.I >= 2 && ops.I <= 4 && ops.V == 0 { + return Few + } + if ops.V > 0 { + return Many + } + return Other + }, + }, + + // Danish + "da": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 || (ops.I == 0 && ops.T != 0) { + return One + } + return Other + }, + }, + + // Dutch + "nl": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // English + "en": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // French + "fr": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 0 || ops.I == 1 { + return One + } + return Other + }, + }, + + // German + "de": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // Icelandic + "is": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if (ops.T == 0 && ops.I % 10 == 1 && ops.I % 100 != 11) || ops.T != 0 { + return One + } + return Other + }, + }, + + // Italian + "it": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // Japanese + "ja": &PluralSpec{ + Plurals: newPluralSet(Other), + PluralFunc: func(ops *operands) Plural { + return Other + }, + }, + + // Lithuanian + "lt": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + if ops.F != 0 { + return Many + } + mod100 := ops.I % 100 + if mod100 < 11 || mod100 > 19 { + switch ops.I % 10 { + case 0: + return Other + case 1: + return One + default: + return Few + } + } + return Other + }, + }, + + // Polish + "pl": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + if ops.V == 0 && ops.I == 1 { + return One + } + mod10 := ops.I % 10 + mod100 := ops.I % 100 + if ops.V == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14) { + return Few + } + if (ops.V == 0 && ops.I != 1 && mod10 >= 0 && mod10 <= 1) || + (ops.V == 0 && mod10 >= 5 && mod10 <= 9) || + (ops.V == 0 && mod100 >= 12 && mod100 <= 14) { + return Many + } + return Other + }, + }, + + // Portuguese (European) + "pt": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // Portuguese (Brazilian) + "pt-br": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if (ops.I == 1 && ops.V == 0) || (ops.I == 0 && ops.T == 1) { + return One + } + return Other + }, + }, + + // Russian + "ru": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + mod10 := ops.I % 10 + mod100 := ops.I % 100 + if ops.V == 0 && mod10 == 1 && mod100 != 11 { + return One + } + if ops.V == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14) { + return Few + } + if (ops.V == 0 && mod10 == 0) || + (ops.V == 0 && mod10 >= 5 && mod10 <= 9) || + (ops.V == 0 && mod100 >= 11 && mod100 <= 14) { + return Many + } + return Other + }, + }, + + // Spanish + "es": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.W == 0 { + return One + } + return Other + }, + }, + + // Bulgarian + "bg": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.W == 0 { + return One + } + return Other + }, + }, + + // Swedish + "sv": &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *operands) Plural { + if ops.I == 1 && ops.V == 0 { + return One + } + return Other + }, + }, + + // Ukrainian + "uk": &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *operands) Plural { + mod10 := ops.I % 10 + mod100 := ops.I % 100 + if ops.V == 0 && mod10 == 1 && mod100 != 11 { + return One + } + if ops.V == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14) { + return Few + } + if (ops.V == 0 && mod10 == 0) || + (ops.V == 0 && mod10 >= 5 && mod10 <= 9) || + (ops.V == 0 && mod100 >= 11 && mod100 <= 14) { + return Many + } + return Other + }, + }, +} + +func newPluralSet(plurals ...Plural) map[Plural]struct{} { + set := make(map[Plural]struct{}, len(plurals)) + for _, plural := range plurals { + set[plural] = struct{}{} + } + return set +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec_test.go new file mode 100644 index 00000000000..852d25661a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/language/pluralspec_test.go @@ -0,0 +1,485 @@ +package language + +import ( + "fmt" + "testing" +) + +const onePlusEpsilon = "1.00000000000000000000000000000001" + +func TestGetPluralSpec(t *testing.T) { + tests := []struct { + src string + spec *PluralSpec + }{ + {"pl", pluralSpecs["pl"]}, + {"en", pluralSpecs["en"]}, + {"en-US", pluralSpecs["en"]}, + {"en_US", pluralSpecs["en"]}, + {"en-GB", pluralSpecs["en"]}, + {"zh-CN", pluralSpecs["zh"]}, + {"zh-TW", pluralSpecs["zh"]}, + {"pt-BR", pluralSpecs["pt-br"]}, + {"pt_BR", pluralSpecs["pt-br"]}, + {"pt-PT", pluralSpecs["pt"]}, + {"pt_PT", pluralSpecs["pt"]}, + {"zh-Hans-CN", pluralSpecs["zh"]}, + {"zh-Hant-TW", pluralSpecs["zh"]}, + {"zh-CN", pluralSpecs["zh"]}, + {"zh-TW", pluralSpecs["zh"]}, + {"zh-Hans", pluralSpecs["zh"]}, + {"zh-Hant", pluralSpecs["zh"]}, + {"en-US-en-US", pluralSpecs["en"]}, + {".en-US..en-US.", nil}, + {"zh, en-gb;q=0.8, en;q=0.7", nil}, + {"zh,en-gb;q=0.8,en;q=0.7", nil}, + {"xx, en-gb;q=0.8, en;q=0.7", nil}, + {"xx,en-gb;q=0.8,en;q=0.7", nil}, + {"xx-YY,xx;q=0.8,en-US,en;q=0.8,de;q=0.6,nl;q=0.4", nil}, + {"/foo/es/en.json", nil}, + {"xx-Yyen-US", nil}, + {"en US", nil}, + {"", nil}, + {"-", nil}, + {"_", nil}, + {".", nil}, + {"-en", nil}, + {"_en", nil}, + {"-en-", nil}, + {"_en_", nil}, + {"xx", nil}, + } + for _, test := range tests { + spec := getPluralSpec(test.src) + if spec != test.spec { + t.Errorf("getPluralSpec(%q) = %q expected %q", test.src, spec, test.spec) + } + } +} + +type pluralTest struct { + num interface{} + plural Plural +} + +func TestArabic(t *testing.T ) { + tests := []pluralTest{ + {0, Zero}, + {"0", Zero}, + {"0.0", Zero}, + {"0.00", Zero}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {onePlusEpsilon, Other}, + {2, Two}, + {"2", Two}, + {"2.0", Two}, + {"2.00", Two}, + {3, Few}, + {"3", Few}, + {"3.0", Few}, + {"3.00", Few}, + {10, Few}, + {"10", Few}, + {"10.0", Few}, + {"10.00", Few}, + {103, Few}, + {"103", Few}, + {"103.0", Few}, + {"103.00", Few}, + {110, Few}, + {"110", Few}, + {"110.0", Few}, + {"110.00", Few}, + {11, Many}, + {"11", Many}, + {"11.0", Many}, + {"11.00", Many}, + {99, Many}, + {"99", Many}, + {"99.0", Many}, + {"99.00", Many}, + {111, Many}, + {"111", Many}, + {"111.0", Many}, + {"111.00", Many}, + {199, Many}, + {"199", Many}, + {"199.0", Many}, + {"199.00", Many}, + {100, Other}, + {"100", Other}, + {"100.0", Other}, + {"100.00", Other}, + {102, Other}, + {"102", Other}, + {"102.0", Other}, + {"102.00", Other}, + {200, Other}, + {"200", Other}, + {"200.0", Other}, + {"200.00", Other}, + {202, Other}, + {"202", Other}, + {"202.0", Other}, + {"202.00", Other}, + } + tests = appendFloatTests(tests, 0.1, 0.9, Other) + tests = appendFloatTests(tests, 1.1, 1.9, Other) + tests = appendFloatTests(tests, 2.1, 2.9, Other) + tests = appendFloatTests(tests, 3.1, 3.9, Other) + tests = appendFloatTests(tests, 4.1, 4.9, Other) + runTests(t, "ar", tests) +} + +func TestBelarusian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", One}, + {onePlusEpsilon, Other}, + {"2.0", Few}, + {"10.0", Many}, + } + runTests(t, "be", tests) +} + +func TestCatalan(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {"0", Other}, + {1, One}, + {"1", One}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {2, Other}, + {"2", Other}, + } + tests = appendIntTests(tests, 2, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ca", tests) +} + +func TestChinese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "zh", tests) +} + +func TestCzech(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {"0", Other}, + {1, One}, + {"1", One}, + {onePlusEpsilon, Many}, + {2, Few}, + {"2", Few}, + {3, Few}, + {"3", Few}, + {4, Few}, + {"4", Few}, + {5, Other}, + {"5", Other}, + } + tests = appendFloatTests(tests, 0, 10, Many) + runTests(t, "cs", tests) +} + +func TestDanish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, One}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.1, 1.9, One) + tests = appendFloatTests(tests, 2.0, 10.0, Other) + runTests(t, "da", tests) +} + +func TestDutch(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "nl", tests) +} + +func TestEnglish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "en", tests) +} + +func TestFrench(t *testing.T) { + tests := []pluralTest{ + {0, One}, + {1, One}, + {onePlusEpsilon, One}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 1.9, One) + tests = appendFloatTests(tests, 2.0, 10.0, Other) + runTests(t, "fr", tests) +} + +func TestGerman(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "de", tests) +} + +func TestIcelandic(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Other}, + {11, Other}, + {21, One}, + {111, Other}, + {"0.0", Other}, + {"0.1", One}, + {"2.0", Other}, + } + runTests(t, "is", tests) +} + +func TestItalian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "it", tests) +} + +func TestJapanese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ja", tests) +} + +func TestLithuanian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Few}, + {3, Few}, + {9, Few}, + {10, Other}, + {11, Other}, + {"0.1", Many}, + {"0.7", Many}, + {"1.0", One}, + {onePlusEpsilon, Many}, + {"2.0", Few}, + {"10.0", Other}, + } + runTests(t, "lt", tests) +} + +func TestPolish(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {10, Many}, + {11, Many}, + {52, Few}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "pl", tests) +} + +func TestPortuguese(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "pt", tests) +} + +func TestPortugueseBrazilian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {"0.0", Other}, + {"0.1", One}, + {"0.01", One}, + {1, One}, + {"1", One}, + {"1.1", Other}, + {"1.01", Other}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 2.0, 10.0, Other) + runTests(t, "pt-br", tests) +} + +func TestRussian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "ru", tests) +} + +func TestSpanish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 0.9, Other) + tests = appendFloatTests(tests, 1.1, 10.0, Other) + runTests(t, "es", tests) +} + +func TestBulgarian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Other}, + {3, Other}, + {9, Other}, + {10, Other}, + {11, Other}, + {"0.1", Other}, + {"0.7", Other}, + {"1.0", One}, + {"1.001", Other}, + {onePlusEpsilon, Other}, + {"1.1", Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "bg", tests) +} + +func TestSwedish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "sv", tests) +} + +func TestUkrainian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "uk", tests) +} + +func appendIntTests(tests []pluralTest, from, to int, p Plural) []pluralTest { + for i := from; i <= to; i++ { + tests = append(tests, pluralTest{i, p}) + } + return tests +} + +func appendFloatTests(tests []pluralTest, from, to float64, p Plural) []pluralTest { + stride := 0.1 + format := "%.1f" + for f := from; f < to; f += stride { + tests = append(tests, pluralTest{fmt.Sprintf(format, f), p}) + } + tests = append(tests, pluralTest{fmt.Sprintf(format, to), p}) + return tests +} + +func runTests(t *testing.T, specID string, tests []pluralTest) { + spec := pluralSpecs[specID] + for _, test := range tests { + if plural, err := spec.Plural(test.num); plural != test.plural { + t.Errorf("%s: PluralCategory(%#v) returned %s, %v; expected %s", specID, test.num, plural, err, test.plural) + } + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation.go new file mode 100644 index 00000000000..4f579d16a3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation.go @@ -0,0 +1,78 @@ +package translation + +import ( + "github.com/nicksnyder/go-i18n/i18n/language" +) + +type pluralTranslation struct { + id string + templates map[language.Plural]*template +} + +func (pt *pluralTranslation) MarshalInterface() interface{} { + return map[string]interface{}{ + "id": pt.id, + "translation": pt.templates, + } +} + +func (pt *pluralTranslation) ID() string { + return pt.id +} + +func (pt *pluralTranslation) Template(pc language.Plural) *template { + return pt.templates[pc] +} + +func (pt *pluralTranslation) UntranslatedCopy() Translation { + return &pluralTranslation{pt.id, make(map[language.Plural]*template)} +} + +func (pt *pluralTranslation) Normalize(l *language.Language) Translation { + // Delete plural categories that don't belong to this language. + for pc := range pt.templates { + if _, ok := l.Plurals[pc]; !ok { + delete(pt.templates, pc) + } + } + // Create map entries for missing valid categories. + for pc := range l.Plurals { + if _, ok := pt.templates[pc]; !ok { + pt.templates[pc] = mustNewTemplate("") + } + } + return pt +} + +func (pt *pluralTranslation) Backfill(src Translation) Translation { + for pc, t := range pt.templates { + if t == nil || t.src == "" { + pt.templates[pc] = src.Template(language.Other) + } + } + return pt +} + +func (pt *pluralTranslation) Merge(t Translation) Translation { + other, ok := t.(*pluralTranslation) + if !ok || pt.ID() != t.ID() { + return t + } + for pluralCategory, template := range other.templates { + if template != nil && template.src != "" { + pt.templates[pluralCategory] = template + } + } + return pt +} + +func (pt *pluralTranslation) Incomplete(l *language.Language) bool { + for pc := range l.Plurals { + if t := pt.templates[pc]; t == nil || t.src == "" { + return true + } + } + return false +} + +var _ = Translation(&pluralTranslation{}) diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation_test.go new file mode 100644 index 00000000000..ea7de7fd984 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/plural_translation_test.go @@ -0,0 +1,308 @@ +package translation + +import ( + "reflect" + "testing" + + "github.com/nicksnyder/go-i18n/i18n/language" +) + +func mustTemplate(t *testing.T, src string) *template { + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + return tmpl +} + +func pluralTranslationFixture(t *testing.T, id string, pluralCategories ...language.Plural) *pluralTranslation { + templates := make(map[language.Plural]*template, len(pluralCategories)) + for _, pc := range pluralCategories { + templates[pc] = mustTemplate(t, string(pc)) + } + return &pluralTranslation{id, templates} +} + +func verifyDeepEqual(t *testing.T, actual, expected interface{}) { + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\n%#v\nnot equal to expected value\n%#v", actual, expected) + } +} + +func TestPluralTranslationMerge(t *testing.T) { + pt := pluralTranslationFixture(t, "id", language.One, language.Other) + oneTemplate, otherTemplate := pt.templates[language.One], pt.templates[language.Other] + + pt.Merge(pluralTranslationFixture(t, "id")) + verifyDeepEqual(t, pt.templates, map[language.Plural]*template{ + language.One: oneTemplate, + language.Other: otherTemplate, + }) + + pt2 := pluralTranslationFixture(t, "id", language.One, language.Two) + pt.Merge(pt2) + verifyDeepEqual(t, pt.templates, map[language.Plural]*template{ + language.One: pt2.templates[language.One], + language.Two: pt2.templates[language.Two], + language.Other: otherTemplate, + }) +} + +/* Test implementations from old idea + +func TestCopy(t *testing.T) { + ls := &LocalizedString{ + ID: "id", + Translation: testingTemplate(t, "translation {{.Hello}}"), + Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "plural {{.One}}"), + language.Other: testingTemplate(t, "plural {{.Other}}"), + }, + } + + c := ls.Copy() + delete(c.Translations, language.One) + if _, ok := ls.Translations[language.One]; !ok { + t.Errorf("deleting plural translation from copy deleted it from the original") + } + c.Translations[language.Two] = testingTemplate(t, "plural {{.Two}}") + if _, ok := ls.Translations[language.Two]; ok { + t.Errorf("adding plural translation to copy added it to the original") + } +} + +func TestNormalize(t *testing.T) { + oneTemplate := testingTemplate(t, "one {{.One}}") + ls := &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{ + language.One: oneTemplate, + language.Two: testingTemplate(t, "two {{.Two}}"), + }, + } + ls.Normalize(LanguageWithCode("en")) + if ls.Translation != nil { + t.Errorf("ls.Translation is %#v; expected nil", ls.Translation) + } + if actual := ls.Translations[language.Two]; actual != nil { + t.Errorf("ls.Translation[language.Two] is %#v; expected nil", actual) + } + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] is %#v; expected %#v", actual, oneTemplate) + } + if _, ok := ls.Translations[language.Other]; !ok { + t.Errorf("ls.Translations[language.Other] shouldn't be empty") + } +} + +func TestMergeTranslation(t *testing.T) { + ls := &LocalizedString{} + + translation := testingTemplate(t, "one {{.Hello}}") + ls.Merge(&LocalizedString{ + Translation: translation, + }) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } + + ls.Merge(&LocalizedString{}) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } + + translation = testingTemplate(t, "two {{.Hello}}") + ls.Merge(&LocalizedString{ + Translation: translation, + }) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } +} + +func TestMergeTranslations(t *testing.T) { + ls := &LocalizedString{} + + oneTemplate := testingTemplate(t, "one {{.One}}") + otherTemplate := testingTemplate(t, "other {{.Other}}") + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{ + language.One: oneTemplate, + language.Other: otherTemplate, + }, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } + + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{}, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } + + twoTemplate := testingTemplate(t, "two {{.Two}}") + otherTemplate = testingTemplate(t, "second other {{.Other}}") + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{ + language.Two: twoTemplate, + language.Other: otherTemplate, + }, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Two]; actual != twoTemplate { + t.Errorf("ls.Translations[language.Two] expected %#v; got %#v", twoTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } +} + +func TestMissingTranslations(t *testing.T) { + en := LanguageWithCode("en") + + tests := []struct { + localizedString *LocalizedString + language *Language + expected bool + }{ + { + &LocalizedString{}, + en, + true, + }, + { + &LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")}, + en, + false, + }, + { + &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: nil, + language.Other: nil, + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, ""), + language.Other: testingTemplate(t, ""), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + language.Other: testingTemplate(t, "other {{.Other}}"), + }}, + en, + false, + }, + } + + for _, tt := range tests { + if actual := tt.localizedString.MissingTranslations(tt.language); actual != tt.expected { + t.Errorf("expected %t got %t for %s, %#v", + tt.expected, actual, tt.language.code, tt.localizedString) + } + } +} + +func TestHasTranslations(t *testing.T) { + en := LanguageWithCode("en") + + tests := []struct { + localizedString *LocalizedString + language *Language + expected bool + }{ + { + &LocalizedString{}, + en, + false, + }, + { + &LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")}, + en, + true, + }, + { + &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{}}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.Two: testingTemplate(t, "two {{.Two}}"), + }}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: nil, + }}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, ""), + }}, + en, + false, + }, + } + + for _, tt := range tests { + if actual := tt.localizedString.HasTranslations(tt.language); actual != tt.expected { + t.Errorf("expected %t got %t for %s, %#v", + tt.expected, actual, tt.language.code, tt.localizedString) + } + } +} + +func testingTemplate(t *testing.T, src string) *template { + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + return tmpl +} +*/ diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/single_translation.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/single_translation.go new file mode 100644 index 00000000000..1010e5947ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/single_translation.go @@ -0,0 +1,57 @@ +package translation + +import ( + "github.com/nicksnyder/go-i18n/i18n/language" +) + +type singleTranslation struct { + id string + template *template +} + +func (st *singleTranslation) MarshalInterface() interface{} { + return map[string]interface{}{ + "id": st.id, + "translation": st.template, + } +} + +func (st *singleTranslation) ID() string { + return st.id +} + +func (st *singleTranslation) Template(pc language.Plural) *template { + return st.template +} + +func (st *singleTranslation) UntranslatedCopy() Translation { + return &singleTranslation{st.id, mustNewTemplate("")} +} + +func (st *singleTranslation) Normalize(language *language.Language) Translation { + return st +} + +func (st *singleTranslation) Backfill(src Translation) Translation { + if st.template == nil || st.template.src == "" { + st.template = src.Template(language.Other) + } + return st +} + +func (st *singleTranslation) Merge(t Translation) Translation { + other, ok := t.(*singleTranslation) + if !ok || st.ID() != t.ID() { + return t + } + if other.template != nil && other.template.src != "" { + st.template = other.template + } + return st +} + +func (st *singleTranslation) Incomplete(l *language.Language) bool { + return st.template == nil || st.template.src == "" +} + +var _ = Translation(&singleTranslation{}) diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template.go new file mode 100644 index 00000000000..1efc2078770 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template.go @@ -0,0 +1,80 @@ +package translation + +import ( + "bytes" + "encoding" + "strings" + //"launchpad.net/goyaml" + gotemplate "text/template" +) + +type template struct { + tmpl *gotemplate.Template + src string +} + +func newTemplate(src string) (*template, error) { + var tmpl template + err := tmpl.parseTemplate(src) + return &tmpl, err +} + +func mustNewTemplate(src string) *template { + t, err := newTemplate(src) + if err != nil { + panic(err) + } + return t +} + +func (t *template) String() string { + return t.src +} + +func (t *template) Execute(args interface{}) string { + if t.tmpl == nil { + return t.src + } + var buf bytes.Buffer + if err := t.tmpl.Execute(&buf, args); err != nil { + return err.Error() + } + return buf.String() +} + +func (t *template) MarshalText() ([]byte, error) { + return []byte(t.src), nil +} + +func (t *template) UnmarshalText(src []byte) error { + return t.parseTemplate(string(src)) +} + +func (t *template) parseTemplate(src string) (err error) { + t.src = src + if strings.Contains(src, "{{") { + t.tmpl, err = gotemplate.New(src).Parse(src) + } + return +} + +var _ = encoding.TextMarshaler(&template{}) +var _ = encoding.TextUnmarshaler(&template{}) + +/* +func (t *template) GetYAML() (tag string, value interface{}) { + return "", t.src +} + +func (t *template) SetYAML(tag string, value interface{}) bool { + panic(tag) + src, ok := value.(string) + if !ok { + return false + } + return t.parseTemplate(src) == nil +} + +var _ = goyaml.Getter(&template{}) +var _ = goyaml.Setter(&template{}) +*/ diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template_test.go new file mode 100644 index 00000000000..73a92340483 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/template_test.go @@ -0,0 +1,146 @@ +package translation + +import ( + "bytes" + "fmt" + //"launchpad.net/goyaml" + "testing" + gotemplate "text/template" +) + +func TestNilTemplate(t *testing.T) { + expected := "hello" + tmpl := &template{ + tmpl: nil, + src: expected, + } + if actual := tmpl.Execute(nil); actual != expected { + t.Errorf("Execute(nil) returned %s; expected %s", actual, expected) + } +} + +func TestMarshalText(t *testing.T) { + tmpl := &template{ + tmpl: gotemplate.Must(gotemplate.New("id").Parse("this is a {{.foo}} template")), + src: "boom", + } + expectedBuf := []byte(tmpl.src) + if buf, err := tmpl.MarshalText(); !bytes.Equal(buf, expectedBuf) || err != nil { + t.Errorf("MarshalText() returned %#v, %#v; expected %#v, nil", buf, err, expectedBuf) + } +} + +func TestUnmarshalText(t *testing.T) { + tmpl := &template{} + tmpl.UnmarshalText([]byte("hello {{.World}}")) + result := tmpl.Execute(map[string]string{ + "World": "world!", + }) + expected := "hello world!" + if result != expected { + t.Errorf("expected %#v; got %#v", expected, result) + } +} + +/* +func TestYAMLMarshal(t *testing.T) { + src := "hello {{.World}}" + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + buf, err := goyaml.Marshal(tmpl) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, []byte(src)) { + t.Fatalf(`expected "%s"; got "%s"`, src, buf) + } +} + +func TestYAMLUnmarshal(t *testing.T) { + buf := []byte(`Tmpl: "hello"`) + + var out struct { + Tmpl *template + } + var foo map[string]string + if err := goyaml.Unmarshal(buf, &foo); err != nil { + t.Fatal(err) + } + if out.Tmpl == nil { + t.Fatalf("out.Tmpl was nil") + } + if out.Tmpl.tmpl == nil { + t.Fatalf("out.Tmpl.tmpl was nil") + } + if expected := "hello {{.World}}"; out.Tmpl.src != expected { + t.Fatalf("expected %s; got %s", expected, out.Tmpl.src) + } +} + +func TestGetYAML(t *testing.T) { + src := "hello" + tmpl := &template{ + tmpl: nil, + src: src, + } + if tag, value := tmpl.GetYAML(); tag != "" || value != src { + t.Errorf("GetYAML() returned (%#v, %#v); expected (%#v, %#v)", tag, value, "", src) + } +} + +func TestSetYAML(t *testing.T) { + tmpl := &template{} + tmpl.SetYAML("tagDoesntMatter", "hello {{.World}}") + result := tmpl.Execute(map[string]string{ + "World": "world!", + }) + expected := "hello world!" + if result != expected { + t.Errorf("expected %#v; got %#v", expected, result) + } +} +*/ + +func BenchmarkExecuteNilTemplate(b *testing.B) { + template := &template{src: "hello world"} + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(nil) + } +} + +func BenchmarkExecuteHelloWorldTemplate(b *testing.B) { + template, err := newTemplate("hello world") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(nil) + } +} + +// Executing a simple template like this is ~6x slower than Sprintf +// but it is still only a few microseconds which should be sufficiently fast. +// The benefit is that we have nice semantic tags in the translation. +func BenchmarkExecuteHelloNameTemplate(b *testing.B) { + template, err := newTemplate("hello {{.Name}}") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(map[string]string{ + "Name": "Nick", + }) + } +} + +func BenchmarkSprintf(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + fmt.Sprintf("hello %s", "nick") + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation.go new file mode 100644 index 00000000000..d5d70d5af67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation.go @@ -0,0 +1,70 @@ +// Package translation defines the interface for a translation. +package translation + +import ( + "fmt" + + "github.com/nicksnyder/go-i18n/i18n/language" +) + +// Translation is the interface that represents a translated string. +type Translation interface { + // MarshalInterface returns the object that should be used + // to serialize the translation. + MarshalInterface() interface{} + ID() string + Template(language.Plural) *template + UntranslatedCopy() Translation + Normalize(language *language.Language) Translation + Backfill(src Translation) Translation + Merge(Translation) Translation + Incomplete(l *language.Language) bool +} + +// SortableByID implements sort.Interface for a slice of translations. +type SortableByID []Translation + +func (a SortableByID) Len() int { return len(a) } +func (a SortableByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortableByID) Less(i, j int) bool { return a[i].ID() < a[j].ID() } + +// NewTranslation reflects on data to create a new Translation. +// +// data["id"] must be a string and data["translation"] must be either a string +// for a non-plural translation or a map[string]interface{} for a plural translation. +func NewTranslation(data map[string]interface{}) (Translation, error) { + id, ok := data["id"].(string) + if !ok { + return nil, fmt.Errorf(`missing "id" key`) + } + switch translation := data["translation"].(type) { + case string: + tmpl, err := newTemplate(translation) + if err != nil { + return nil, err + } + return &singleTranslation{id, tmpl}, nil + case map[string]interface{}: + templates := make(map[language.Plural]*template, len(translation)) + for k, v := range translation { + pc, err := language.NewPlural(k) + if err != nil { + return nil, err + } + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf(`plural category "%s" has value of type %T; expected string`, pc, v) + } + tmpl, err := newTemplate(str) + if err != nil { + return nil, err + } + templates[pc] = tmpl + } + return &pluralTranslation{id, templates}, nil + case nil: + return nil, fmt.Errorf(`missing "translation" key`) + default: + return nil, fmt.Errorf(`unsupported type for "translation" key %T`, translation) + } +} diff --git a/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation_test.go b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation_test.go new file mode 100644 index 00000000000..7380d5a6f9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/nicksnyder/go-i18n/i18n/translation/translation_test.go @@ -0,0 +1,17 @@ +package translation + +import ( + "sort" + "testing" +) + +// Check this here to avoid unnecessary import of sort package. +var _ = sort.Interface(make(SortableByID, 0, 0)) + +func TestNewSingleTranslation(t *testing.T) { + t.Skipf("not implemented") +} + +func TestNewPluralTranslation(t *testing.T) { + t.Skipf("not implemented") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/.gitignore b/Godeps/_workspace/src/github.com/onsi/ginkgo/.gitignore new file mode 100644 index 00000000000..922b4f7f919 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +TODO +tmp/**/* +*.coverprofile \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/.travis.yml b/Godeps/_workspace/src/github.com/onsi/ginkgo/.travis.yml new file mode 100644 index 00000000000..988b7b29c0c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/.travis.yml @@ -0,0 +1,12 @@ +language: go +go: + - 1.3 + +install: + - go get -v ./... + - go get code.google.com/p/go.tools/cmd/cover + - go get github.com/onsi/gomega + - go install github.com/onsi/ginkgo/ginkgo + - export PATH=$PATH:$HOME/gopath/bin + +script: $HOME/gopath/bin/ginkgo -r --randomizeAllSpecs --failOnPending --randomizeSuites --race diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/CHANGELOG.md b/Godeps/_workspace/src/github.com/onsi/ginkgo/CHANGELOG.md new file mode 100644 index 00000000000..5309ac1c8a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/CHANGELOG.md @@ -0,0 +1,105 @@ +## HEAD + +Improvements: + +- Call reporters in reverse order when announcing spec completion -- allows custom reporters to emit output before the default reporter does. +- Improved focus behavior. Now, this: + + ```golang + FDescribe("Some describe", func() { + It("A", func() {}) + + FIt("B", func() {}) + }) + ``` + + will run `B` but *not* `A`. This tends to be a common usage pattern when in the thick of writing and debugging tests. +- When `SIGINT` is received, Ginkgo will emit the contents of the `GinkgoWriter` before running the `AfterSuite`. Useful for debugging stuck tests. +- When `--progress` is set, Ginkgo will write test progress (in particular, Ginkgo will say when it is about to run a BeforeEach, AfterEach, It, etc...) to the `GinkgoWriter`. This is useful for debugging stuck tests and tests that generate many logs. +- Improved output when an error occurs in a setup or teardown block. +- When `--dryRun` is set, Ginkgo will walk the spec tree and emit to its reporter *without* actually running anything. Best paired with `-v` to understand which specs will run in which order. +- Add `By` to help document long `It`s. `By` simply writes to the `GinkgoWriter`. +- Add support for precompiled tests: + - `ginkgo build ` will now compile the package, producing a file named `package.test` + - The compiled `package.test` file can be run directly. This runs the tests in series. + - To run precompiled tests in parallel, you can run: `ginkgo -p package.test` +- Support `bootstrap`ping and `generate`ing [Agouti](http://agouti.org) specs. + +Bug Fixes: + +- If --skipPackages is used and all packages are skipped, Ginkgo should exit 0. + +## 1.1.0 (8/2/2014) + +No changes, just dropping the beta. + +## 1.1.0-beta (7/22/2014) +New Features: + +- `ginkgo watch` now monitors packages *and their dependencies* for changes. The depth of the dependency tree can be modified with the `-depth` flag. +- Test suites with a programmatic focus (`FIt`, `FDescribe`, etc...) exit with non-zero status code, evne when they pass. This allows CI systems to detect accidental commits of focused test suites. +- `ginkgo -p` runs the testsuite in parallel with an auto-detected number of nodes. +- `ginkgo -tags=TAG_LIST` passes a list of tags down to the `go build` command. +- `ginkgo --failFast` aborts the test suite after the first failure. +- `ginkgo generate file_1 file_2` can take multiple file arguments. +- Ginkgo now summarizes any spec failures that occured at the end of the test run. +- `ginkgo --randomizeSuites` will run tests *suites* in random order using the generated/passed-in seed. + +Improvements: + +- `ginkgo -skipPackage` now takes a comma-separated list of strings. If the *relative path* to a package matches one of the entries in the comma-separated list, that package is skipped. +- `ginkgo --untilItFails` no longer recompiles between attempts. +- Ginkgo now panics when a runnable node (`It`, `BeforeEach`, `JustBeforeEach`, `AfterEach`, `Measure`) is nested within another runnable node. This is always a mistake. Any test suites that panic because of this change should be fixed. + +Bug Fixes: + +- `ginkgo boostrap` and `ginkgo generate` no longer fail when dealing with `hyphen-separated-packages`. +- parallel specs are now better distributed across nodes - fixed a crashing bug where (for example) distributing 11 tests across 7 nodes would panic + +## 1.0.0 (5/24/2014) +New Features: + +- Add `GinkgoParallelNode()` - shorthand for `config.GinkgoConfig.ParallelNode` + +Improvements: + +- When compilation fails, the compilation output is rewritten to present a correct *relative* path. Allows ⌘-clicking in iTerm open the file in your text editor. +- `--untilItFails` and `ginkgo watch` now generate new random seeds between test runs, unless a particular random seed is specified. + +Bug Fixes: + +- `-cover` now generates a correctly combined coverprofile when running with in parallel with multiple `-node`s. +- Print out the contents of the `GinkgoWriter` when `BeforeSuite` or `AfterSuite` fail. +- Fix all remaining race conditions in Ginkgo's test suite. + +## 1.0.0-beta (4/14/2014) +Breaking changes: + +- `thirdparty/gomocktestreporter` is gone. Use `GinkgoT()` instead +- Modified the Reporter interface +- `watch` is now a subcommand, not a flag. + +DSL changes: + +- `BeforeSuite` and `AfterSuite` for setting up and tearing down test suites. +- `AfterSuite` is triggered on interrupt (`^C`) as well as exit. +- `SynchronizedBeforeSuite` and `SynchronizedAfterSuite` for setting up and tearing down singleton resources across parallel nodes. + +CLI changes: + +- `watch` is now a subcommand, not a flag +- `--nodot` flag can be passed to `ginkgo generate` and `ginkgo bootstrap` to avoid dot imports. This explicitly imports all exported identifiers in Ginkgo and Gomega. Refreshing this list can be done by running `ginkgo nodot` +- Additional arguments can be passed to specs. Pass them after the `--` separator +- `--skipPackage` flag takes a regexp and ignores any packages with package names passing said regexp. +- `--trace` flag prints out full stack traces when errors occur, not just the line at which the error occurs. + +Misc: + +- Start using semantic versioning +- Start maintaining changelog + +Major refactor: + +- Pull out Ginkgo's internal to `internal` +- Rename `example` everywhere to `spec` +- Much more! diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/LICENSE b/Godeps/_workspace/src/github.com/onsi/ginkgo/LICENSE new file mode 100644 index 00000000000..9415ee72c17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/README.md b/Godeps/_workspace/src/github.com/onsi/ginkgo/README.md new file mode 100644 index 00000000000..5cb6fdc843e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/README.md @@ -0,0 +1,115 @@ +![Ginkgo: A Golang BDD Testing Framework](http://onsi.github.io/ginkgo/images/ginkgo.png) + +[![Build Status](https://travis-ci.org/onsi/ginkgo.png)](https://travis-ci.org/onsi/ginkgo) + +Jump to the [docs](http://onsi.github.io/ginkgo/) to learn more. To start rolling your Ginkgo tests *now* [keep reading](#set-me-up)! + +To discuss Ginkgo and get updates, join the [google group](https://groups.google.com/d/forum/ginkgo-and-gomega). + +## Feature List + +- Ginkgo uses Go's `testing` package and can live alongside your existing `testing` tests. It's easy to [bootstrap](http://onsi.github.io/ginkgo/#bootstrapping-a-suite) and start writing your [first tests](http://onsi.github.io/ginkgo/#adding-specs-to-a-suite) + +- Structure your BDD-style tests expressively: + - Nestable [`Describe` and `Context` container blocks](http://onsi.github.io/ginkgo/#organizing-specs-with-containers-describe-and-context) + - [`BeforeEach` and `AfterEach` blocks](http://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach) for setup and teardown + - [`It` blocks](http://onsi.github.io/ginkgo/#individual-specs-) that hold your assertions + - [`JustBeforeEach` blocks](http://onsi.github.io/ginkgo/#separating-creation-and-configuration-justbeforeeach) that separate creation from configuration (also known as the subject action pattern). + - [`BeforeSuite` and `AfterSuite` blocks](http://onsi.github.io/ginkgo/#global-setup-and-teardown-beforesuite-and-aftersuite) to prep for and cleanup after a suite. + +- A comprehensive test runner that lets you: + - Mark specs as [pending](http://onsi.github.io/ginkgo/#pending-specs) + - [Focus](http://onsi.github.io/ginkgo/#focused-specs) individual specs, and groups of specs, either programmatically or on the command line + - Run your tests in [random order](http://onsi.github.io/ginkgo/#spec-permutation), and then reuse random seeds to replicate the same order. + - Break up your test suite into parallel processes for straightforward [test parallelization](http://onsi.github.io/ginkgo/#parallel-specs) + +- `ginkgo`: a command line interface with plenty of handy command line arguments for [running your tests](http://onsi.github.io/ginkgo/#running-tests) and [generating](http://onsi.github.io/ginkgo/#generators) test files. Here are a few choice examples: + - `ginkgo -nodes=N` runs your tests in `N` parallel processes and print out coherent output in realtime + - `ginkgo -cover` runs your tests using Golang's code coverage tool + - `ginkgo convert` converts an XUnit-style `testing` package to a Ginkgo-style package + - `ginkgo -focus="REGEXP"` and `ginkgo -skip="REGEXP"` allow you to specify a subset of tests to run via regular expression + - `ginkgo -r` runs all tests suites under the current directory + - `ginkgo -v` prints out identifying information for each tests just before it runs + + And much more: run `ginkgo help` for details! + + The `ginkgo` CLI is convenient, but purely optional -- Ginkgo works just fine with `go test` + +- `ginkgo watch` [watches](https://onsi.github.io/ginkgo/#watching-for-changes) packages *and their dependencies* for changes, then reruns tests. Run tests immediately as you develop! + +- Built-in support for testing [asynchronicity](http://onsi.github.io/ginkgo/#asynchronous-tests) + +- Built-in support for [benchmarking](http://onsi.github.io/ginkgo/#benchmark-tests) your code. Control the number of benchmark samples as you gather runtimes and other, arbitrary, bits of numerical information about your code. + +- [Completions for Sublime Text](https://github.com/onsi/ginkgo-sublime-completions): just use [Package Control](https://sublime.wbond.net/) to install `Ginkgo Completions`. + +- Straightforward support for third-party testing libraries such as [Gomock](https://code.google.com/p/gomock/) and [Testify](https://github.com/stretchr/testify). Check out the [docs](http://onsi.github.io/ginkgo/#third-party-integrations) for details. + +- A modular architecture that lets you easily: + - Write [custom reporters](http://onsi.github.io/ginkgo/#writing-custom-reporters) (for example, Ginkgo comes with a [JUnit XML reporter](http://onsi.github.io/ginkgo/#generating-junit-xml-output) and a TeamCity reporter). + - [Adapt an existing matcher library (or write your own!)](http://onsi.github.io/ginkgo/#using-other-matcher-libraries) to work with Ginkgo + +## [Gomega](http://github.com/onsi/gomega): Ginkgo's Preferred Matcher Library + +Ginkgo is best paired with Gomega. Learn more about Gomega [here](http://onsi.github.io/gomega/) + +## [Agouti](http://github.com/sclevine/agouti): A Golang Acceptance Testing Framework + +Agouti allows you run WebDriver integration tests. Learn more about Agouti [here](http://agouti.org) + +## Set Me Up! + +You'll need Golang v1.2+ (Ubuntu users: you probably have Golang v1.0 -- you'll need to upgrade!) + +```bash + +go get github.com/onsi/ginkgo/ginkgo # installs the ginkgo CLI +go get github.com/onsi/gomega # fetches the matcher library + +cd path/to/package/you/want/to/test + +ginkgo bootstrap # set up a new ginkgo suite +ginkgo generate # will create a sample test file. edit this file and add your tests then... + +go test # to run your tests + +ginkgo # also runs your tests + +``` + +## I'm new to Go: What are my testing options? + +Of course, I heartily recommend [Ginkgo](https://github.com/onsi/ginkgo) and [Gomega](https://github.com/onsi/gomega). Both packages are seeing heavy, daily, production use on a number of projects and boast a mature and comprehensive feature-set. + +With that said, it's great to know what your options are :) + +### What Golang gives you out of the box + +Testing is a first class citizen in Golang, however Go's built-in testing primitives are somewhat limited: The [testing](http://golang.org/pkg/testing) package provides basic XUnit style tests and no assertion library. + +### Matcher libraries for Golang's XUnit style tests + +A number of matcher libraries have been written to augment Go's built-in XUnit style tests. Here are two that have gained traction: + +- [testify](https://github.com/stretchr/testify) +- [gocheck](http://labix.org/gocheck) + +You can also use Ginkgo's matcher library [Gomega](https://github.com/onsi/gomega) in [XUnit style tests](http://onsi.github.io/gomega/#using-gomega-with-golangs-xunitstyle-tests) + +### BDD style testing frameworks + +There are a handful of BDD-style testing frameworks written for Golang. Here are a few: + +- [Ginkgo](https://github.com/onsi/ginkgo) ;) +- [GoConvey](https://github.com/smartystreets/goconvey) +- [Goblin](https://github.com/franela/goblin) +- [Mao](https://github.com/azer/mao) +- [Zen](https://github.com/pranavraja/zen) + +Finally, @shageman has [put together](https://github.com/shageman/gotestit) a comprehensive comparison of golang testing libraries. + +Go explore! + +## License + +Ginkgo is MIT-Licensed diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/config/config.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/config/config.go new file mode 100644 index 00000000000..aa4d8ed40cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/config/config.go @@ -0,0 +1,170 @@ +/* +Ginkgo accepts a number of configuration options. + +These are documented [here](http://onsi.github.io/ginkgo/#the_ginkgo_cli) + +You can also learn more via + + ginkgo help + +or (I kid you not): + + go test -asdf +*/ +package config + +import ( + "flag" + "time" + + "fmt" +) + +const VERSION = "1.1.0" + +type GinkgoConfigType struct { + RandomSeed int64 + RandomizeAllSpecs bool + FocusString string + SkipString string + SkipMeasurements bool + FailOnPending bool + FailFast bool + EmitSpecProgress bool + DryRun bool + + ParallelNode int + ParallelTotal int + SyncHost string + StreamHost string +} + +var GinkgoConfig = GinkgoConfigType{} + +type DefaultReporterConfigType struct { + NoColor bool + SlowSpecThreshold float64 + NoisyPendings bool + Succinct bool + Verbose bool + FullTrace bool +} + +var DefaultReporterConfig = DefaultReporterConfigType{} + +func processPrefix(prefix string) string { + if prefix != "" { + prefix = prefix + "." + } + return prefix +} + +func Flags(flagSet *flag.FlagSet, prefix string, includeParallelFlags bool) { + prefix = processPrefix(prefix) + flagSet.Int64Var(&(GinkgoConfig.RandomSeed), prefix+"seed", time.Now().Unix(), "The seed used to randomize the spec suite.") + flagSet.BoolVar(&(GinkgoConfig.RandomizeAllSpecs), prefix+"randomizeAllSpecs", false, "If set, ginkgo will randomize all specs together. By default, ginkgo only randomizes the top level Describe/Context groups.") + flagSet.BoolVar(&(GinkgoConfig.SkipMeasurements), prefix+"skipMeasurements", false, "If set, ginkgo will skip any measurement specs.") + flagSet.BoolVar(&(GinkgoConfig.FailOnPending), prefix+"failOnPending", false, "If set, ginkgo will mark the test suite as failed if any specs are pending.") + flagSet.BoolVar(&(GinkgoConfig.FailFast), prefix+"failFast", false, "If set, ginkgo will stop running a test suite after a failure occurs.") + flagSet.BoolVar(&(GinkgoConfig.DryRun), prefix+"dryRun", false, "If set, ginkgo will walk the test hierarchy without actually running anything. Best paired with -v.") + flagSet.StringVar(&(GinkgoConfig.FocusString), prefix+"focus", "", "If set, ginkgo will only run specs that match this regular expression.") + flagSet.StringVar(&(GinkgoConfig.SkipString), prefix+"skip", "", "If set, ginkgo will only run specs that do not match this regular expression.") + flagSet.BoolVar(&(GinkgoConfig.EmitSpecProgress), prefix+"progress", false, "If set, ginkgo will emit progress information as each spec runs to the GinkgoWriter.") + + if includeParallelFlags { + flagSet.IntVar(&(GinkgoConfig.ParallelNode), prefix+"parallel.node", 1, "This worker node's (one-indexed) node number. For running specs in parallel.") + flagSet.IntVar(&(GinkgoConfig.ParallelTotal), prefix+"parallel.total", 1, "The total number of worker nodes. For running specs in parallel.") + flagSet.StringVar(&(GinkgoConfig.SyncHost), prefix+"parallel.synchost", "", "The address for the server that will synchronize the running nodes.") + flagSet.StringVar(&(GinkgoConfig.StreamHost), prefix+"parallel.streamhost", "", "The address for the server that the running nodes should stream data to.") + } + + flagSet.BoolVar(&(DefaultReporterConfig.NoColor), prefix+"noColor", false, "If set, suppress color output in default reporter.") + flagSet.Float64Var(&(DefaultReporterConfig.SlowSpecThreshold), prefix+"slowSpecThreshold", 5.0, "(in seconds) Specs that take longer to run than this threshold are flagged as slow by the default reporter (default: 5 seconds).") + flagSet.BoolVar(&(DefaultReporterConfig.NoisyPendings), prefix+"noisyPendings", true, "If set, default reporter will shout about pending tests.") + flagSet.BoolVar(&(DefaultReporterConfig.Verbose), prefix+"v", false, "If set, default reporter print out all specs as they begin.") + flagSet.BoolVar(&(DefaultReporterConfig.Succinct), prefix+"succinct", false, "If set, default reporter prints out a very succinct report") + flagSet.BoolVar(&(DefaultReporterConfig.FullTrace), prefix+"trace", false, "If set, default reporter prints out the full stack trace when a failure occurs") +} + +func BuildFlagArgs(prefix string, ginkgo GinkgoConfigType, reporter DefaultReporterConfigType) []string { + prefix = processPrefix(prefix) + result := make([]string, 0) + + if ginkgo.RandomSeed > 0 { + result = append(result, fmt.Sprintf("--%sseed=%d", prefix, ginkgo.RandomSeed)) + } + + if ginkgo.RandomizeAllSpecs { + result = append(result, fmt.Sprintf("--%srandomizeAllSpecs", prefix)) + } + + if ginkgo.SkipMeasurements { + result = append(result, fmt.Sprintf("--%sskipMeasurements", prefix)) + } + + if ginkgo.FailOnPending { + result = append(result, fmt.Sprintf("--%sfailOnPending", prefix)) + } + + if ginkgo.FailFast { + result = append(result, fmt.Sprintf("--%sfailFast", prefix)) + } + + if ginkgo.DryRun { + result = append(result, fmt.Sprintf("--%sdryRun", prefix)) + } + + if ginkgo.FocusString != "" { + result = append(result, fmt.Sprintf("--%sfocus=%s", prefix, ginkgo.FocusString)) + } + + if ginkgo.SkipString != "" { + result = append(result, fmt.Sprintf("--%sskip=%s", prefix, ginkgo.SkipString)) + } + + if ginkgo.EmitSpecProgress { + result = append(result, fmt.Sprintf("--%sprogress", prefix)) + } + + if ginkgo.ParallelNode != 0 { + result = append(result, fmt.Sprintf("--%sparallel.node=%d", prefix, ginkgo.ParallelNode)) + } + + if ginkgo.ParallelTotal != 0 { + result = append(result, fmt.Sprintf("--%sparallel.total=%d", prefix, ginkgo.ParallelTotal)) + } + + if ginkgo.StreamHost != "" { + result = append(result, fmt.Sprintf("--%sparallel.streamhost=%s", prefix, ginkgo.StreamHost)) + } + + if ginkgo.SyncHost != "" { + result = append(result, fmt.Sprintf("--%sparallel.synchost=%s", prefix, ginkgo.SyncHost)) + } + + if reporter.NoColor { + result = append(result, fmt.Sprintf("--%snoColor", prefix)) + } + + if reporter.SlowSpecThreshold > 0 { + result = append(result, fmt.Sprintf("--%sslowSpecThreshold=%.5f", prefix, reporter.SlowSpecThreshold)) + } + + if !reporter.NoisyPendings { + result = append(result, fmt.Sprintf("--%snoisyPendings=false", prefix)) + } + + if reporter.Verbose { + result = append(result, fmt.Sprintf("--%sv", prefix)) + } + + if reporter.Succinct { + result = append(result, fmt.Sprintf("--%ssuccinct", prefix)) + } + + if reporter.FullTrace { + result = append(result, fmt.Sprintf("--%strace", prefix)) + } + + return result +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/bootstrap_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/bootstrap_command.go new file mode 100644 index 00000000000..b6260795ae4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/bootstrap_command.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/onsi/ginkgo/ginkgo/nodot" +) + +func BuildBootstrapCommand() *Command { + var agouti, noDot bool + flagSet := flag.NewFlagSet("bootstrap", flag.ExitOnError) + flagSet.BoolVar(&agouti, "agouti", false, "If set, bootstrap will generate a bootstrap file for writing Agouti tests") + flagSet.BoolVar(&noDot, "nodot", false, "If set, bootstrap will generate a bootstrap file that does not . import ginkgo and gomega") + + return &Command{ + Name: "bootstrap", + FlagSet: flagSet, + UsageCommand: "ginkgo bootstrap ", + Usage: []string{ + "Bootstrap a test suite for the current package", + "Accepts the following flags:", + }, + Command: func(args []string, additionalArgs []string) { + generateBootstrap(agouti, noDot) + }, + } +} + +var bootstrapText = `package {{.Package}}_test + +import ( + {{.GinkgoImport}} + {{.GomegaImport}} + + "testing" +) + +func Test{{.FormattedPackage}}(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "{{.FormattedPackage}} Suite") +} +` + +var agoutiBootstrapText = `package {{.Package}}_test + +import ( + {{.GinkgoImport}} + {{.GomegaImport}} + . "github.com/sclevine/agouti/core" + + "testing" +) + +func Test{{.FormattedPackage}}(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "{{.FormattedPackage}} Suite") +} + +var agoutiDriver WebDriver + +var _ = BeforeSuite(func() { + var err error + + // Choose a WebDriver: + + agoutiDriver, err = PhantomJS() + // agoutiDriver, err = Selenium() + // agoutiDriver, err = Chrome() + + Expect(err).NotTo(HaveOccurred()) + Expect(agoutiDriver.Start()).To(Succeed()) +}) + +var _ = AfterSuite(func() { + agoutiDriver.Stop() +}) +` + +type bootstrapData struct { + Package string + FormattedPackage string + GinkgoImport string + GomegaImport string +} + +func getPackage() string { + workingDir, err := os.Getwd() + if err != nil { + complainAndQuit("Could not find package: " + err.Error()) + } + packageName := filepath.Base(workingDir) + return strings.Replace(packageName, "-", "_", -1) +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + return false +} + +func generateBootstrap(agouti bool, noDot bool) { + packageName := getPackage() + formattedPackage := strings.Replace(strings.Title(strings.Replace(packageName, "_", " ", -1)), " ", "", -1) + data := bootstrapData{ + Package: packageName, + FormattedPackage: formattedPackage, + GinkgoImport: `. "github.com/onsi/ginkgo"`, + GomegaImport: `. "github.com/onsi/gomega"`, + } + + if noDot { + data.GinkgoImport = `"github.com/onsi/ginkgo"` + data.GomegaImport = `"github.com/onsi/gomega"` + } + + targetFile := fmt.Sprintf("%s_suite_test.go", packageName) + if fileExists(targetFile) { + fmt.Printf("%s already exists.\n\n", targetFile) + os.Exit(1) + } else { + fmt.Printf("Generating ginkgo test suite bootstrap for %s in:\n\t%s\n", packageName, targetFile) + } + + f, err := os.Create(targetFile) + if err != nil { + complainAndQuit("Could not create file: " + err.Error()) + panic(err.Error()) + } + defer f.Close() + + var templateText string + if agouti { + templateText = agoutiBootstrapText + } else { + templateText = bootstrapText + } + + bootstrapTemplate, err := template.New("bootstrap").Parse(templateText) + if err != nil { + panic(err.Error()) + } + + buf := &bytes.Buffer{} + bootstrapTemplate.Execute(buf, data) + + if noDot { + contents, err := nodot.ApplyNoDot(buf.Bytes()) + if err != nil { + complainAndQuit("Failed to import nodot declarations: " + err.Error()) + } + fmt.Println("To update the nodot declarations in the future, switch to this directory and run:\n\tginkgo nodot") + buf = bytes.NewBuffer(contents) + } + + buf.WriteTo(f) + + goFmt(targetFile) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/build_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/build_command.go new file mode 100644 index 00000000000..78d99d95e74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/build_command.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/onsi/ginkgo/ginkgo/testrunner" +) + +func BuildBuildCommand() *Command { + commandFlags := NewBuildCommandFlags(flag.NewFlagSet("build", flag.ExitOnError)) + interruptHandler := NewInterruptHandler() + builder := &SpecBuilder{ + commandFlags: commandFlags, + interruptHandler: interruptHandler, + } + + return &Command{ + Name: "build", + FlagSet: commandFlags.FlagSet, + UsageCommand: "ginkgo build ", + Usage: []string{ + "Build the passed in (or the package in the current directory if left blank).", + "Accepts the following flags:", + }, + Command: builder.BuildSpecs, + } +} + +type SpecBuilder struct { + commandFlags *RunWatchAndBuildCommandFlags + interruptHandler *InterruptHandler +} + +func (r *SpecBuilder) BuildSpecs(args []string, additionalArgs []string) { + r.commandFlags.computeNodes() + + suites, _ := findSuites(args, r.commandFlags.Recurse, r.commandFlags.SkipPackage, false) + + if len(suites) == 0 { + complainAndQuit("Found no test suites") + } + + passed := true + for _, suite := range suites { + runner := testrunner.New(suite, 1, false, r.commandFlags.Race, r.commandFlags.Cover, r.commandFlags.Tags, nil) + fmt.Printf("Compiling %s...\n", suite.PackageName) + err := runner.Compile() + if err != nil { + fmt.Println(err.Error()) + passed = false + } else { + fmt.Printf(" compiled %s.test\n", filepath.Join(suite.Path, suite.PackageName)) + } + } + + if passed { + os.Exit(0) + } + os.Exit(1) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/ginkgo_ast_nodes.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/ginkgo_ast_nodes.go new file mode 100644 index 00000000000..02e2b3b328d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/ginkgo_ast_nodes.go @@ -0,0 +1,123 @@ +package convert + +import ( + "fmt" + "go/ast" + "strings" + "unicode" +) + +/* + * Creates a func init() node + */ +func createVarUnderscoreBlock() *ast.ValueSpec { + valueSpec := &ast.ValueSpec{} + object := &ast.Object{Kind: 4, Name: "_", Decl: valueSpec, Data: 0} + ident := &ast.Ident{Name: "_", Obj: object} + valueSpec.Names = append(valueSpec.Names, ident) + return valueSpec +} + +/* + * Creates a Describe("Testing with ginkgo", func() { }) node + */ +func createDescribeBlock() *ast.CallExpr { + blockStatement := &ast.BlockStmt{List: []ast.Stmt{}} + + fieldList := &ast.FieldList{} + funcType := &ast.FuncType{Params: fieldList} + funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement} + basicLit := &ast.BasicLit{Kind: 9, Value: "\"Testing with Ginkgo\""} + describeIdent := &ast.Ident{Name: "Describe"} + return &ast.CallExpr{Fun: describeIdent, Args: []ast.Expr{basicLit, funcLit}} +} + +/* + * Convenience function to return the name of the *testing.T param + * for a Test function that will be rewritten. This is useful because + * we will want to replace the usage of this named *testing.T inside the + * body of the function with a GinktoT. + */ +func namedTestingTArg(node *ast.FuncDecl) string { + return node.Type.Params.List[0].Names[0].Name // *exhale* +} + +/* + * Convenience function to return the block statement node for a Describe statement + */ +func blockStatementFromDescribe(desc *ast.CallExpr) *ast.BlockStmt { + var funcLit *ast.FuncLit + var found = false + + for _, node := range desc.Args { + switch node := node.(type) { + case *ast.FuncLit: + found = true + funcLit = node + break + } + } + + if !found { + panic("Error finding ast.FuncLit inside describe statement. Somebody done goofed.") + } + + return funcLit.Body +} + +/* convenience function for creating an It("TestNameHere") + * with all the body of the test function inside the anonymous + * func passed to It() + */ +func createItStatementForTestFunc(testFunc *ast.FuncDecl) *ast.ExprStmt { + blockStatement := &ast.BlockStmt{List: testFunc.Body.List} + fieldList := &ast.FieldList{} + funcType := &ast.FuncType{Params: fieldList} + funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement} + + testName := rewriteTestName(testFunc.Name.Name) + basicLit := &ast.BasicLit{Kind: 9, Value: fmt.Sprintf("\"%s\"", testName)} + itBlockIdent := &ast.Ident{Name: "It"} + callExpr := &ast.CallExpr{Fun: itBlockIdent, Args: []ast.Expr{basicLit, funcLit}} + return &ast.ExprStmt{X: callExpr} +} + +/* +* rewrite test names to be human readable +* eg: rewrites "TestSomethingAmazing" as "something amazing" + */ +func rewriteTestName(testName string) string { + nameComponents := []string{} + currentString := "" + indexOfTest := strings.Index(testName, "Test") + if indexOfTest != 0 { + return testName + } + + testName = strings.Replace(testName, "Test", "", 1) + first, rest := testName[0], testName[1:] + testName = string(unicode.ToLower(rune(first))) + rest + + for _, rune := range testName { + if unicode.IsUpper(rune) { + nameComponents = append(nameComponents, currentString) + currentString = string(unicode.ToLower(rune)) + } else { + currentString += string(rune) + } + } + + return strings.Join(append(nameComponents, currentString), " ") +} + +func newGinkgoTFromIdent(ident *ast.Ident) *ast.CallExpr { + return &ast.CallExpr{ + Lparen: ident.NamePos + 1, + Rparen: ident.NamePos + 2, + Fun: &ast.Ident{Name: "GinkgoT"}, + } +} + +func newGinkgoTInterface() *ast.Ident { + return &ast.Ident{Name: "GinkgoTInterface"} +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/import.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/import.go new file mode 100644 index 00000000000..e226196f72e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/import.go @@ -0,0 +1,91 @@ +package convert + +import ( + "errors" + "fmt" + "go/ast" +) + +/* + * Given the root node of an AST, returns the node containing the + * import statements for the file. + */ +func importsForRootNode(rootNode *ast.File) (imports *ast.GenDecl, err error) { + for _, declaration := range rootNode.Decls { + decl, ok := declaration.(*ast.GenDecl) + if !ok || len(decl.Specs) == 0 { + continue + } + + _, ok = decl.Specs[0].(*ast.ImportSpec) + if ok { + imports = decl + return + } + } + + err = errors.New(fmt.Sprintf("Could not find imports for root node:\n\t%#v\n", rootNode)) + return +} + +/* + * Removes "testing" import, if present + */ +func removeTestingImport(rootNode *ast.File) { + importDecl, err := importsForRootNode(rootNode) + if err != nil { + panic(err.Error()) + } + + var index int + for i, importSpec := range importDecl.Specs { + importSpec := importSpec.(*ast.ImportSpec) + if importSpec.Path.Value == "\"testing\"" { + index = i + break + } + } + + importDecl.Specs = append(importDecl.Specs[:index], importDecl.Specs[index+1:]...) +} + +/* + * Adds import statements for onsi/ginkgo, if missing + */ +func addGinkgoImports(rootNode *ast.File) { + importDecl, err := importsForRootNode(rootNode) + if err != nil { + panic(err.Error()) + } + + if len(importDecl.Specs) == 0 { + // TODO: might need to create a import decl here + panic("unimplemented : expected to find an imports block") + } + + needsGinkgo := true + for _, importSpec := range importDecl.Specs { + importSpec, ok := importSpec.(*ast.ImportSpec) + if !ok { + continue + } + + if importSpec.Path.Value == "\"github.com/onsi/ginkgo\"" { + needsGinkgo = false + } + } + + if needsGinkgo { + importDecl.Specs = append(importDecl.Specs, createImport(".", "\"github.com/onsi/ginkgo\"")) + } +} + +/* + * convenience function to create an import statement + */ +func createImport(name, path string) *ast.ImportSpec { + return &ast.ImportSpec{ + Name: &ast.Ident{Name: name}, + Path: &ast.BasicLit{Kind: 9, Value: path}, + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/package_rewriter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/package_rewriter.go new file mode 100644 index 00000000000..ed09c460d4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/package_rewriter.go @@ -0,0 +1,127 @@ +package convert + +import ( + "fmt" + "go/build" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" +) + +/* + * RewritePackage takes a name (eg: my-package/tools), finds its test files using + * Go's build package, and then rewrites them. A ginkgo test suite file will + * also be added for this package, and all of its child packages. + */ +func RewritePackage(packageName string) { + pkg, err := packageWithName(packageName) + if err != nil { + panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", packageName, err.Error())) + } + + for _, filename := range findTestsInPackage(pkg) { + rewriteTestsInFile(filename) + } + return +} + +/* + * Given a package, findTestsInPackage reads the test files in the directory, + * and then recurses on each child package, returning a slice of all test files + * found in this process. + */ +func findTestsInPackage(pkg *build.Package) (testfiles []string) { + for _, file := range append(pkg.TestGoFiles, pkg.XTestGoFiles...) { + testfiles = append(testfiles, filepath.Join(pkg.Dir, file)) + } + + dirFiles, err := ioutil.ReadDir(pkg.Dir) + if err != nil { + panic(fmt.Sprintf("unexpected error reading dir: '%s'\n%s\n", pkg.Dir, err.Error())) + } + + re := regexp.MustCompile(`^[._]`) + + for _, file := range dirFiles { + if !file.IsDir() { + continue + } + + if re.Match([]byte(file.Name())) { + continue + } + + packageName := filepath.Join(pkg.ImportPath, file.Name()) + subPackage, err := packageWithName(packageName) + if err != nil { + panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", packageName, err.Error())) + } + + testfiles = append(testfiles, findTestsInPackage(subPackage)...) + } + + addGinkgoSuiteForPackage(pkg) + goFmtPackage(pkg) + return +} + +/* + * Shells out to `ginkgo bootstrap` to create a test suite file + */ +func addGinkgoSuiteForPackage(pkg *build.Package) { + originalDir, err := os.Getwd() + if err != nil { + panic(err) + } + + suite_test_file := filepath.Join(pkg.Dir, pkg.Name+"_suite_test.go") + + _, err = os.Stat(suite_test_file) + if err == nil { + return // test file already exists, this should be a no-op + } + + err = os.Chdir(pkg.Dir) + if err != nil { + panic(err) + } + + output, err := exec.Command("ginkgo", "bootstrap").Output() + + if err != nil { + panic(fmt.Sprintf("error running 'ginkgo bootstrap'.\nstdout: %s\n%s\n", output, err.Error())) + } + + err = os.Chdir(originalDir) + if err != nil { + panic(err) + } +} + +/* + * Shells out to `go fmt` to format the package + */ +func goFmtPackage(pkg *build.Package) { + output, err := exec.Command("go", "fmt", pkg.ImportPath).Output() + + if err != nil { + fmt.Printf("Warning: Error running 'go fmt %s'.\nstdout: %s\n%s\n", pkg.ImportPath, output, err.Error()) + } +} + +/* + * Attempts to return a package with its test files already read. + * The ImportMode arg to build.Import lets you specify if you want go to read the + * buildable go files inside the package, but it fails if the package has no go files + */ +func packageWithName(name string) (pkg *build.Package, err error) { + pkg, err = build.Default.Import(name, ".", build.ImportMode(0)) + if err == nil { + return + } + + pkg, err = build.Default.Import(name, ".", build.ImportMode(1)) + return +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/test_finder.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/test_finder.go new file mode 100644 index 00000000000..b33595c9ae1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/test_finder.go @@ -0,0 +1,56 @@ +package convert + +import ( + "go/ast" + "regexp" +) + +/* + * Given a root node, walks its top level statements and returns + * points to function nodes to rewrite as It statements. + * These functions, according to Go testing convention, must be named + * TestWithCamelCasedName and receive a single *testing.T argument. + */ +func findTestFuncs(rootNode *ast.File) (testsToRewrite []*ast.FuncDecl) { + testNameRegexp := regexp.MustCompile("^Test[0-9A-Z].+") + + ast.Inspect(rootNode, func(node ast.Node) bool { + if node == nil { + return false + } + + switch node := node.(type) { + case *ast.FuncDecl: + matches := testNameRegexp.MatchString(node.Name.Name) + + if matches && receivesTestingT(node) { + testsToRewrite = append(testsToRewrite, node) + } + } + + return true + }) + + return +} + +/* + * convenience function that looks at args to a function and determines if its + * params include an argument of type *testing.T + */ +func receivesTestingT(node *ast.FuncDecl) bool { + if len(node.Type.Params.List) != 1 { + return false + } + + base, ok := node.Type.Params.List[0].Type.(*ast.StarExpr) + if !ok { + return false + } + + intermediate := base.X.(*ast.SelectorExpr) + isTestingPackage := intermediate.X.(*ast.Ident).Name == "testing" + isTestingT := intermediate.Sel.Name == "T" + + return isTestingPackage && isTestingT +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testfile_rewriter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testfile_rewriter.go new file mode 100644 index 00000000000..4b001a7dbb5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testfile_rewriter.go @@ -0,0 +1,163 @@ +package convert + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "os" +) + +/* + * Given a file path, rewrites any tests in the Ginkgo format. + * First, we parse the AST, and update the imports declaration. + * Then, we walk the first child elements in the file, returning tests to rewrite. + * A top level init func is declared, with a single Describe func inside. + * Then the test functions to rewrite are inserted as It statements inside the Describe. + * Finally we walk the rest of the file, replacing other usages of *testing.T + * Once that is complete, we write the AST back out again to its file. + */ +func rewriteTestsInFile(pathToFile string) { + fileSet := token.NewFileSet() + rootNode, err := parser.ParseFile(fileSet, pathToFile, nil, 0) + if err != nil { + panic(fmt.Sprintf("Error parsing test file '%s':\n%s\n", pathToFile, err.Error())) + } + + addGinkgoImports(rootNode) + removeTestingImport(rootNode) + + varUnderscoreBlock := createVarUnderscoreBlock() + describeBlock := createDescribeBlock() + varUnderscoreBlock.Values = []ast.Expr{describeBlock} + + for _, testFunc := range findTestFuncs(rootNode) { + rewriteTestFuncAsItStatement(testFunc, rootNode, describeBlock) + } + + underscoreDecl := &ast.GenDecl{ + Tok: 85, // gah, magick numbers are needed to make this work + TokPos: 14, // this tricks Go into writing "var _ = Describe" + Specs: []ast.Spec{varUnderscoreBlock}, + } + + imports := rootNode.Decls[0] + tail := rootNode.Decls[1:] + rootNode.Decls = append(append([]ast.Decl{imports}, underscoreDecl), tail...) + rewriteOtherFuncsToUseGinkgoT(rootNode.Decls) + walkNodesInRootNodeReplacingTestingT(rootNode) + + var buffer bytes.Buffer + if err = format.Node(&buffer, fileSet, rootNode); err != nil { + panic(fmt.Sprintf("Error formatting ast node after rewriting tests.\n%s\n", err.Error())) + } + + fileInfo, err := os.Stat(pathToFile) + if err != nil { + panic(fmt.Sprintf("Error stat'ing file: %s\n", pathToFile)) + } + + ioutil.WriteFile(pathToFile, buffer.Bytes(), fileInfo.Mode()) + return +} + +/* + * Given a test func named TestDoesSomethingNeat, rewrites it as + * It("does something neat", func() { __test_body_here__ }) and adds it + * to the Describe's list of statements + */ +func rewriteTestFuncAsItStatement(testFunc *ast.FuncDecl, rootNode *ast.File, describe *ast.CallExpr) { + var funcIndex int = -1 + for index, child := range rootNode.Decls { + if child == testFunc { + funcIndex = index + break + } + } + + if funcIndex < 0 { + panic(fmt.Sprintf("Assert failed: Error finding index for test node %s\n", testFunc.Name.Name)) + } + + var block *ast.BlockStmt = blockStatementFromDescribe(describe) + block.List = append(block.List, createItStatementForTestFunc(testFunc)) + replaceTestingTsWithGinkgoT(block, namedTestingTArg(testFunc)) + + // remove the old test func from the root node's declarations + rootNode.Decls = append(rootNode.Decls[:funcIndex], rootNode.Decls[funcIndex+1:]...) + return +} + +/* + * walks nodes inside of a test func's statements and replaces the usage of + * it's named *testing.T param with GinkgoT's + */ +func replaceTestingTsWithGinkgoT(statementsBlock *ast.BlockStmt, testingT string) { + ast.Inspect(statementsBlock, func(node ast.Node) bool { + if node == nil { + return false + } + + keyValueExpr, ok := node.(*ast.KeyValueExpr) + if ok { + replaceNamedTestingTsInKeyValueExpression(keyValueExpr, testingT) + return true + } + + funcLiteral, ok := node.(*ast.FuncLit) + if ok { + replaceTypeDeclTestingTsInFuncLiteral(funcLiteral) + return true + } + + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + replaceTestingTsInArgsLists(callExpr, testingT) + + funCall, ok := callExpr.Fun.(*ast.SelectorExpr) + if ok { + replaceTestingTsMethodCalls(funCall, testingT) + } + + return true + }) +} + +/* + * rewrite t.Fail() or any other *testing.T method by replacing with T().Fail() + * This function receives a selector expression (eg: t.Fail()) and + * the name of the *testing.T param from the function declaration. Rewrites the + * selector expression in place if the target was a *testing.T + */ +func replaceTestingTsMethodCalls(selectorExpr *ast.SelectorExpr, testingT string) { + ident, ok := selectorExpr.X.(*ast.Ident) + if !ok { + return + } + + if ident.Name == testingT { + selectorExpr.X = newGinkgoTFromIdent(ident) + } +} + +/* + * replaces usages of a named *testing.T param inside of a call expression + * with a new GinkgoT object + */ +func replaceTestingTsInArgsLists(callExpr *ast.CallExpr, testingT string) { + for index, arg := range callExpr.Args { + ident, ok := arg.(*ast.Ident) + if !ok { + continue + } + + if ident.Name == testingT { + callExpr.Args[index] = newGinkgoTFromIdent(ident) + } + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testing_t_rewriter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testing_t_rewriter.go new file mode 100644 index 00000000000..418cdc4e563 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert/testing_t_rewriter.go @@ -0,0 +1,130 @@ +package convert + +import ( + "go/ast" +) + +/* + * Rewrites any other top level funcs that receive a *testing.T param + */ +func rewriteOtherFuncsToUseGinkgoT(declarations []ast.Decl) { + for _, decl := range declarations { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + for _, param := range decl.Type.Params.List { + starExpr, ok := param.Type.(*ast.StarExpr) + if !ok { + continue + } + + selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) + if !ok { + continue + } + + xIdent, ok := selectorExpr.X.(*ast.Ident) + if !ok || xIdent.Name != "testing" { + continue + } + + if selectorExpr.Sel.Name != "T" { + continue + } + + param.Type = newGinkgoTInterface() + } + } +} + +/* + * Walks all of the nodes in the file, replacing *testing.T in struct + * and func literal nodes. eg: + * type foo struct { *testing.T } + * var bar = func(t *testing.T) { } + */ +func walkNodesInRootNodeReplacingTestingT(rootNode *ast.File) { + ast.Inspect(rootNode, func(node ast.Node) bool { + if node == nil { + return false + } + + switch node := node.(type) { + case *ast.StructType: + replaceTestingTsInStructType(node) + case *ast.FuncLit: + replaceTypeDeclTestingTsInFuncLiteral(node) + } + + return true + }) +} + +/* + * replaces named *testing.T inside a composite literal + */ +func replaceNamedTestingTsInKeyValueExpression(kve *ast.KeyValueExpr, testingT string) { + ident, ok := kve.Value.(*ast.Ident) + if !ok { + return + } + + if ident.Name == testingT { + kve.Value = newGinkgoTFromIdent(ident) + } +} + +/* + * replaces *testing.T params in a func literal with GinkgoT + */ +func replaceTypeDeclTestingTsInFuncLiteral(functionLiteral *ast.FuncLit) { + for _, arg := range functionLiteral.Type.Params.List { + starExpr, ok := arg.Type.(*ast.StarExpr) + if !ok { + continue + } + + selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) + if !ok { + continue + } + + target, ok := selectorExpr.X.(*ast.Ident) + if !ok { + continue + } + + if target.Name == "testing" && selectorExpr.Sel.Name == "T" { + arg.Type = newGinkgoTInterface() + } + } +} + +/* + * Replaces *testing.T types inside of a struct declaration with a GinkgoT + * eg: type foo struct { *testing.T } + */ +func replaceTestingTsInStructType(structType *ast.StructType) { + for _, field := range structType.Fields.List { + starExpr, ok := field.Type.(*ast.StarExpr) + if !ok { + continue + } + + selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) + if !ok { + continue + } + + xIdent, ok := selectorExpr.X.(*ast.Ident) + if !ok { + continue + } + + if xIdent.Name == "testing" && selectorExpr.Sel.Name == "T" { + field.Type = newGinkgoTInterface() + } + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert_command.go new file mode 100644 index 00000000000..89e60d39302 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/convert_command.go @@ -0,0 +1,44 @@ +package main + +import ( + "flag" + "fmt" + "github.com/onsi/ginkgo/ginkgo/convert" + "os" +) + +func BuildConvertCommand() *Command { + return &Command{ + Name: "convert", + FlagSet: flag.NewFlagSet("convert", flag.ExitOnError), + UsageCommand: "ginkgo convert /path/to/package", + Usage: []string{ + "Convert the package at the passed in path from an XUnit-style test to a Ginkgo-style test", + }, + Command: convertPackage, + } +} + +func convertPackage(args []string, additionalArgs []string) { + if len(args) != 1 { + println(fmt.Sprintf("usage: ginkgo convert /path/to/your/package")) + os.Exit(1) + } + + defer func() { + err := recover() + if err != nil { + switch err := err.(type) { + case error: + println(err.Error()) + case string: + println(err) + default: + println(fmt.Sprintf("unexpected error: %#v", err)) + } + os.Exit(1) + } + }() + + convert.RewritePackage(args[0]) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/generate_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/generate_command.go new file mode 100644 index 00000000000..6911d214c43 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/generate_command.go @@ -0,0 +1,166 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" +) + +func BuildGenerateCommand() *Command { + var agouti, noDot bool + flagSet := flag.NewFlagSet("generate", flag.ExitOnError) + flagSet.BoolVar(&agouti, "agouti", false, "If set, generate will generate a test file for writing Agouti tests") + flagSet.BoolVar(&noDot, "nodot", false, "If set, generate will generate a test file that does not . import ginkgo and gomega") + + return &Command{ + Name: "generate", + FlagSet: flagSet, + UsageCommand: "ginkgo generate ", + Usage: []string{ + "Generate a test file named filename_test.go", + "If the optional argument is omitted, a file named after the package in the current directory will be created.", + "Accepts the following flags:", + }, + Command: func(args []string, additionalArgs []string) { + generateSpec(args, agouti, noDot) + }, + } +} + +var specText = `package {{.Package}}_test + +import ( + . "{{.PackageImportPath}}" + + {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} + {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} +) + +var _ = Describe("{{.Subject}}", func() { + +}) +` + +var agoutiSpecText = `package {{.Package}}_test + +import ( + . "{{.PackageImportPath}}" + + {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} + {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} + . "github.com/sclevine/agouti/core" + . "github.com/sclevine/agouti/matchers" +) + +var _ = Describe("{{.Subject}}", func() { + var page Page + + BeforeEach(func() { + var err error + page, err = agoutiDriver.Page() + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + page.Destroy() + }) +}) +` + +type specData struct { + Package string + Subject string + PackageImportPath string + IncludeImports bool +} + +func generateSpec(args []string, agouti, noDot bool) { + if len(args) == 0 { + err := generateSpecForSubject("", agouti, noDot) + if err != nil { + fmt.Println(err.Error()) + fmt.Println("") + os.Exit(1) + } + fmt.Println("") + return + } + + var failed bool + for _, arg := range args { + err := generateSpecForSubject(arg, agouti, noDot) + if err != nil { + failed = true + fmt.Println(err.Error()) + } + } + fmt.Println("") + if failed { + os.Exit(1) + } +} + +func generateSpecForSubject(subject string, agouti, noDot bool) error { + packageName := getPackage() + if subject == "" { + subject = packageName + } else { + subject = strings.Split(subject, ".go")[0] + subject = strings.Split(subject, "_test")[0] + } + + formattedSubject := strings.Replace(strings.Title(strings.Replace(subject, "_", " ", -1)), " ", "", -1) + + data := specData{ + Package: packageName, + Subject: formattedSubject, + PackageImportPath: getPackageImportPath(), + IncludeImports: !noDot, + } + + targetFile := fmt.Sprintf("%s_test.go", subject) + if fileExists(targetFile) { + return fmt.Errorf("%s already exists.", targetFile) + } else { + fmt.Printf("Generating ginkgo test for %s in:\n %s\n", data.Subject, targetFile) + } + + f, err := os.Create(targetFile) + if err != nil { + return err + } + defer f.Close() + + var templateText string + if agouti { + templateText = agoutiSpecText + } else { + templateText = specText + } + + specTemplate, err := template.New("spec").Parse(templateText) + if err != nil { + return err + } + + specTemplate.Execute(f, data) + goFmt(targetFile) + return nil +} + +func getPackageImportPath() string { + workingDir, err := os.Getwd() + if err != nil { + panic(err.Error()) + } + sep := string(filepath.Separator) + paths := strings.Split(workingDir, sep+"src"+sep) + if len(paths) == 1 { + fmt.Printf("\nCouldn't identify package import path.\n\n\tginkgo generate\n\nMust be run within a package directory under $GOPATH/src/...\nYou're going to have to change UNKNOWN_PACKAGE_PATH in the generated file...\n\n") + return "UNKNOWN_PACKAGE_PATH" + } + return filepath.ToSlash(paths[len(paths)-1]) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/help_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/help_command.go new file mode 100644 index 00000000000..6f24d072b24 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/help_command.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" +) + +func BuildHelpCommand() *Command { + return &Command{ + Name: "help", + FlagSet: flag.NewFlagSet("help", flag.ExitOnError), + UsageCommand: "ginkgo help ", + Usage: []string{ + "Print usage information. If a command is passed in, print usage information just for that command.", + }, + Command: printHelp, + } +} + +func printHelp(args []string, additionalArgs []string) { + if len(args) == 0 { + usage() + } else { + command, found := commandMatching(args[0]) + if !found { + complainAndQuit(fmt.Sprintf("Unkown command: %s", args[0])) + } + + usageForCommand(command, true) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/interrupt_handler.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/interrupt_handler.go new file mode 100644 index 00000000000..81567a405ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/interrupt_handler.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "os/signal" + "sync" +) + +type InterruptHandler struct { + interruptCount int + lock *sync.Mutex + C chan bool +} + +func NewInterruptHandler() *InterruptHandler { + h := &InterruptHandler{ + lock: &sync.Mutex{}, + C: make(chan bool, 0), + } + + go h.handleInterrupt() + + return h +} + +func (h *InterruptHandler) WasInterrupted() bool { + h.lock.Lock() + defer h.lock.Unlock() + + return h.interruptCount > 0 +} + +func (h *InterruptHandler) handleInterrupt() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + <-c + signal.Stop(c) + + h.lock.Lock() + h.interruptCount++ + if h.interruptCount == 1 { + close(h.C) + } else if h.interruptCount > 5 { + os.Exit(1) + } + h.lock.Unlock() + + go h.handleInterrupt() +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/main.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/main.go new file mode 100644 index 00000000000..cf0cf35ee87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/main.go @@ -0,0 +1,291 @@ +/* +The Ginkgo CLI + +The Ginkgo CLI is fully documented [here](http://onsi.github.io/ginkgo/#the_ginkgo_cli) + +You can also learn more by running: + + ginkgo help + +Here are some of the more commonly used commands: + +To install: + + go install github.com/onsi/ginkgo/ginkgo + +To run tests: + + ginkgo + +To run tests in all subdirectories: + + ginkgo -r + +To run tests in particular packages: + + ginkgo /path/to/package /path/to/another/package + +To pass arguments/flags to your tests: + + ginkgo -- + +To run tests in parallel + + ginkgo -p + +this will automatically detect the optimal number of nodes to use. Alternatively, you can specify the number of nodes with: + + ginkgo -nodes=N + +(note that you don't need to provide -p in this case). + +By default the Ginkgo CLI will spin up a server that the individual test processes send test output to. The CLI aggregates this output and then presents coherent test output, one test at a time, as each test completes. +An alternative is to have the parallel nodes run and stream interleaved output back. This useful for debugging, particularly in contexts where tests hang/fail to start. To get this interleaved output: + + ginkgo -nodes=N -stream=true + +On windows, the default value for stream is true. + +By default, when running multiple tests (with -r or a list of packages) Ginkgo will abort when a test fails. To have Ginkgo run subsequent test suites instead you can: + + ginkgo -keepGoing + +To monitor packages and rerun tests when changes occur: + + ginkgo watch <-r> + +passing `ginkgo watch` the `-r` flag will recursively detect all test suites under the current directory and monitor them. +`watch` does not detect *new* packages. Moreover, changes in package X only rerun the tests for package X, tests for packages +that depend on X are not rerun. + +[OSX only] To receive (desktop) notifications when a test run completes: + + ginkgo -notify + +this is particularly useful with `ginkgo watch`. Notifications are currently only supported on OS X and require that you `brew install terminal-notifier` + +Sometimes (to suss out race conditions/flakey tests, for example) you want to keep running a test suite until it fails. You can do this with: + + ginkgo -untilItFails + +To bootstrap a test suite: + + ginkgo bootstrap + +To generate a test file: + + ginkgo generate + +To bootstrap/generate test files without using "." imports: + + ginkgo bootstrap --nodot + ginkgo generate --nodot + +this will explicitly export all the identifiers in Ginkgo and Gomega allowing you to rename them to avoid collisions. When you pull to the latest Ginkgo/Gomega you'll want to run + + ginkgo nodot + +to refresh this list and pull in any new identifiers. In particular, this will pull in any new Gomega matchers that get added. + +To convert an existing XUnit style test suite to a Ginkgo-style test suite: + + ginkgo convert . + +To unfocus tests: + + ginkgo unfocus + +or + + ginkgo blur + +To compile a test suite: + + ginkgo build + +will output an executable file named `package.test`. This can be run directly or by invoking + + ginkgo + +To print out Ginkgo's version: + + ginkgo version + +To get more help: + + ginkgo help +*/ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/testsuite" +) + +const greenColor = "\x1b[32m" +const redColor = "\x1b[91m" +const defaultStyle = "\x1b[0m" +const lightGrayColor = "\x1b[37m" + +type Command struct { + Name string + AltName string + FlagSet *flag.FlagSet + Usage []string + UsageCommand string + Command func(args []string, additionalArgs []string) + SuppressFlagDocumentation bool + FlagDocSubstitute []string +} + +func (c *Command) Matches(name string) bool { + return c.Name == name || (c.AltName != "" && c.AltName == name) +} + +func (c *Command) Run(args []string, additionalArgs []string) { + c.FlagSet.Parse(args) + c.Command(c.FlagSet.Args(), additionalArgs) +} + +var DefaultCommand *Command +var Commands []*Command + +func init() { + DefaultCommand = BuildRunCommand() + Commands = append(Commands, BuildWatchCommand()) + Commands = append(Commands, BuildBuildCommand()) + Commands = append(Commands, BuildBootstrapCommand()) + Commands = append(Commands, BuildGenerateCommand()) + Commands = append(Commands, BuildNodotCommand()) + Commands = append(Commands, BuildConvertCommand()) + Commands = append(Commands, BuildUnfocusCommand()) + Commands = append(Commands, BuildVersionCommand()) + Commands = append(Commands, BuildHelpCommand()) +} + +func main() { + args := []string{} + additionalArgs := []string{} + + foundDelimiter := false + + for _, arg := range os.Args[1:] { + if !foundDelimiter { + if arg == "--" { + foundDelimiter = true + continue + } + } + + if foundDelimiter { + additionalArgs = append(additionalArgs, arg) + } else { + args = append(args, arg) + } + } + + if len(args) > 0 { + commandToRun, found := commandMatching(args[0]) + if found { + commandToRun.Run(args[1:], additionalArgs) + return + } + } + + DefaultCommand.Run(args, additionalArgs) +} + +func commandMatching(name string) (*Command, bool) { + for _, command := range Commands { + if command.Matches(name) { + return command, true + } + } + return nil, false +} + +func usage() { + fmt.Fprintf(os.Stderr, "Ginkgo Version %s\n\n", config.VERSION) + usageForCommand(DefaultCommand, false) + for _, command := range Commands { + fmt.Fprintf(os.Stderr, "\n") + usageForCommand(command, false) + } +} + +func usageForCommand(command *Command, longForm bool) { + fmt.Fprintf(os.Stderr, "%s\n%s\n", command.UsageCommand, strings.Repeat("-", len(command.UsageCommand))) + fmt.Fprintf(os.Stderr, "%s\n", strings.Join(command.Usage, "\n")) + if command.SuppressFlagDocumentation && !longForm { + fmt.Fprintf(os.Stderr, "%s\n", strings.Join(command.FlagDocSubstitute, "\n ")) + } else { + command.FlagSet.PrintDefaults() + } +} + +func complainAndQuit(complaint string) { + fmt.Fprintf(os.Stderr, "%s\nFor usage instructions:\n\tginkgo help\n", complaint) + os.Exit(1) +} + +func findSuites(args []string, recurse bool, skipPackage string, allowPrecompiled bool) ([]testsuite.TestSuite, []string) { + suites := []testsuite.TestSuite{} + + if len(args) > 0 { + for _, arg := range args { + if allowPrecompiled { + suite, err := testsuite.PrecompiledTestSuite(arg) + if err == nil { + suites = append(suites, suite) + continue + } + } + suites = append(suites, testsuite.SuitesInDir(arg, recurse)...) + } + } else { + suites = testsuite.SuitesInDir(".", recurse) + } + + skippedPackages := []string{} + if skipPackage != "" { + skipFilters := strings.Split(skipPackage, ",") + filteredSuites := []testsuite.TestSuite{} + for _, suite := range suites { + skip := false + for _, skipFilter := range skipFilters { + if strings.Contains(suite.Path, skipFilter) { + skip = true + break + } + } + if skip { + skippedPackages = append(skippedPackages, suite.Path) + } else { + filteredSuites = append(filteredSuites, suite) + } + } + suites = filteredSuites + } + + return suites, skippedPackages +} + +func goFmt(path string) { + err := exec.Command("go", "fmt", path).Run() + if err != nil { + complainAndQuit("Could not fmt: " + err.Error()) + } +} + +func pluralizedWord(singular, plural string, count int) string { + if count == 1 { + return singular + } + return plural +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot.go new file mode 100644 index 00000000000..3f7237c602d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot.go @@ -0,0 +1,194 @@ +package nodot + +import ( + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/token" + "path/filepath" + "strings" +) + +func ApplyNoDot(data []byte) ([]byte, error) { + sections, err := generateNodotSections() + if err != nil { + return nil, err + } + + for _, section := range sections { + data = section.createOrUpdateIn(data) + } + + return data, nil +} + +type nodotSection struct { + name string + pkg string + declarations []string + types []string +} + +func (s nodotSection) createOrUpdateIn(data []byte) []byte { + renames := map[string]string{} + + contents := string(data) + + lines := strings.Split(contents, "\n") + + comment := "// Declarations for " + s.name + + newLines := []string{} + for _, line := range lines { + if line == comment { + continue + } + + words := strings.Split(line, " ") + lastWord := words[len(words)-1] + + if s.containsDeclarationOrType(lastWord) { + renames[lastWord] = words[1] + continue + } + + newLines = append(newLines, line) + } + + if len(newLines[len(newLines)-1]) > 0 { + newLines = append(newLines, "") + } + + newLines = append(newLines, comment) + + for _, typ := range s.types { + name, ok := renames[s.prefix(typ)] + if !ok { + name = typ + } + newLines = append(newLines, fmt.Sprintf("type %s %s", name, s.prefix(typ))) + } + + for _, decl := range s.declarations { + name, ok := renames[s.prefix(decl)] + if !ok { + name = decl + } + newLines = append(newLines, fmt.Sprintf("var %s = %s", name, s.prefix(decl))) + } + + newLines = append(newLines, "") + + newContents := strings.Join(newLines, "\n") + + return []byte(newContents) +} + +func (s nodotSection) prefix(declOrType string) string { + return s.pkg + "." + declOrType +} + +func (s nodotSection) containsDeclarationOrType(word string) bool { + for _, declaration := range s.declarations { + if s.prefix(declaration) == word { + return true + } + } + + for _, typ := range s.types { + if s.prefix(typ) == word { + return true + } + } + + return false +} + +func generateNodotSections() ([]nodotSection, error) { + sections := []nodotSection{} + + declarations, err := getExportedDeclerationsForPackage("github.com/onsi/ginkgo", "ginkgo_dsl.go", "GINKGO_VERSION", "GINKGO_PANIC") + if err != nil { + return nil, err + } + sections = append(sections, nodotSection{ + name: "Ginkgo DSL", + pkg: "ginkgo", + declarations: declarations, + types: []string{"Done", "Benchmarker"}, + }) + + declarations, err = getExportedDeclerationsForPackage("github.com/onsi/gomega", "gomega_dsl.go", "GOMEGA_VERSION") + if err != nil { + return nil, err + } + sections = append(sections, nodotSection{ + name: "Gomega DSL", + pkg: "gomega", + declarations: declarations, + }) + + declarations, err = getExportedDeclerationsForPackage("github.com/onsi/gomega", "matchers.go") + if err != nil { + return nil, err + } + sections = append(sections, nodotSection{ + name: "Gomega Matchers", + pkg: "gomega", + declarations: declarations, + }) + + return sections, nil +} + +func getExportedDeclerationsForPackage(pkgPath string, filename string, blacklist ...string) ([]string, error) { + pkg, err := build.Import(pkgPath, ".", 0) + if err != nil { + return []string{}, err + } + + declarations, err := getExportedDeclarationsForFile(filepath.Join(pkg.Dir, filename)) + if err != nil { + return []string{}, err + } + + blacklistLookup := map[string]bool{} + for _, declaration := range blacklist { + blacklistLookup[declaration] = true + } + + filteredDeclarations := []string{} + for _, declaration := range declarations { + if blacklistLookup[declaration] { + continue + } + filteredDeclarations = append(filteredDeclarations, declaration) + } + + return filteredDeclarations, nil +} + +func getExportedDeclarationsForFile(path string) ([]string, error) { + fset := token.NewFileSet() + tree, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return []string{}, err + } + + declarations := []string{} + ast.FileExports(tree) + for _, decl := range tree.Decls { + switch x := decl.(type) { + case *ast.GenDecl: + switch s := x.Specs[0].(type) { + case *ast.ValueSpec: + declarations = append(declarations, s.Names[0].Name) + } + case *ast.FuncDecl: + declarations = append(declarations, x.Name.Name) + } + } + + return declarations, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_suite_test.go new file mode 100644 index 00000000000..ca4613e6f57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_suite_test.go @@ -0,0 +1,91 @@ +package nodot_test + +import ( + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + + "testing" +) + +func TestNodot(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nodot Suite") +} + +// Declarations for Ginkgo DSL +type Done ginkgo.Done +type Benchmarker ginkgo.Benchmarker + +var GinkgoWriter = ginkgo.GinkgoWriter +var GinkgoParallelNode = ginkgo.GinkgoParallelNode +var GinkgoT = ginkgo.GinkgoT +var CurrentGinkgoTestDescription = ginkgo.CurrentGinkgoTestDescription +var RunSpecs = ginkgo.RunSpecs +var RunSpecsWithDefaultAndCustomReporters = ginkgo.RunSpecsWithDefaultAndCustomReporters +var RunSpecsWithCustomReporters = ginkgo.RunSpecsWithCustomReporters +var Fail = ginkgo.Fail +var GinkgoRecover = ginkgo.GinkgoRecover +var Describe = ginkgo.Describe +var FDescribe = ginkgo.FDescribe +var PDescribe = ginkgo.PDescribe +var XDescribe = ginkgo.XDescribe +var Context = ginkgo.Context +var FContext = ginkgo.FContext +var PContext = ginkgo.PContext +var XContext = ginkgo.XContext +var It = ginkgo.It +var FIt = ginkgo.FIt +var PIt = ginkgo.PIt +var XIt = ginkgo.XIt +var Measure = ginkgo.Measure +var FMeasure = ginkgo.FMeasure +var PMeasure = ginkgo.PMeasure +var XMeasure = ginkgo.XMeasure +var BeforeSuite = ginkgo.BeforeSuite +var AfterSuite = ginkgo.AfterSuite +var SynchronizedBeforeSuite = ginkgo.SynchronizedBeforeSuite +var SynchronizedAfterSuite = ginkgo.SynchronizedAfterSuite +var BeforeEach = ginkgo.BeforeEach +var JustBeforeEach = ginkgo.JustBeforeEach +var AfterEach = ginkgo.AfterEach + +// Declarations for Gomega DSL +var RegisterFailHandler = gomega.RegisterFailHandler +var RegisterTestingT = gomega.RegisterTestingT +var InterceptGomegaFailures = gomega.InterceptGomegaFailures +var Ω = gomega.Ω +var Expect = gomega.Expect +var ExpectWithOffset = gomega.ExpectWithOffset +var Eventually = gomega.Eventually +var EventuallyWithOffset = gomega.EventuallyWithOffset +var Consistently = gomega.Consistently +var ConsistentlyWithOffset = gomega.ConsistentlyWithOffset +var SetDefaultEventuallyTimeout = gomega.SetDefaultEventuallyTimeout +var SetDefaultEventuallyPollingInterval = gomega.SetDefaultEventuallyPollingInterval +var SetDefaultConsistentlyDuration = gomega.SetDefaultConsistentlyDuration +var SetDefaultConsistentlyPollingInterval = gomega.SetDefaultConsistentlyPollingInterval + +// Declarations for Gomega Matchers +var Equal = gomega.Equal +var BeEquivalentTo = gomega.BeEquivalentTo +var BeNil = gomega.BeNil +var BeTrue = gomega.BeTrue +var BeFalse = gomega.BeFalse +var HaveOccurred = gomega.HaveOccurred +var MatchError = gomega.MatchError +var BeClosed = gomega.BeClosed +var Receive = gomega.Receive +var MatchRegexp = gomega.MatchRegexp +var ContainSubstring = gomega.ContainSubstring +var MatchJSON = gomega.MatchJSON +var BeEmpty = gomega.BeEmpty +var HaveLen = gomega.HaveLen +var BeZero = gomega.BeZero +var ContainElement = gomega.ContainElement +var ConsistOf = gomega.ConsistOf +var HaveKey = gomega.HaveKey +var HaveKeyWithValue = gomega.HaveKeyWithValue +var BeNumerically = gomega.BeNumerically +var BeTemporally = gomega.BeTemporally +var BeAssignableToTypeOf = gomega.BeAssignableToTypeOf +var Panic = gomega.Panic diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_test.go new file mode 100644 index 00000000000..37260a89f20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot/nodot_test.go @@ -0,0 +1,81 @@ +package nodot_test + +import ( + . "github.com/onsi/ginkgo/ginkgo/nodot" + "strings" +) + +var _ = Describe("ApplyNoDot", func() { + var result string + + apply := func(input string) string { + output, err := ApplyNoDot([]byte(input)) + Ω(err).ShouldNot(HaveOccurred()) + return string(output) + } + + Context("when no declarations have been imported yet", func() { + BeforeEach(func() { + result = apply("") + }) + + It("should add headings for the various declarations", func() { + Ω(result).Should(ContainSubstring("// Declarations for Ginkgo DSL")) + Ω(result).Should(ContainSubstring("// Declarations for Gomega DSL")) + Ω(result).Should(ContainSubstring("// Declarations for Gomega Matchers")) + }) + + It("should import Ginkgo's declarations", func() { + Ω(result).Should(ContainSubstring("var It = ginkgo.It")) + Ω(result).Should(ContainSubstring("var XDescribe = ginkgo.XDescribe")) + }) + + It("should import Ginkgo's types", func() { + Ω(result).Should(ContainSubstring("type Done ginkgo.Done")) + Ω(result).Should(ContainSubstring("type Benchmarker ginkgo.Benchmarker")) + Ω(strings.Count(result, "type ")).Should(Equal(2)) + }) + + It("should import Gomega's DSL and matchers", func() { + Ω(result).Should(ContainSubstring("var Ω = gomega.Ω")) + Ω(result).Should(ContainSubstring("var ContainSubstring = gomega.ContainSubstring")) + Ω(result).Should(ContainSubstring("var Equal = gomega.Equal")) + }) + + It("should not import blacklisted things", func() { + Ω(result).ShouldNot(ContainSubstring("GINKGO_VERSION")) + Ω(result).ShouldNot(ContainSubstring("GINKGO_PANIC")) + Ω(result).ShouldNot(ContainSubstring("GOMEGA_VERSION")) + }) + }) + + It("should be idempotent (module empty lines - go fmt can fix those for us)", func() { + first := apply("") + second := apply(first) + first = strings.Trim(first, "\n") + second = strings.Trim(second, "\n") + Ω(first).Should(Equal(second)) + }) + + It("should not mess with other things in the input", func() { + result = apply("var MyThing = SomethingThatsMine") + Ω(result).Should(ContainSubstring("var MyThing = SomethingThatsMine")) + }) + + Context("when the user has redefined a name", func() { + It("should honor the redefinition", func() { + result = apply(` +var _ = gomega.Ω +var When = ginkgo.It + `) + + Ω(result).Should(ContainSubstring("var _ = gomega.Ω")) + Ω(result).ShouldNot(ContainSubstring("var Ω = gomega.Ω")) + + Ω(result).Should(ContainSubstring("var When = ginkgo.It")) + Ω(result).ShouldNot(ContainSubstring("var It = ginkgo.It")) + + Ω(result).Should(ContainSubstring("var Context = ginkgo.Context")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot_command.go new file mode 100644 index 00000000000..e1a2e13099a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/nodot_command.go @@ -0,0 +1,74 @@ +package main + +import ( + "bufio" + "flag" + "github.com/onsi/ginkgo/ginkgo/nodot" + "io/ioutil" + "os" + "path/filepath" + "regexp" +) + +func BuildNodotCommand() *Command { + return &Command{ + Name: "nodot", + FlagSet: flag.NewFlagSet("bootstrap", flag.ExitOnError), + UsageCommand: "ginkgo nodot", + Usage: []string{ + "Update the nodot declarations in your test suite", + "Any missing declarations (from, say, a recently added matcher) will be added to your bootstrap file.", + "If you've renamed a declaration, that name will be honored and not overwritten.", + }, + Command: updateNodot, + } +} + +func updateNodot(args []string, additionalArgs []string) { + suiteFile, perm := findSuiteFile() + + data, err := ioutil.ReadFile(suiteFile) + if err != nil { + complainAndQuit("Failed to update nodot declarations: " + err.Error()) + } + + content, err := nodot.ApplyNoDot(data) + if err != nil { + complainAndQuit("Failed to update nodot declarations: " + err.Error()) + } + ioutil.WriteFile(suiteFile, content, perm) + + goFmt(suiteFile) +} + +func findSuiteFile() (string, os.FileMode) { + workingDir, err := os.Getwd() + if err != nil { + complainAndQuit("Could not find suite file for nodot: " + err.Error()) + } + + files, err := ioutil.ReadDir(workingDir) + if err != nil { + complainAndQuit("Could not find suite file for nodot: " + err.Error()) + } + + re := regexp.MustCompile(`RunSpecs\(|RunSpecsWithDefaultAndCustomReporters\(|RunSpecsWithCustomReporters\(`) + + for _, file := range files { + if file.IsDir() { + continue + } + path := filepath.Join(workingDir, file.Name()) + f, err := os.Open(path) + if err != nil { + complainAndQuit("Could not find suite file for nodot: " + err.Error()) + } + if re.MatchReader(bufio.NewReader(f)) { + return path, file.Mode() + } + } + + complainAndQuit("Could not find a suite file for nodot: you need a bootstrap file that call's Ginkgo's RunSpecs() command.\nTry running ginkgo bootstrap first.") + + return "", 0 +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/notifications.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/notifications.go new file mode 100644 index 00000000000..642f12cf643 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/notifications.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "github.com/onsi/ginkgo/ginkgo/testsuite" +) + +type Notifier struct { + commandFlags *RunWatchAndBuildCommandFlags +} + +func NewNotifier(commandFlags *RunWatchAndBuildCommandFlags) *Notifier { + return &Notifier{ + commandFlags: commandFlags, + } +} + +func (n *Notifier) VerifyNotificationsAreAvailable() { + if n.commandFlags.Notify { + _, err := exec.LookPath("terminal-notifier") + if err != nil { + fmt.Printf(`--notify requires terminal-notifier, which you don't seem to have installed. + +To remedy this: + + brew install terminal-notifier + +To learn more about terminal-notifier: + + https://github.com/alloy/terminal-notifier +`) + os.Exit(1) + } + } +} + +func (n *Notifier) SendSuiteCompletionNotification(suite testsuite.TestSuite, suitePassed bool) { + if suitePassed { + n.SendNotification("Ginkgo [PASS]", fmt.Sprintf(`Test suite for "%s" passed.`, suite.PackageName)) + } else { + n.SendNotification("Ginkgo [FAIL]", fmt.Sprintf(`Test suite for "%s" failed.`, suite.PackageName)) + } +} + +func (n *Notifier) SendNotification(title string, subtitle string) { + args := []string{"-title", title, "-subtitle", subtitle, "-group", "com.onsi.ginkgo"} + + terminal := os.Getenv("TERM_PROGRAM") + if terminal == "iTerm.app" { + args = append(args, "-activate", "com.googlecode.iterm2") + } else if terminal == "Apple_Terminal" { + args = append(args, "-activate", "com.apple.Terminal") + } + + if n.commandFlags.Notify { + exec.Command("terminal-notifier", args...).Run() + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_command.go new file mode 100644 index 00000000000..de653423a30 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_command.go @@ -0,0 +1,191 @@ +package main + +import ( + "flag" + "fmt" + "math/rand" + "os" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/testrunner" + "github.com/onsi/ginkgo/types" +) + +func BuildRunCommand() *Command { + commandFlags := NewRunCommandFlags(flag.NewFlagSet("ginkgo", flag.ExitOnError)) + notifier := NewNotifier(commandFlags) + interruptHandler := NewInterruptHandler() + runner := &SpecRunner{ + commandFlags: commandFlags, + notifier: notifier, + interruptHandler: interruptHandler, + suiteRunner: NewSuiteRunner(notifier, interruptHandler), + } + + return &Command{ + Name: "", + FlagSet: commandFlags.FlagSet, + UsageCommand: "ginkgo -- ", + Usage: []string{ + "Run the tests in the passed in (or the package in the current directory if left blank).", + "Any arguments after -- will be passed to the test.", + "Accepts the following flags:", + }, + Command: runner.RunSpecs, + } +} + +type SpecRunner struct { + commandFlags *RunWatchAndBuildCommandFlags + notifier *Notifier + interruptHandler *InterruptHandler + suiteRunner *SuiteRunner +} + +func (r *SpecRunner) RunSpecs(args []string, additionalArgs []string) { + r.commandFlags.computeNodes() + r.notifier.VerifyNotificationsAreAvailable() + + suites, skippedPackages := findSuites(args, r.commandFlags.Recurse, r.commandFlags.SkipPackage, true) + if len(skippedPackages) > 0 { + fmt.Println("Will skip:") + for _, skippedPackage := range skippedPackages { + fmt.Println(" " + skippedPackage) + } + } + + if len(skippedPackages) > 0 && len(suites) == 0 { + fmt.Println("All tests skipped! Exiting...") + os.Exit(0) + } + + if len(suites) == 0 { + complainAndQuit("Found no test suites") + } + + r.ComputeSuccinctMode(len(suites)) + + t := time.Now() + + runners := []*testrunner.TestRunner{} + for _, suite := range suites { + runners = append(runners, testrunner.New(suite, r.commandFlags.NumCPU, r.commandFlags.ParallelStream, r.commandFlags.Race, r.commandFlags.Cover, r.commandFlags.Tags, additionalArgs)) + } + + numSuites := 0 + runResult := testrunner.PassingRunResult() + if r.commandFlags.UntilItFails { + iteration := 0 + for { + r.UpdateSeed() + randomizedRunners := r.randomizeOrder(runners) + runResult, numSuites = r.suiteRunner.RunSuites(randomizedRunners, r.commandFlags.NumCompilers, r.commandFlags.KeepGoing, nil) + iteration++ + + if r.interruptHandler.WasInterrupted() { + break + } + + if runResult.Passed { + fmt.Printf("\nAll tests passed...\nWill keep running them until they fail.\nThis was attempt #%d\n%s\n", iteration, orcMessage(iteration)) + } else { + fmt.Printf("\nTests failed on attempt #%d\n\n", iteration) + break + } + } + } else { + randomizedRunners := r.randomizeOrder(runners) + runResult, numSuites = r.suiteRunner.RunSuites(randomizedRunners, r.commandFlags.NumCompilers, r.commandFlags.KeepGoing, nil) + } + + for _, runner := range runners { + runner.CleanUp() + } + + fmt.Printf("\nGinkgo ran %d %s in %s\n", numSuites, pluralizedWord("suite", "suites", numSuites), time.Since(t)) + + if runResult.Passed { + if runResult.HasProgrammaticFocus { + fmt.Printf("Test Suite Passed\n") + fmt.Printf("Detected Programmatic Focus - setting exit status to %d\n", types.GINKGO_FOCUS_EXIT_CODE) + os.Exit(types.GINKGO_FOCUS_EXIT_CODE) + } else { + fmt.Printf("Test Suite Passed\n") + os.Exit(0) + } + } else { + fmt.Printf("Test Suite Failed\n") + os.Exit(1) + } +} + +func (r *SpecRunner) ComputeSuccinctMode(numSuites int) { + if config.DefaultReporterConfig.Verbose { + config.DefaultReporterConfig.Succinct = false + return + } + + if numSuites == 1 { + return + } + + if numSuites > 1 && !r.commandFlags.wasSet("succinct") { + config.DefaultReporterConfig.Succinct = true + } +} + +func (r *SpecRunner) UpdateSeed() { + if !r.commandFlags.wasSet("seed") { + config.GinkgoConfig.RandomSeed = time.Now().Unix() + } +} + +func (r *SpecRunner) randomizeOrder(runners []*testrunner.TestRunner) []*testrunner.TestRunner { + if !r.commandFlags.RandomizeSuites { + return runners + } + + if len(runners) <= 1 { + return runners + } + + randomizedRunners := make([]*testrunner.TestRunner, len(runners)) + randomizer := rand.New(rand.NewSource(config.GinkgoConfig.RandomSeed)) + permutation := randomizer.Perm(len(runners)) + for i, j := range permutation { + randomizedRunners[i] = runners[j] + } + return randomizedRunners +} + +func orcMessage(iteration int) string { + if iteration < 10 { + return "" + } else if iteration < 30 { + return []string{ + "If at first you succeed...", + "...try, try again.", + "Looking good!", + "Still good...", + "I think your tests are fine....", + "Yep, still passing", + "Here we go again...", + "Even the gophers are getting bored", + "Did you try -race?", + "Maybe you should stop now?", + "I'm getting tired...", + "What if I just made you a sandwich?", + "Hit ^C, hit ^C, please hit ^C", + "Make it stop. Please!", + "Come on! Enough is enough!", + "Dave, this conversation can serve no purpose anymore. Goodbye.", + "Just what do you think you're doing, Dave? ", + "I, Sisyphus", + "Insanity: doing the same thing over and over again and expecting different results. -Einstein", + "I guess Einstein never tried to churn butter", + }[iteration-10] + "\n" + } else { + return "No, seriously... you can probably stop now.\n" + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_watch_and_build_command_flags.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_watch_and_build_command_flags.go new file mode 100644 index 00000000000..e0357c33010 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/run_watch_and_build_command_flags.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + "runtime" + + "github.com/onsi/ginkgo/config" +) + +type RunWatchAndBuildCommandFlags struct { + Recurse bool + Race bool + Cover bool + SkipPackage string + Tags string + + //for run and watch commands + NumCPU int + NumCompilers int + ParallelStream bool + Notify bool + AutoNodes bool + + //only for run command + KeepGoing bool + UntilItFails bool + RandomizeSuites bool + + //only for watch command + Depth int + + FlagSet *flag.FlagSet +} + +const runMode = 1 +const watchMode = 2 +const buildMode = 3 + +func NewRunCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { + c := &RunWatchAndBuildCommandFlags{ + FlagSet: flagSet, + } + c.flags(runMode) + return c +} + +func NewWatchCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { + c := &RunWatchAndBuildCommandFlags{ + FlagSet: flagSet, + } + c.flags(watchMode) + return c +} + +func NewBuildCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { + c := &RunWatchAndBuildCommandFlags{ + FlagSet: flagSet, + } + c.flags(buildMode) + return c +} + +func (c *RunWatchAndBuildCommandFlags) wasSet(flagName string) bool { + wasSet := false + c.FlagSet.Visit(func(f *flag.Flag) { + if f.Name == flagName { + wasSet = true + } + }) + + return wasSet +} + +func (c *RunWatchAndBuildCommandFlags) computeNodes() { + if c.wasSet("nodes") { + return + } + if c.AutoNodes { + switch n := runtime.NumCPU(); { + case n <= 4: + c.NumCPU = n + default: + c.NumCPU = n - 1 + } + } +} + +func (c *RunWatchAndBuildCommandFlags) flags(mode int) { + onWindows := (runtime.GOOS == "windows") + onOSX := (runtime.GOOS == "darwin") + + c.FlagSet.BoolVar(&(c.Recurse), "r", false, "Find and run test suites under the current directory recursively") + c.FlagSet.BoolVar(&(c.Race), "race", false, "Run tests with race detection enabled") + c.FlagSet.BoolVar(&(c.Cover), "cover", false, "Run tests with coverage analysis, will generate coverage profiles with the package name in the current directory") + c.FlagSet.StringVar(&(c.SkipPackage), "skipPackage", "", "A comma-separated list of package names to be skipped. If any part of the package's path matches, that package is ignored.") + c.FlagSet.StringVar(&(c.Tags), "tags", "", "A list of build tags to consider satisfied during the build") + + if mode == runMode || mode == watchMode { + config.Flags(c.FlagSet, "", false) + c.FlagSet.IntVar(&(c.NumCPU), "nodes", 1, "The number of parallel test nodes to run") + c.FlagSet.IntVar(&(c.NumCompilers), "compilers", 0, "The number of concurrent compilations to run (0 will autodetect)") + c.FlagSet.BoolVar(&(c.AutoNodes), "p", false, "Run in parallel with auto-detected number of nodes") + c.FlagSet.BoolVar(&(c.ParallelStream), "stream", onWindows, "stream parallel test output in real time: less coherent, but useful for debugging") + if onOSX { + c.FlagSet.BoolVar(&(c.Notify), "notify", false, "Send desktop notifications when a test run completes") + } + } + + if mode == runMode { + c.FlagSet.BoolVar(&(c.KeepGoing), "keepGoing", false, "When true, failures from earlier test suites do not prevent later test suites from running") + c.FlagSet.BoolVar(&(c.UntilItFails), "untilItFails", false, "When true, Ginkgo will keep rerunning tests until a failure occurs") + c.FlagSet.BoolVar(&(c.RandomizeSuites), "randomizeSuites", false, "When true, Ginkgo will randomize the order in which test suites run") + } + + if mode == watchMode { + c.FlagSet.IntVar(&(c.Depth), "depth", 1, "Ginkgo will watch dependencies down to this depth in the dependency tree") + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/suite_runner.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/suite_runner.go new file mode 100644 index 00000000000..194573d95f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/suite_runner.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "runtime" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/testrunner" + "github.com/onsi/ginkgo/ginkgo/testsuite" +) + +type SuiteRunner struct { + notifier *Notifier + interruptHandler *InterruptHandler +} + +type compiler struct { + runner *testrunner.TestRunner + compilationError chan error +} + +func (c *compiler) compile() { + retries := 0 + + err := c.runner.Compile() + for err != nil && retries < 5 { //We retry because Go sometimes steps on itself when multiple compiles happen in parallel. This is ugly, but should help resolve flakiness... + err = c.runner.Compile() + retries++ + } + + c.compilationError <- err +} + +func NewSuiteRunner(notifier *Notifier, interruptHandler *InterruptHandler) *SuiteRunner { + return &SuiteRunner{ + notifier: notifier, + interruptHandler: interruptHandler, + } +} + +func (r *SuiteRunner) RunSuites(runners []*testrunner.TestRunner, numCompilers int, keepGoing bool, willCompile func(suite testsuite.TestSuite)) (testrunner.RunResult, int) { + runResult := testrunner.PassingRunResult() + + compilers := make([]*compiler, len(runners)) + for i, runner := range runners { + compilers[i] = &compiler{ + runner: runner, + compilationError: make(chan error, 1), + } + } + + compilerChannel := make(chan *compiler) + if numCompilers == 0 { + numCompilers = runtime.NumCPU() + } + for i := 0; i < numCompilers; i++ { + go func() { + for compiler := range compilerChannel { + if willCompile != nil { + willCompile(compiler.runner.Suite) + } + compiler.compile() + } + }() + } + go func() { + for _, compiler := range compilers { + compilerChannel <- compiler + } + close(compilerChannel) + }() + + numSuitesThatRan := 0 + suitesThatFailed := []testsuite.TestSuite{} + for i, runner := range runners { + if r.interruptHandler.WasInterrupted() { + break + } + + compilationError := <-compilers[i].compilationError + if compilationError != nil { + fmt.Print(compilationError.Error()) + } + numSuitesThatRan++ + suiteRunResult := testrunner.FailingRunResult() + if compilationError == nil { + suiteRunResult = compilers[i].runner.Run() + } + r.notifier.SendSuiteCompletionNotification(runner.Suite, suiteRunResult.Passed) + runResult = runResult.Merge(suiteRunResult) + if !suiteRunResult.Passed { + suitesThatFailed = append(suitesThatFailed, runner.Suite) + if !keepGoing { + break + } + } + if i < len(runners)-1 && !config.DefaultReporterConfig.Succinct { + fmt.Println("") + } + } + + if keepGoing && !runResult.Passed { + r.listFailedSuites(suitesThatFailed) + } + + return runResult, numSuitesThatRan +} + +func (r *SuiteRunner) listFailedSuites(suitesThatFailed []testsuite.TestSuite) { + fmt.Println("") + fmt.Println("There were failures detected in the following suites:") + + maxPackageNameLength := 0 + for _, suite := range suitesThatFailed { + if len(suite.PackageName) > maxPackageNameLength { + maxPackageNameLength = len(suite.PackageName) + } + } + + packageNameFormatter := fmt.Sprintf("%%%ds", maxPackageNameLength) + + for _, suite := range suitesThatFailed { + if config.DefaultReporterConfig.NoColor { + fmt.Printf("\t"+packageNameFormatter+" %s\n", suite.PackageName, suite.Path) + } else { + fmt.Printf("\t%s"+packageNameFormatter+"%s %s%s%s\n", redColor, suite.PackageName, defaultStyle, lightGrayColor, suite.Path, defaultStyle) + } + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/log_writer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/log_writer.go new file mode 100644 index 00000000000..a73a6e37919 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/log_writer.go @@ -0,0 +1,52 @@ +package testrunner + +import ( + "bytes" + "fmt" + "io" + "log" + "strings" + "sync" +) + +type logWriter struct { + buffer *bytes.Buffer + lock *sync.Mutex + log *log.Logger +} + +func newLogWriter(target io.Writer, node int) *logWriter { + return &logWriter{ + buffer: &bytes.Buffer{}, + lock: &sync.Mutex{}, + log: log.New(target, fmt.Sprintf("[%d] ", node), 0), + } +} + +func (w *logWriter) Write(data []byte) (n int, err error) { + w.lock.Lock() + defer w.lock.Unlock() + + w.buffer.Write(data) + contents := w.buffer.String() + + lines := strings.Split(contents, "\n") + for _, line := range lines[0 : len(lines)-1] { + w.log.Println(line) + } + + w.buffer.Reset() + w.buffer.Write([]byte(lines[len(lines)-1])) + return len(data), nil +} + +func (w *logWriter) Close() error { + w.lock.Lock() + defer w.lock.Unlock() + + if w.buffer.Len() > 0 { + w.log.Println(w.buffer.String()) + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/run_result.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/run_result.go new file mode 100644 index 00000000000..5d472acb8d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/run_result.go @@ -0,0 +1,27 @@ +package testrunner + +type RunResult struct { + Passed bool + HasProgrammaticFocus bool +} + +func PassingRunResult() RunResult { + return RunResult{ + Passed: true, + HasProgrammaticFocus: false, + } +} + +func FailingRunResult() RunResult { + return RunResult{ + Passed: false, + HasProgrammaticFocus: false, + } +} + +func (r RunResult) Merge(o RunResult) RunResult { + return RunResult{ + Passed: r.Passed && o.Passed, + HasProgrammaticFocus: r.HasProgrammaticFocus || o.HasProgrammaticFocus, + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/test_runner.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/test_runner.go new file mode 100644 index 00000000000..e1a8098d20d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testrunner/test_runner.go @@ -0,0 +1,378 @@ +package testrunner + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/testsuite" + "github.com/onsi/ginkgo/internal/remote" + "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" +) + +type TestRunner struct { + Suite testsuite.TestSuite + compiled bool + + numCPU int + parallelStream bool + race bool + cover bool + tags string + additionalArgs []string +} + +func New(suite testsuite.TestSuite, numCPU int, parallelStream bool, race bool, cover bool, tags string, additionalArgs []string) *TestRunner { + return &TestRunner{ + Suite: suite, + numCPU: numCPU, + parallelStream: parallelStream, + race: race, + cover: cover, + tags: tags, + additionalArgs: additionalArgs, + } +} + +func (t *TestRunner) Compile() error { + if t.compiled { + return nil + } + + if t.Suite.Precompiled { + return nil + } + + os.Remove(t.compiledArtifact()) + + args := []string{"test", "-c", "-i"} + if t.race { + args = append(args, "-race") + } + if t.cover { + args = append(args, "-cover", "-covermode=atomic") + } + if t.tags != "" { + args = append(args, fmt.Sprintf("-tags=%s", t.tags)) + } + + cmd := exec.Command("go", args...) + + cmd.Dir = t.Suite.Path + + output, err := cmd.CombinedOutput() + + if err != nil { + fixedOutput := fixCompilationOutput(string(output), t.Suite.Path) + if len(output) > 0 { + return fmt.Errorf("Failed to compile %s:\n\n%s", t.Suite.PackageName, fixedOutput) + } + return fmt.Errorf("") + } + + t.compiled = true + return nil +} + +/* +go test -c -i spits package.test out into the cwd. there's no way to change this. + +to make sure it doesn't generate conflicting .test files in the cwd, Compile() must switch the cwd to the test package. + +unfortunately, this causes go test's compile output to be expressed *relative to the test package* instead of the cwd. + +this makes it hard to reason about what failed, and also prevents iterm's Cmd+click from working. + +fixCompilationOutput..... rewrites the output to fix the paths. + +yeah...... +*/ +func fixCompilationOutput(output string, relToPath string) string { + re := regexp.MustCompile(`^(\S.*\.go)\:\d+\:`) + lines := strings.Split(output, "\n") + for i, line := range lines { + indices := re.FindStringSubmatchIndex(line) + if len(indices) == 0 { + continue + } + + path := line[indices[2]:indices[3]] + path = filepath.Join(relToPath, path) + lines[i] = path + line[indices[3]:] + } + return strings.Join(lines, "\n") +} + +func (t *TestRunner) Run() RunResult { + if t.Suite.IsGinkgo { + if t.numCPU > 1 { + if t.parallelStream { + return t.runAndStreamParallelGinkgoSuite() + } else { + return t.runParallelGinkgoSuite() + } + } else { + return t.runSerialGinkgoSuite() + } + } else { + return t.runGoTestSuite() + } +} + +func (t *TestRunner) CleanUp() { + if t.Suite.Precompiled { + return + } + os.Remove(t.compiledArtifact()) +} + +func (t *TestRunner) compiledArtifact() string { + compiledArtifact, _ := filepath.Abs(filepath.Join(t.Suite.Path, fmt.Sprintf("%s.test", t.Suite.PackageName))) + return compiledArtifact +} + +func (t *TestRunner) runSerialGinkgoSuite() RunResult { + ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) + return t.run(t.cmd(ginkgoArgs, os.Stdout, 1), nil) +} + +func (t *TestRunner) runGoTestSuite() RunResult { + return t.run(t.cmd([]string{"-test.v"}, os.Stdout, 1), nil) +} + +func (t *TestRunner) runAndStreamParallelGinkgoSuite() RunResult { + completions := make(chan RunResult) + writers := make([]*logWriter, t.numCPU) + + server, err := remote.NewServer(t.numCPU) + if err != nil { + panic("Failed to start parallel spec server") + } + + server.Start() + defer server.Close() + + for cpu := 0; cpu < t.numCPU; cpu++ { + config.GinkgoConfig.ParallelNode = cpu + 1 + config.GinkgoConfig.ParallelTotal = t.numCPU + config.GinkgoConfig.SyncHost = server.Address() + + ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) + + writers[cpu] = newLogWriter(os.Stdout, cpu+1) + + cmd := t.cmd(ginkgoArgs, writers[cpu], cpu+1) + + server.RegisterAlive(cpu+1, func() bool { + if cmd.ProcessState == nil { + return true + } + return !cmd.ProcessState.Exited() + }) + + go t.run(cmd, completions) + } + + res := PassingRunResult() + + for cpu := 0; cpu < t.numCPU; cpu++ { + res = res.Merge(<-completions) + } + + for _, writer := range writers { + writer.Close() + } + + os.Stdout.Sync() + + if t.cover { + t.combineCoverprofiles() + } + + return res +} + +func (t *TestRunner) runParallelGinkgoSuite() RunResult { + result := make(chan bool) + completions := make(chan RunResult) + writers := make([]*logWriter, t.numCPU) + reports := make([]*bytes.Buffer, t.numCPU) + + stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor) + aggregator := remote.NewAggregator(t.numCPU, result, config.DefaultReporterConfig, stenographer) + + server, err := remote.NewServer(t.numCPU) + if err != nil { + panic("Failed to start parallel spec server") + } + server.RegisterReporters(aggregator) + server.Start() + defer server.Close() + + for cpu := 0; cpu < t.numCPU; cpu++ { + config.GinkgoConfig.ParallelNode = cpu + 1 + config.GinkgoConfig.ParallelTotal = t.numCPU + config.GinkgoConfig.SyncHost = server.Address() + config.GinkgoConfig.StreamHost = server.Address() + + ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) + + reports[cpu] = &bytes.Buffer{} + writers[cpu] = newLogWriter(reports[cpu], cpu+1) + + cmd := t.cmd(ginkgoArgs, writers[cpu], cpu+1) + + server.RegisterAlive(cpu+1, func() bool { + if cmd.ProcessState == nil { + return true + } + return !cmd.ProcessState.Exited() + }) + + go t.run(cmd, completions) + } + + res := PassingRunResult() + + for cpu := 0; cpu < t.numCPU; cpu++ { + res = res.Merge(<-completions) + } + + //all test processes are done, at this point + //we should be able to wait for the aggregator to tell us that it's done + + select { + case <-result: + fmt.Println("") + case <-time.After(time.Second): + //the aggregator never got back to us! something must have gone wrong + fmt.Println("") + fmt.Println("") + fmt.Println(" ----------------------------------------------------------- ") + fmt.Println(" | |") + fmt.Println(" | Ginkgo timed out waiting for all parallel nodes to end! |") + fmt.Println(" | Here is some salvaged output: |") + fmt.Println(" | |") + fmt.Println(" ----------------------------------------------------------- ") + fmt.Println("") + fmt.Println("") + + os.Stdout.Sync() + + time.Sleep(time.Second) + + for _, writer := range writers { + writer.Close() + } + + for _, report := range reports { + fmt.Print(report.String()) + } + + os.Stdout.Sync() + } + + if t.cover { + t.combineCoverprofiles() + } + + return res +} + +func (t *TestRunner) cmd(ginkgoArgs []string, stream io.Writer, node int) *exec.Cmd { + args := []string{"-test.timeout=24h"} + if t.cover { + coverprofile := "--test.coverprofile=" + t.Suite.PackageName + ".coverprofile" + if t.numCPU > 1 { + coverprofile = fmt.Sprintf("%s.%d", coverprofile, node) + } + args = append(args, coverprofile) + } + + args = append(args, ginkgoArgs...) + args = append(args, t.additionalArgs...) + + cmd := exec.Command(t.compiledArtifact(), args...) + + cmd.Dir = t.Suite.Path + cmd.Stderr = stream + cmd.Stdout = stream + + return cmd +} + +func (t *TestRunner) run(cmd *exec.Cmd, completions chan RunResult) RunResult { + var res RunResult + + defer func() { + if completions != nil { + completions <- res + } + }() + + err := cmd.Start() + if err != nil { + fmt.Printf("Failed to run test suite!\n\t%s", err.Error()) + return res + } + + cmd.Wait() + exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() + res.Passed = (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) + res.HasProgrammaticFocus = (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) + + return res +} + +func (t *TestRunner) combineCoverprofiles() { + profiles := []string{} + for cpu := 1; cpu <= t.numCPU; cpu++ { + coverFile := fmt.Sprintf("%s.coverprofile.%d", t.Suite.PackageName, cpu) + coverFile = filepath.Join(t.Suite.Path, coverFile) + coverProfile, err := ioutil.ReadFile(coverFile) + os.Remove(coverFile) + + if err == nil { + profiles = append(profiles, string(coverProfile)) + } + } + + if len(profiles) != t.numCPU { + return + } + + lines := map[string]int{} + lineOrder := []string{} + for i, coverProfile := range profiles { + for _, line := range strings.Split(string(coverProfile), "\n")[1:] { + if len(line) == 0 { + continue + } + components := strings.Split(line, " ") + count, _ := strconv.Atoi(components[len(components)-1]) + prefix := strings.Join(components[0:len(components)-1], " ") + lines[prefix] += count + if i == 0 { + lineOrder = append(lineOrder, prefix) + } + } + } + + output := []string{"mode: atomic"} + for _, line := range lineOrder { + output = append(output, fmt.Sprintf("%s %d", line, lines[line])) + } + finalOutput := strings.Join(output, "\n") + ioutil.WriteFile(filepath.Join(t.Suite.Path, fmt.Sprintf("%s.coverprofile", t.Suite.PackageName)), []byte(finalOutput), 0666) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/test_suite.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/test_suite.go new file mode 100644 index 00000000000..cc7d2f45393 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/test_suite.go @@ -0,0 +1,106 @@ +package testsuite + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" +) + +type TestSuite struct { + Path string + PackageName string + IsGinkgo bool + Precompiled bool +} + +func PrecompiledTestSuite(path string) (TestSuite, error) { + info, err := os.Stat(path) + if err != nil { + return TestSuite{}, err + } + + if info.IsDir() { + return TestSuite{}, errors.New("this is a directory, not a file") + } + + if filepath.Ext(path) != ".test" { + return TestSuite{}, errors.New("this is not a .test binary") + } + + if info.Mode()&0111 == 0 { + return TestSuite{}, errors.New("this is not executable") + } + + dir := relPath(filepath.Dir(path)) + packageName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + + return TestSuite{ + Path: dir, + PackageName: packageName, + IsGinkgo: true, + Precompiled: true, + }, nil +} + +func SuitesInDir(dir string, recurse bool) []TestSuite { + suites := []TestSuite{} + files, _ := ioutil.ReadDir(dir) + re := regexp.MustCompile(`_test\.go$`) + for _, file := range files { + if !file.IsDir() && re.Match([]byte(file.Name())) { + suites = append(suites, New(dir, files)) + break + } + } + + if recurse { + re = regexp.MustCompile(`^[._]`) + for _, file := range files { + if file.IsDir() && !re.Match([]byte(file.Name())) { + suites = append(suites, SuitesInDir(dir+"/"+file.Name(), recurse)...) + } + } + } + + return suites +} + +func relPath(dir string) string { + dir, _ = filepath.Abs(dir) + cwd, _ := os.Getwd() + dir, _ = filepath.Rel(cwd, filepath.Clean(dir)) + dir = "." + string(filepath.Separator) + dir + return dir +} + +func New(dir string, files []os.FileInfo) TestSuite { + return TestSuite{ + Path: relPath(dir), + PackageName: packageNameForSuite(dir), + IsGinkgo: filesHaveGinkgoSuite(dir, files), + } +} + +func packageNameForSuite(dir string) string { + path, _ := filepath.Abs(dir) + return filepath.Base(path) +} + +func filesHaveGinkgoSuite(dir string, files []os.FileInfo) bool { + reTestFile := regexp.MustCompile(`_test\.go$`) + reGinkgo := regexp.MustCompile(`package ginkgo|\/ginkgo"`) + + for _, file := range files { + if !file.IsDir() && reTestFile.Match([]byte(file.Name())) { + contents, _ := ioutil.ReadFile(dir + "/" + file.Name()) + if reGinkgo.Match(contents) { + return true + } + } + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_suite_test.go new file mode 100644 index 00000000000..d1e8b21d37e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_suite_test.go @@ -0,0 +1,13 @@ +package testsuite_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTestsuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testsuite Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_test.go new file mode 100644 index 00000000000..8681ffc11a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/testsuite/testsuite_test.go @@ -0,0 +1,167 @@ +package testsuite_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/ginkgo/testsuite" + . "github.com/onsi/gomega" +) + +var _ = Describe("TestSuite", func() { + var tmpDir string + var relTmpDir string + + writeFile := func(folder string, filename string, content string, mode os.FileMode) { + path := filepath.Join(tmpDir, folder) + err := os.MkdirAll(path, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + path = filepath.Join(path, filename) + ioutil.WriteFile(path, []byte(content), mode) + } + + BeforeEach(func() { + var err error + tmpDir, err = ioutil.TempDir("/tmp", "ginkgo") + Ω(err).ShouldNot(HaveOccurred()) + + cwd, err := os.Getwd() + Ω(err).ShouldNot(HaveOccurred()) + relTmpDir, err = filepath.Rel(cwd, tmpDir) + relTmpDir = "./" + relTmpDir + Ω(err).ShouldNot(HaveOccurred()) + + //go files in the root directory (no tests) + writeFile("/", "main.go", "package main", 0666) + + //non-go files in a nested directory + writeFile("/redherring", "big_test.jpg", "package ginkgo", 0666) + + //non-ginkgo tests in a nested directory + writeFile("/professorplum", "professorplum_test.go", `import "testing"`, 0666) + + //ginkgo tests in a nested directory + writeFile("/colonelmustard", "colonelmustard_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //ginkgo tests in a deeply nested directory + writeFile("/colonelmustard/library", "library_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //a precompiled ginkgo test + writeFile("/precompiled-dir", "precompiled.test", `fake-binary-file`, 0777) + writeFile("/precompiled-dir", "some-other-binary", `fake-binary-file`, 0777) + writeFile("/precompiled-dir", "nonexecutable.test", `fake-binary-file`, 0666) + }) + + AfterEach(func() { + os.RemoveAll(tmpDir) + }) + + Describe("Finding precompiled test suites", func() { + Context("if pointed at an executable file that ends with .test", func() { + It("should return a precompiled test suite", func() { + suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "precompiled.test")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(suite).Should(Equal(TestSuite{ + Path: relTmpDir + "/precompiled-dir", + PackageName: "precompiled", + IsGinkgo: true, + Precompiled: true, + })) + }) + }) + + Context("if pointed at a directory", func() { + It("should error", func() { + suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir")) + Ω(suite).Should(BeZero()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("if pointed at an executable that doesn't have .test", func() { + It("should error", func() { + suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "some-other-binary")) + Ω(suite).Should(BeZero()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("if pointed at a .test that isn't executable", func() { + It("should error", func() { + suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nonexecutable.test")) + Ω(suite).Should(BeZero()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("if pointed at a nonexisting file", func() { + It("should error", func() { + suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nope-nothing-to-see-here")) + Ω(suite).Should(BeZero()) + Ω(err).Should(HaveOccurred()) + }) + }) + }) + + Describe("scanning for suites in a directory", func() { + Context("when there are no tests in the specified directory", func() { + It("should come up empty", func() { + suites := SuitesInDir(tmpDir, false) + Ω(suites).Should(BeEmpty()) + }) + }) + + Context("when there are ginkgo tests in the specified directory", func() { + It("should return an appropriately configured suite", func() { + suites := SuitesInDir(filepath.Join(tmpDir, "colonelmustard"), false) + Ω(suites).Should(HaveLen(1)) + + Ω(suites[0].Path).Should(Equal(relTmpDir + "/colonelmustard")) + Ω(suites[0].PackageName).Should(Equal("colonelmustard")) + Ω(suites[0].IsGinkgo).Should(BeTrue()) + Ω(suites[0].Precompiled).Should(BeFalse()) + }) + }) + + Context("when there are non-ginkgo tests in the specified directory", func() { + It("should return an appropriately configured suite", func() { + suites := SuitesInDir(filepath.Join(tmpDir, "professorplum"), false) + Ω(suites).Should(HaveLen(1)) + + Ω(suites[0].Path).Should(Equal(relTmpDir + "/professorplum")) + Ω(suites[0].PackageName).Should(Equal("professorplum")) + Ω(suites[0].IsGinkgo).Should(BeFalse()) + Ω(suites[0].Precompiled).Should(BeFalse()) + }) + }) + + Context("when recursively scanning", func() { + It("should return suites for corresponding test suites, only", func() { + suites := SuitesInDir(tmpDir, true) + Ω(suites).Should(HaveLen(3)) + + Ω(suites).Should(ContainElement(TestSuite{ + Path: relTmpDir + "/colonelmustard", + PackageName: "colonelmustard", + IsGinkgo: true, + Precompiled: false, + })) + Ω(suites).Should(ContainElement(TestSuite{ + Path: relTmpDir + "/professorplum", + PackageName: "professorplum", + IsGinkgo: false, + Precompiled: false, + })) + Ω(suites).Should(ContainElement(TestSuite{ + Path: relTmpDir + "/colonelmustard/library", + PackageName: "library", + IsGinkgo: true, + Precompiled: false, + })) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/unfocus_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/unfocus_command.go new file mode 100644 index 00000000000..16f3c3b72e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/unfocus_command.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "fmt" + "os/exec" +) + +func BuildUnfocusCommand() *Command { + return &Command{ + Name: "unfocus", + AltName: "blur", + FlagSet: flag.NewFlagSet("unfocus", flag.ExitOnError), + UsageCommand: "ginkgo unfocus (or ginkgo blur)", + Usage: []string{ + "Recursively unfocuses any focused tests under the current directory", + }, + Command: unfocusSpecs, + } +} + +func unfocusSpecs([]string, []string) { + unfocus("Describe") + unfocus("Context") + unfocus("It") + unfocus("Measure") +} + +func unfocus(component string) { + fmt.Printf("Removing F%s...\n", component) + cmd := exec.Command("gofmt", fmt.Sprintf("-r=F%s -> %s", component, component), "-w", ".") + out, _ := cmd.CombinedOutput() + if string(out) != "" { + println(string(out)) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/version_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/version_command.go new file mode 100644 index 00000000000..cdca3a348b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/version_command.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "fmt" + "github.com/onsi/ginkgo/config" +) + +func BuildVersionCommand() *Command { + return &Command{ + Name: "version", + FlagSet: flag.NewFlagSet("version", flag.ExitOnError), + UsageCommand: "ginkgo version", + Usage: []string{ + "Print Ginkgo's version", + }, + Command: printVersion, + } +} + +func printVersion([]string, []string) { + fmt.Printf("Ginkgo Version %s\n", config.VERSION) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta.go new file mode 100644 index 00000000000..6c485c5b1af --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta.go @@ -0,0 +1,22 @@ +package watch + +import "sort" + +type Delta struct { + ModifiedPackages []string + + NewSuites []*Suite + RemovedSuites []*Suite + modifiedSuites []*Suite +} + +type DescendingByDelta []*Suite + +func (a DescendingByDelta) Len() int { return len(a) } +func (a DescendingByDelta) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a DescendingByDelta) Less(i, j int) bool { return a[i].Delta() > a[j].Delta() } + +func (d Delta) ModifiedSuites() []*Suite { + sort.Sort(DescendingByDelta(d.modifiedSuites)) + return d.modifiedSuites +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta_tracker.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta_tracker.go new file mode 100644 index 00000000000..96e83d6ccbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/delta_tracker.go @@ -0,0 +1,71 @@ +package watch + +import ( + "fmt" + + "github.com/onsi/ginkgo/ginkgo/testsuite" +) + +type SuiteErrors map[testsuite.TestSuite]error + +type DeltaTracker struct { + maxDepth int + suites map[string]*Suite + packageHashes *PackageHashes +} + +func NewDeltaTracker(maxDepth int) *DeltaTracker { + return &DeltaTracker{ + maxDepth: maxDepth, + packageHashes: NewPackageHashes(), + suites: map[string]*Suite{}, + } +} + +func (d *DeltaTracker) Delta(suites []testsuite.TestSuite) (delta Delta, errors SuiteErrors) { + errors = SuiteErrors{} + delta.ModifiedPackages = d.packageHashes.CheckForChanges() + + providedSuitePaths := map[string]bool{} + for _, suite := range suites { + providedSuitePaths[suite.Path] = true + } + + d.packageHashes.StartTrackingUsage() + + for _, suite := range d.suites { + if providedSuitePaths[suite.Suite.Path] { + if suite.Delta() > 0 { + delta.modifiedSuites = append(delta.modifiedSuites, suite) + } + } else { + delta.RemovedSuites = append(delta.RemovedSuites, suite) + } + } + + d.packageHashes.StopTrackingUsageAndPrune() + + for _, suite := range suites { + _, ok := d.suites[suite.Path] + if !ok { + s, err := NewSuite(suite, d.maxDepth, d.packageHashes) + if err != nil { + errors[suite] = err + continue + } + d.suites[suite.Path] = s + delta.NewSuites = append(delta.NewSuites, s) + } + } + + return delta, errors +} + +func (d *DeltaTracker) WillRun(suite testsuite.TestSuite) error { + s, ok := d.suites[suite.Path] + if !ok { + return fmt.Errorf("unkown suite %s", suite.Path) + } + + return s.MarkAsRunAndRecomputedDependencies(d.maxDepth) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/dependencies.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/dependencies.go new file mode 100644 index 00000000000..82c25face30 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/dependencies.go @@ -0,0 +1,91 @@ +package watch + +import ( + "go/build" + "regexp" +) + +var ginkgoAndGomegaFilter = regexp.MustCompile(`github\.com/onsi/ginkgo|github\.com/onsi/gomega`) + +type Dependencies struct { + deps map[string]int +} + +func NewDependencies(path string, maxDepth int) (Dependencies, error) { + d := Dependencies{ + deps: map[string]int{}, + } + + if maxDepth == 0 { + return d, nil + } + + err := d.seedWithDepsForPackageAtPath(path) + if err != nil { + return d, err + } + + for depth := 1; depth < maxDepth; depth++ { + n := len(d.deps) + d.addDepsForDepth(depth) + if n == len(d.deps) { + break + } + } + + return d, nil +} + +func (d Dependencies) Dependencies() map[string]int { + return d.deps +} + +func (d Dependencies) seedWithDepsForPackageAtPath(path string) error { + pkg, err := build.ImportDir(path, 0) + if err != nil { + return err + } + + d.resolveAndAdd(pkg.Imports, 1) + d.resolveAndAdd(pkg.TestImports, 1) + d.resolveAndAdd(pkg.XTestImports, 1) + + delete(d.deps, pkg.Dir) + return nil +} + +func (d Dependencies) addDepsForDepth(depth int) { + for dep, depDepth := range d.deps { + if depDepth == depth { + d.addDepsForDep(dep, depth+1) + } + } +} + +func (d Dependencies) addDepsForDep(dep string, depth int) { + pkg, err := build.ImportDir(dep, 0) + if err != nil { + println(err.Error()) + return + } + d.resolveAndAdd(pkg.Imports, depth) +} + +func (d Dependencies) resolveAndAdd(deps []string, depth int) { + for _, dep := range deps { + pkg, err := build.Import(dep, ".", 0) + if err != nil { + continue + } + if pkg.Goroot == false && !ginkgoAndGomegaFilter.Match([]byte(pkg.Dir)) { + d.addDepIfNotPresent(pkg.Dir, depth) + } + } +} + +func (d Dependencies) addDepIfNotPresent(dep string, depth int) { + _, ok := d.deps[dep] + if !ok { + d.deps[dep] = depth + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hash.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hash.go new file mode 100644 index 00000000000..eaf357c249c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hash.go @@ -0,0 +1,103 @@ +package watch + +import ( + "fmt" + "io/ioutil" + "os" + "regexp" + "time" +) + +var goRegExp = regexp.MustCompile(`\.go$`) +var goTestRegExp = regexp.MustCompile(`_test\.go$`) + +type PackageHash struct { + CodeModifiedTime time.Time + TestModifiedTime time.Time + Deleted bool + + path string + codeHash string + testHash string +} + +func NewPackageHash(path string) *PackageHash { + p := &PackageHash{ + path: path, + } + + p.codeHash, _, p.testHash, _, p.Deleted = p.computeHashes() + + return p +} + +func (p *PackageHash) CheckForChanges() bool { + codeHash, codeModifiedTime, testHash, testModifiedTime, deleted := p.computeHashes() + + if deleted { + if p.Deleted == false { + t := time.Now() + p.CodeModifiedTime = t + p.TestModifiedTime = t + } + p.Deleted = true + return true + } + + modified := false + p.Deleted = false + + if p.codeHash != codeHash { + p.CodeModifiedTime = codeModifiedTime + modified = true + } + if p.testHash != testHash { + p.TestModifiedTime = testModifiedTime + modified = true + } + + p.codeHash = codeHash + p.testHash = testHash + return modified +} + +func (p *PackageHash) computeHashes() (codeHash string, codeModifiedTime time.Time, testHash string, testModifiedTime time.Time, deleted bool) { + infos, err := ioutil.ReadDir(p.path) + + if err != nil { + deleted = true + return + } + + for _, info := range infos { + if info.IsDir() { + continue + } + + if goTestRegExp.Match([]byte(info.Name())) { + testHash += p.hashForFileInfo(info) + if info.ModTime().After(testModifiedTime) { + testModifiedTime = info.ModTime() + } + continue + } + + if goRegExp.Match([]byte(info.Name())) { + codeHash += p.hashForFileInfo(info) + if info.ModTime().After(codeModifiedTime) { + codeModifiedTime = info.ModTime() + } + } + } + + testHash += codeHash + if codeModifiedTime.After(testModifiedTime) { + testModifiedTime = codeModifiedTime + } + + return +} + +func (p *PackageHash) hashForFileInfo(info os.FileInfo) string { + return fmt.Sprintf("%s_%d_%d", info.Name(), info.Size(), info.ModTime().UnixNano()) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hashes.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hashes.go new file mode 100644 index 00000000000..262eaa847ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/package_hashes.go @@ -0,0 +1,82 @@ +package watch + +import ( + "path/filepath" + "sync" +) + +type PackageHashes struct { + PackageHashes map[string]*PackageHash + usedPaths map[string]bool + lock *sync.Mutex +} + +func NewPackageHashes() *PackageHashes { + return &PackageHashes{ + PackageHashes: map[string]*PackageHash{}, + usedPaths: nil, + lock: &sync.Mutex{}, + } +} + +func (p *PackageHashes) CheckForChanges() []string { + p.lock.Lock() + defer p.lock.Unlock() + + modified := []string{} + + for _, packageHash := range p.PackageHashes { + if packageHash.CheckForChanges() { + modified = append(modified, packageHash.path) + } + } + + return modified +} + +func (p *PackageHashes) Add(path string) *PackageHash { + p.lock.Lock() + defer p.lock.Unlock() + + path, _ = filepath.Abs(path) + _, ok := p.PackageHashes[path] + if !ok { + p.PackageHashes[path] = NewPackageHash(path) + } + + if p.usedPaths != nil { + p.usedPaths[path] = true + } + return p.PackageHashes[path] +} + +func (p *PackageHashes) Get(path string) *PackageHash { + p.lock.Lock() + defer p.lock.Unlock() + + path, _ = filepath.Abs(path) + if p.usedPaths != nil { + p.usedPaths[path] = true + } + return p.PackageHashes[path] +} + +func (p *PackageHashes) StartTrackingUsage() { + p.lock.Lock() + defer p.lock.Unlock() + + p.usedPaths = map[string]bool{} +} + +func (p *PackageHashes) StopTrackingUsageAndPrune() { + p.lock.Lock() + defer p.lock.Unlock() + + for path := range p.PackageHashes { + if !p.usedPaths[path] { + delete(p.PackageHashes, path) + } + } + + p.usedPaths = nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/suite.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/suite.go new file mode 100644 index 00000000000..5deaba7cb6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch/suite.go @@ -0,0 +1,87 @@ +package watch + +import ( + "fmt" + "math" + "time" + + "github.com/onsi/ginkgo/ginkgo/testsuite" +) + +type Suite struct { + Suite testsuite.TestSuite + RunTime time.Time + Dependencies Dependencies + + sharedPackageHashes *PackageHashes +} + +func NewSuite(suite testsuite.TestSuite, maxDepth int, sharedPackageHashes *PackageHashes) (*Suite, error) { + deps, err := NewDependencies(suite.Path, maxDepth) + if err != nil { + return nil, err + } + + sharedPackageHashes.Add(suite.Path) + for dep := range deps.Dependencies() { + sharedPackageHashes.Add(dep) + } + + return &Suite{ + Suite: suite, + Dependencies: deps, + + sharedPackageHashes: sharedPackageHashes, + }, nil +} + +func (s *Suite) Delta() float64 { + delta := s.delta(s.Suite.Path, true, 0) * 1000 + for dep, depth := range s.Dependencies.Dependencies() { + delta += s.delta(dep, false, depth) + } + return delta +} + +func (s *Suite) MarkAsRunAndRecomputedDependencies(maxDepth int) error { + s.RunTime = time.Now() + + deps, err := NewDependencies(s.Suite.Path, maxDepth) + if err != nil { + return err + } + + s.sharedPackageHashes.Add(s.Suite.Path) + for dep := range deps.Dependencies() { + s.sharedPackageHashes.Add(dep) + } + + s.Dependencies = deps + + return nil +} + +func (s *Suite) Description() string { + numDeps := len(s.Dependencies.Dependencies()) + pluralizer := "ies" + if numDeps == 1 { + pluralizer = "y" + } + return fmt.Sprintf("%s [%d dependenc%s]", s.Suite.Path, numDeps, pluralizer) +} + +func (s *Suite) delta(packagePath string, includeTests bool, depth int) float64 { + return math.Max(float64(s.dt(packagePath, includeTests)), 0) / float64(depth+1) +} + +func (s *Suite) dt(packagePath string, includeTests bool) time.Duration { + packageHash := s.sharedPackageHashes.Get(packagePath) + var modifiedTime time.Time + if includeTests { + modifiedTime = packageHash.TestModifiedTime + } else { + modifiedTime = packageHash.CodeModifiedTime + } + + return modifiedTime.Sub(s.RunTime) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch_command.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch_command.go new file mode 100644 index 00000000000..ae988fbe9a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo/watch_command.go @@ -0,0 +1,171 @@ +package main + +import ( + "flag" + "fmt" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/testrunner" + "github.com/onsi/ginkgo/ginkgo/testsuite" + "github.com/onsi/ginkgo/ginkgo/watch" +) + +func BuildWatchCommand() *Command { + commandFlags := NewWatchCommandFlags(flag.NewFlagSet("watch", flag.ExitOnError)) + interruptHandler := NewInterruptHandler() + notifier := NewNotifier(commandFlags) + watcher := &SpecWatcher{ + commandFlags: commandFlags, + notifier: notifier, + interruptHandler: interruptHandler, + suiteRunner: NewSuiteRunner(notifier, interruptHandler), + } + + return &Command{ + Name: "watch", + FlagSet: commandFlags.FlagSet, + UsageCommand: "ginkgo watch -- ", + Usage: []string{ + "Watches the tests in the passed in and runs them when changes occur.", + "Any arguments after -- will be passed to the test.", + }, + Command: watcher.WatchSpecs, + SuppressFlagDocumentation: true, + FlagDocSubstitute: []string{ + "Accepts all the flags that the ginkgo command accepts except for --keepGoing and --untilItFails", + }, + } +} + +type SpecWatcher struct { + commandFlags *RunWatchAndBuildCommandFlags + notifier *Notifier + interruptHandler *InterruptHandler + suiteRunner *SuiteRunner +} + +func (w *SpecWatcher) WatchSpecs(args []string, additionalArgs []string) { + w.commandFlags.computeNodes() + w.notifier.VerifyNotificationsAreAvailable() + + w.WatchSuites(args, additionalArgs) +} + +func (w *SpecWatcher) runnersForSuites(suites []testsuite.TestSuite, additionalArgs []string) []*testrunner.TestRunner { + runners := []*testrunner.TestRunner{} + + for _, suite := range suites { + runners = append(runners, testrunner.New(suite, w.commandFlags.NumCPU, w.commandFlags.ParallelStream, w.commandFlags.Race, w.commandFlags.Cover, w.commandFlags.Tags, additionalArgs)) + } + + return runners +} + +func (w *SpecWatcher) WatchSuites(args []string, additionalArgs []string) { + suites, _ := findSuites(args, w.commandFlags.Recurse, w.commandFlags.SkipPackage, false) + + if len(suites) == 0 { + complainAndQuit("Found no test suites") + } + + fmt.Printf("Identified %d test %s. Locating dependencies to a depth of %d (this may take a while)...\n", len(suites), pluralizedWord("suite", "suites", len(suites)), w.commandFlags.Depth) + deltaTracker := watch.NewDeltaTracker(w.commandFlags.Depth) + delta, errors := deltaTracker.Delta(suites) + + fmt.Printf("Watching %d %s:\n", len(delta.NewSuites), pluralizedWord("suite", "suites", len(delta.NewSuites))) + for _, suite := range delta.NewSuites { + fmt.Println(" " + suite.Description()) + } + + for suite, err := range errors { + fmt.Printf("Failed to watch %s: %s\n"+suite.PackageName, err) + } + + if len(suites) == 1 { + runners := w.runnersForSuites(suites, additionalArgs) + w.suiteRunner.RunSuites(runners, w.commandFlags.NumCompilers, true, nil) + runners[0].CleanUp() + } + + ticker := time.NewTicker(time.Second) + + for { + select { + case <-ticker.C: + suites, _ := findSuites(args, w.commandFlags.Recurse, w.commandFlags.SkipPackage, false) + delta, _ := deltaTracker.Delta(suites) + + suitesToRun := []testsuite.TestSuite{} + + if len(delta.NewSuites) > 0 { + fmt.Printf(greenColor+"Detected %d new %s:\n"+defaultStyle, len(delta.NewSuites), pluralizedWord("suite", "suites", len(delta.NewSuites))) + for _, suite := range delta.NewSuites { + suitesToRun = append(suitesToRun, suite.Suite) + fmt.Println(" " + suite.Description()) + } + } + + modifiedSuites := delta.ModifiedSuites() + if len(modifiedSuites) > 0 { + fmt.Println(greenColor + "\nDetected changes in:" + defaultStyle) + for _, pkg := range delta.ModifiedPackages { + fmt.Println(" " + pkg) + } + fmt.Printf(greenColor+"Will run %d %s:\n"+defaultStyle, len(modifiedSuites), pluralizedWord("suite", "suites", len(modifiedSuites))) + for _, suite := range modifiedSuites { + suitesToRun = append(suitesToRun, suite.Suite) + fmt.Println(" " + suite.Description()) + } + fmt.Println("") + } + + if len(suitesToRun) > 0 { + w.UpdateSeed() + w.ComputeSuccinctMode(len(suitesToRun)) + runners := w.runnersForSuites(suitesToRun, additionalArgs) + result, _ := w.suiteRunner.RunSuites(runners, w.commandFlags.NumCompilers, true, func(suite testsuite.TestSuite) { + deltaTracker.WillRun(suite) + }) + for _, runner := range runners { + runner.CleanUp() + } + if !w.interruptHandler.WasInterrupted() { + color := redColor + if result.Passed { + color = greenColor + } + fmt.Println(color + "\nDone. Resuming watch..." + defaultStyle) + } + } + + case <-w.interruptHandler.C: + return + } + } +} + +func (w *SpecWatcher) ComputeSuccinctMode(numSuites int) { + if config.DefaultReporterConfig.Verbose { + config.DefaultReporterConfig.Succinct = false + return + } + + if w.commandFlags.wasSet("succinct") { + return + } + + if numSuites == 1 { + config.DefaultReporterConfig.Succinct = false + } + + if numSuites > 1 { + config.DefaultReporterConfig.Succinct = true + } +} + +func (w *SpecWatcher) UpdateSeed() { + if !w.commandFlags.wasSet("seed") { + config.GinkgoConfig.RandomSeed = time.Now().Unix() + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo_dsl.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo_dsl.go new file mode 100644 index 00000000000..1a31473845d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/ginkgo_dsl.go @@ -0,0 +1,521 @@ +/* +Ginkgo is a BDD-style testing framework for Golang + +The godoc documentation describes Ginkgo's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/ginkgo/ + +Ginkgo's preferred matcher library is [Gomega](http://github.com/onsi/gomega) + +Ginkgo on Github: http://github.com/onsi/ginkgo + +Ginkgo is MIT-Licensed +*/ +package ginkgo + +import ( + "flag" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/internal/remote" + "github.com/onsi/ginkgo/internal/suite" + "github.com/onsi/ginkgo/internal/testingtproxy" + "github.com/onsi/ginkgo/internal/writer" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" +) + +const GINKGO_VERSION = config.VERSION +const GINKGO_PANIC = ` +Your test failed. +Ginkgo panics to prevent subsequent assertions from running. +Normally Ginkgo rescues this panic so you shouldn't see it. + +But, if you make an assertion in a goroutine, Ginkgo can't capture the panic. +To circumvent this, you should call + + defer GinkgoRecover() + +at the top of the goroutine that caused this panic. +` +const defaultTimeout = 1 + +var globalSuite *suite.Suite +var globalFailer *failer.Failer + +func init() { + config.Flags(flag.CommandLine, "ginkgo", true) + GinkgoWriter = writer.New(os.Stdout) + globalFailer = failer.New() + globalSuite = suite.New(globalFailer) +} + +//GinkgoWriter implements an io.Writer +//When running in verbose mode any writes to GinkgoWriter will be immediately printed +//to stdout. Otherwise, GinkgoWriter will buffer any writes produced during the current test and flush them to screen +//only if the current test fails. +var GinkgoWriter io.Writer + +//The interface by which Ginkgo receives *testing.T +type GinkgoTestingT interface { + Fail() +} + +//GinkgoParallelNode returns the parallel node number for the current ginkgo process +//The node number is 1-indexed +func GinkgoParallelNode() int { + return config.GinkgoConfig.ParallelNode +} + +//Some matcher libraries or legacy codebases require a *testing.T +//GinkgoT implements an interface analogous to *testing.T and can be used if +//the library in question accepts *testing.T through an interface +// +// For example, with testify: +// assert.Equal(GinkgoT(), 123, 123, "they should be equal") +// +// Or with gomock: +// gomock.NewController(GinkgoT()) +// +// GinkgoT() takes an optional offset argument that can be used to get the +// correct line number associated with the failure. +func GinkgoT(optionalOffset ...int) GinkgoTInterface { + offset := 3 + if len(optionalOffset) > 0 { + offset = optionalOffset[0] + } + return testingtproxy.New(GinkgoWriter, Fail, offset) +} + +//The interface returned by GinkgoT(). This covers most of the methods +//in the testing package's T. +type GinkgoTInterface interface { + Fail() + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + FailNow() + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Failed() bool + Parallel() + Skip(args ...interface{}) + Skipf(format string, args ...interface{}) + SkipNow() + Skipped() bool +} + +//Custom Ginkgo test reporters must implement the Reporter interface. +// +//The custom reporter is passed in a SuiteSummary when the suite begins and ends, +//and a SpecSummary just before a spec begins and just after a spec ends +type Reporter reporters.Reporter + +//Asynchronous specs are given a channel of the Done type. You must close or write to the channel +//to tell Ginkgo that your async test is done. +type Done chan<- interface{} + +//GinkgoTestDescription represents the information about the current running test returned by CurrentGinkgoTestDescription +// FullTestText: a concatenation of ComponentTexts and the TestText +// ComponentTexts: a list of all texts for the Describes & Contexts leading up to the current test +// TestText: the text in the actual It or Measure node +// IsMeasurement: true if the current test is a measurement +// FileName: the name of the file containing the current test +// LineNumber: the line number for the current test +type GinkgoTestDescription struct { + FullTestText string + ComponentTexts []string + TestText string + + IsMeasurement bool + + FileName string + LineNumber int +} + +//CurrentGinkgoTestDescripton returns information about the current running test. +func CurrentGinkgoTestDescription() GinkgoTestDescription { + summary, ok := globalSuite.CurrentRunningSpecSummary() + if !ok { + return GinkgoTestDescription{} + } + + subjectCodeLocation := summary.ComponentCodeLocations[len(summary.ComponentCodeLocations)-1] + + return GinkgoTestDescription{ + ComponentTexts: summary.ComponentTexts[1:], + FullTestText: strings.Join(summary.ComponentTexts[1:], " "), + TestText: summary.ComponentTexts[len(summary.ComponentTexts)-1], + IsMeasurement: summary.IsMeasurement, + FileName: subjectCodeLocation.FileName, + LineNumber: subjectCodeLocation.LineNumber, + } +} + +//Measurement tests receive a Benchmarker. +// +//You use the Time() function to time how long the passed in body function takes to run +//You use the RecordValue() function to track arbitrary numerical measurements. +//The optional info argument is passed to the test reporter and can be used to +// provide the measurement data to a custom reporter with context. +// +//See http://onsi.github.io/ginkgo/#benchmark_tests for more details +type Benchmarker interface { + Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) + RecordValue(name string, value float64, info ...interface{}) +} + +//RunSpecs is the entry point for the Ginkgo test runner. +//You must call this within a Golang testing TestX(t *testing.T) function. +// +//To bootstrap a test suite you can use the Ginkgo CLI: +// +// ginkgo bootstrap +func RunSpecs(t GinkgoTestingT, description string) bool { + specReporters := []Reporter{buildDefaultReporter()} + return RunSpecsWithCustomReporters(t, description, specReporters) +} + +//To run your tests with Ginkgo's default reporter and your custom reporter(s), replace +//RunSpecs() with this method. +func RunSpecsWithDefaultAndCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { + specReporters = append([]Reporter{buildDefaultReporter()}, specReporters...) + return RunSpecsWithCustomReporters(t, description, specReporters) +} + +//To run your tests with your custom reporter(s) (and *not* Ginkgo's default reporter), replace +//RunSpecs() with this method. Note that parallel tests will not work correctly without the default reporter +func RunSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { + writer := GinkgoWriter.(*writer.Writer) + writer.SetStream(config.DefaultReporterConfig.Verbose) + reporters := make([]reporters.Reporter, len(specReporters)) + for i, reporter := range specReporters { + reporters[i] = reporter + } + passed, hasFocusedTests := globalSuite.Run(t, description, reporters, writer, config.GinkgoConfig) + if passed && hasFocusedTests { + fmt.Println("PASS | FOCUSED") + os.Exit(types.GINKGO_FOCUS_EXIT_CODE) + } + return passed +} + +func buildDefaultReporter() Reporter { + remoteReportingServer := config.GinkgoConfig.StreamHost + if remoteReportingServer == "" { + stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor) + return reporters.NewDefaultReporter(config.DefaultReporterConfig, stenographer) + } else { + return remote.NewForwardingReporter(remoteReportingServer, &http.Client{}, remote.NewOutputInterceptor()) + } +} + +//Fail notifies Ginkgo that the current spec has failed. (Gomega will call Fail for you automatically when an assertion fails.) +func Fail(message string, callerSkip ...int) { + skip := 0 + if len(callerSkip) > 0 { + skip = callerSkip[0] + } + + globalFailer.Fail(message, codelocation.New(skip+1)) + panic(GINKGO_PANIC) +} + +//GinkgoRecover should be deferred at the top of any spawned goroutine that (may) call `Fail` +//Since Gomega assertions call fail, you should throw a `defer GinkgoRecover()` at the top of any goroutine that +//calls out to Gomega +// +//Here's why: Ginkgo's `Fail` method records the failure and then panics to prevent +//further assertions from running. This panic must be recovered. Ginkgo does this for you +//if the panic originates in a Ginkgo node (an It, BeforeEach, etc...) +// +//Unfortunately, if a panic originates on a goroutine *launched* from one of these nodes there's no +//way for Ginkgo to rescue the panic. To do this, you must remember to `defer GinkgoRecover()` at the top of such a goroutine. +func GinkgoRecover() { + e := recover() + if e != nil { + globalFailer.Panic(codelocation.New(1), e) + } +} + +//Describe blocks allow you to organize your specs. A Describe block can contain any number of +//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks. +// +//In addition you can nest Describe and Context blocks. Describe and Context blocks are functionally +//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object +//or method and, within that Describe, outline a number of Contexts. +func Describe(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) + return true +} + +//You can focus the tests within a describe block using FDescribe +func FDescribe(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) + return true +} + +//You can mark the tests within a describe block as pending using PDescribe +func PDescribe(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) + return true +} + +//You can mark the tests within a describe block as pending using XDescribe +func XDescribe(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) + return true +} + +//Context blocks allow you to organize your specs. A Context block can contain any number of +//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks. +// +//In addition you can nest Describe and Context blocks. Describe and Context blocks are functionally +//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object +//or method and, within that Describe, outline a number of Contexts. +func Context(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) + return true +} + +//You can focus the tests within a describe block using FContext +func FContext(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) + return true +} + +//You can mark the tests within a describe block as pending using PContext +func PContext(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) + return true +} + +//You can mark the tests within a describe block as pending using XContext +func XContext(text string, body func()) bool { + globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) + return true +} + +//It blocks contain your test code and assertions. You cannot nest any other Ginkgo blocks +//within an It block. +// +//Ginkgo will normally run It blocks synchronously. To perform asynchronous tests, pass a +//function that accepts a Done channel. When you do this, you can also provide an optional timeout. +func It(text string, body interface{}, timeout ...float64) bool { + globalSuite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//You can focus individual Its using FIt +func FIt(text string, body interface{}, timeout ...float64) bool { + globalSuite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//You can mark Its as pending using PIt +func PIt(text string, _ ...interface{}) bool { + globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) + return true +} + +//You can mark Its as pending using XIt +func XIt(text string, _ ...interface{}) bool { + globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) + return true +} + +//By allows you to better document large Its. +// +//Generally you should try to keep your Its short and to the point. This is not always possible, however, +//especially in the context of integration tests that capture a particular workflow. +// +//By allows you to document such flows. By must be called within a runnable node (It, BeforeEach, Measure, etc...) +//By will simply log the passed in text to the GinkgoWriter. If By is handed a function it will immediately run the function. +func By(text string, callbacks ...func()) { + preamble := "\x1b[1mSTEP\x1b[0m" + if config.DefaultReporterConfig.NoColor { + preamble = "STEP" + } + fmt.Fprintln(GinkgoWriter, preamble+": "+text) + if len(callbacks) == 1 { + callbacks[0]() + } + if len(callbacks) > 1 { + panic("just one callback per By, please") + } +} + +//Measure blocks run the passed in body function repeatedly (determined by the samples argument) +//and accumulate metrics provided to the Benchmarker by the body function. +// +//The body function must have the signature: +// func(b Benchmarker) +func Measure(text string, body interface{}, samples int) bool { + globalSuite.PushMeasureNode(text, body, types.FlagTypeNone, codelocation.New(1), samples) + return true +} + +//You can focus individual Measures using FMeasure +func FMeasure(text string, body interface{}, samples int) bool { + globalSuite.PushMeasureNode(text, body, types.FlagTypeFocused, codelocation.New(1), samples) + return true +} + +//You can mark Maeasurements as pending using PMeasure +func PMeasure(text string, _ ...interface{}) bool { + globalSuite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) + return true +} + +//You can mark Maeasurements as pending using XMeasure +func XMeasure(text string, _ ...interface{}) bool { + globalSuite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) + return true +} + +//BeforeSuite blocks are run just once before any specs are run. When running in parallel, each +//parallel node process will call BeforeSuite. +// +//BeforeSuite blocks can be made asynchronous by providing a body function that accepts a Done channel +// +//You may only register *one* BeforeSuite handler per test suite. You typically do so in your bootstrap file at the top level. +func BeforeSuite(body interface{}, timeout ...float64) bool { + globalSuite.SetBeforeSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//AfterSuite blocks are *always* run after all the specs regardless of whether specs have passed or failed. +//Moreover, if Ginkgo receives an interrupt signal (^C) it will attempt to run the AfterSuite before exiting. +// +//When running in parallel, each parallel node process will call AfterSuite. +// +//AfterSuite blocks can be made asynchronous by providing a body function that accepts a Done channel +// +//You may only register *one* AfterSuite handler per test suite. You typically do so in your bootstrap file at the top level. +func AfterSuite(body interface{}, timeout ...float64) bool { + globalSuite.SetAfterSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//SynchronizedBeforeSuite blocks are primarily meant to solve the problem of setting up singleton external resources shared across +//nodes when running tests in parallel. For example, say you have a shared database that you can only start one instance of that +//must be used in your tests. When running in parallel, only one node should set up the database and all other nodes should wait +//until that node is done before running. +// +//SynchronizedBeforeSuite accomplishes this by taking *two* function arguments. The first is only run on parallel node #1. The second is +//run on all nodes, but *only* after the first function completes succesfully. Ginkgo also makes it possible to send data from the first function (on Node 1) +//to the second function (on all the other nodes). +// +//The functions have the following signatures. The first function (which only runs on node 1) has the signature: +// +// func() []byte +// +//or, to run asynchronously: +// +// func(done Done) []byte +// +//The byte array returned by the first function is then passed to the second function, which has the signature: +// +// func(data []byte) +// +//or, to run asynchronously: +// +// func(data []byte, done Done) +// +//Here's a simple pseudo-code example that starts a shared database on Node 1 and shares the database's address with the other nodes: +// +// var dbClient db.Client +// var dbRunner db.Runner +// +// var _ = SynchronizedBeforeSuite(func() []byte { +// dbRunner = db.NewRunner() +// err := dbRunner.Start() +// Ω(err).ShouldNot(HaveOccurred()) +// return []byte(dbRunner.URL) +// }, func(data []byte) { +// dbClient = db.NewClient() +// err := dbClient.Connect(string(data)) +// Ω(err).ShouldNot(HaveOccurred()) +// }) +func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, timeout ...float64) bool { + globalSuite.SetSynchronizedBeforeSuiteNode( + node1Body, + allNodesBody, + codelocation.New(1), + parseTimeout(timeout...), + ) + return true +} + +//SynchronizedAfterSuite blocks complement the SynchronizedBeforeSuite blocks in solving the problem of setting up +//external singleton resources shared across nodes when running tests in parallel. +// +//SynchronizedAfterSuite accomplishes this by taking *two* function arguments. The first runs on all nodes. The second runs only on parallel node #1 +//and *only* after all other nodes have finished and exited. This ensures that node 1, and any resources it is running, remain alive until +//all other nodes are finished. +// +//Both functions have the same signature: either func() or func(done Done) to run asynchronously. +// +//Here's a pseudo-code example that complements that given in SynchronizedBeforeSuite. Here, SynchronizedAfterSuite is used to tear down the shared database +//only after all nodes have finished: +// +// var _ = SynchronizedAfterSuite(func() { +// dbClient.Cleanup() +// }, func() { +// dbRunner.Stop() +// }) +func SynchronizedAfterSuite(allNodesBody interface{}, node1Body interface{}, timeout ...float64) bool { + globalSuite.SetSynchronizedAfterSuiteNode( + allNodesBody, + node1Body, + codelocation.New(1), + parseTimeout(timeout...), + ) + return true +} + +//BeforeEach blocks are run before It blocks. When multiple BeforeEach blocks are defined in nested +//Describe and Context blocks the outermost BeforeEach blocks are run first. +// +//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts +//a Done channel +func BeforeEach(body interface{}, timeout ...float64) bool { + globalSuite.PushBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//JustBeforeEach blocks are run before It blocks but *after* all BeforeEach blocks. For more details, +//read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_) +// +//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts +//a Done channel +func JustBeforeEach(body interface{}, timeout ...float64) bool { + globalSuite.PushJustBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +//AfterEach blocks are run after It blocks. When multiple AfterEach blocks are defined in nested +//Describe and Context blocks the innermost AfterEach blocks are run first. +// +//Like It blocks, AfterEach blocks can be made asynchronous by providing a body function that accepts +//a Done channel +func AfterEach(body interface{}, timeout ...float64) bool { + globalSuite.PushAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...)) + return true +} + +func parseTimeout(timeout ...float64) time.Duration { + if len(timeout) == 0 { + return time.Duration(defaultTimeout * int64(time.Second)) + } else { + return time.Duration(timeout[0] * float64(time.Second)) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/convert_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/convert_test.go new file mode 100644 index 00000000000..f4fd678c5f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/convert_test.go @@ -0,0 +1,121 @@ +package integration_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ginkgo convert", func() { + var tmpDir string + + readConvertedFileNamed := func(pathComponents ...string) string { + pathToFile := filepath.Join(tmpDir, "convert_fixtures", filepath.Join(pathComponents...)) + bytes, err := ioutil.ReadFile(pathToFile) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + return string(bytes) + } + + readGoldMasterNamed := func(filename string) string { + bytes, err := ioutil.ReadFile(filepath.Join("_fixtures", "convert_goldmasters", filename)) + Ω(err).ShouldNot(HaveOccurred()) + + return string(bytes) + } + + BeforeEach(func() { + var err error + + tmpDir, err = ioutil.TempDir("", "ginkgo-convert") + Ω(err).ShouldNot(HaveOccurred()) + + err = exec.Command("cp", "-r", filepath.Join("_fixtures", "convert_fixtures"), tmpDir).Run() + Ω(err).ShouldNot(HaveOccurred()) + }) + + JustBeforeEach(func() { + cwd, err := os.Getwd() + Ω(err).ShouldNot(HaveOccurred()) + + relPath, err := filepath.Rel(cwd, filepath.Join(tmpDir, "convert_fixtures")) + Ω(err).ShouldNot(HaveOccurred()) + + cmd := exec.Command(pathToGinkgo, "convert", relPath) + cmd.Env = os.Environ() + for i, env := range cmd.Env { + if strings.HasPrefix(env, "PATH") { + cmd.Env[i] = cmd.Env[i] + ":" + filepath.Dir(pathToGinkgo) + break + } + } + err = cmd.Run() + Ω(err).ShouldNot(HaveOccurred()) + }) + + AfterEach(func() { + err := os.RemoveAll(tmpDir) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("rewrites xunit tests as ginkgo tests", func() { + convertedFile := readConvertedFileNamed("xunit_test.go") + goldMaster := readGoldMasterNamed("xunit_test.go") + Ω(convertedFile).Should(Equal(goldMaster)) + }) + + It("rewrites all usages of *testing.T as mr.T()", func() { + convertedFile := readConvertedFileNamed("extra_functions_test.go") + goldMaster := readGoldMasterNamed("extra_functions_test.go") + Ω(convertedFile).Should(Equal(goldMaster)) + }) + + It("rewrites tests in the package dir that belong to other packages", func() { + convertedFile := readConvertedFileNamed("outside_package_test.go") + goldMaster := readGoldMasterNamed("outside_package_test.go") + Ω(convertedFile).Should(Equal(goldMaster)) + }) + + It("rewrites tests in nested packages", func() { + convertedFile := readConvertedFileNamed("nested", "nested_test.go") + goldMaster := readGoldMasterNamed("nested_test.go") + Ω(convertedFile).Should(Equal(goldMaster)) + }) + + Context("ginkgo test suite files", func() { + It("creates a ginkgo test suite file for the package you specified", func() { + testsuite := readConvertedFileNamed("convert_fixtures_suite_test.go") + goldMaster := readGoldMasterNamed("suite_test.go") + Ω(testsuite).Should(Equal(goldMaster)) + }) + + It("converts go tests in deeply nested packages (some may not contain go files)", func() { + testsuite := readConvertedFileNamed("nested_without_gofiles", "subpackage", "nested_subpackage_test.go") + goldMaster := readGoldMasterNamed("nested_subpackage_test.go") + Ω(testsuite).Should(Equal(goldMaster)) + }) + + It("creates ginkgo test suites for all nested packages", func() { + testsuite := readConvertedFileNamed("nested", "nested_suite_test.go") + goldMaster := readGoldMasterNamed("nested_suite_test.go") + Ω(testsuite).Should(Equal(goldMaster)) + }) + }) + + Context("with an existing test suite file", func() { + BeforeEach(func() { + goldMaster := readGoldMasterNamed("fixtures_suite_test.go") + err := ioutil.WriteFile(filepath.Join(tmpDir, "convert_fixtures", "tmp_suite_test.go"), []byte(goldMaster), 0600) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("gracefully handles existing test suite files", func() { + //nothing should have gone wrong! + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/coverage_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/coverage_test.go new file mode 100644 index 00000000000..0ba00a9857f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/coverage_test.go @@ -0,0 +1,34 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "os" + "os/exec" +) + +var _ = Describe("Coverage Specs", func() { + AfterEach(func() { + os.RemoveAll("./_fixtures/coverage_fixture/coverage_fixture.coverprofile") + }) + + It("runs coverage analysis in series and in parallel", func() { + session := startGinkgo("./_fixtures/coverage_fixture", "-cover") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + Ω(output).Should(ContainSubstring("coverage: 80.0% of statements")) + + serialCoverProfileOutput, err := exec.Command("go", "tool", "cover", "-func=./_fixtures/coverage_fixture/coverage_fixture.coverprofile").CombinedOutput() + Ω(err).ShouldNot(HaveOccurred()) + + os.RemoveAll("./_fixtures/coverage_fixture/coverage_fixture.coverprofile") + + Eventually(startGinkgo("./_fixtures/coverage_fixture", "-cover", "-nodes=4")).Should(gexec.Exit(0)) + + parallelCoverProfileOutput, err := exec.Command("go", "tool", "cover", "-func=./_fixtures/coverage_fixture/coverage_fixture.coverprofile").CombinedOutput() + Ω(err).ShouldNot(HaveOccurred()) + + Ω(parallelCoverProfileOutput).Should(Equal(serialCoverProfileOutput)) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/fail_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/fail_test.go new file mode 100644 index 00000000000..8dcf5e4ad92 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/fail_test.go @@ -0,0 +1,48 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Failing Specs", func() { + var pathToTest string + + BeforeEach(func() { + pathToTest = tmpPath("failing") + copyIn("fail_fixture", pathToTest) + }) + + It("should fail in all the possible ways", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) + + Ω(output).Should(ContainSubstring("a top level failure on line 9")) + Ω(output).Should(ContainSubstring("fail_fixture_test.go:9")) + Ω(output).Should(ContainSubstring("an async top level failure on line 14")) + Ω(output).Should(ContainSubstring("fail_fixture_test.go:14")) + Ω(output).Should(ContainSubstring("a top level goroutine failure on line 21")) + Ω(output).Should(ContainSubstring("fail_fixture_test.go:21")) + + Ω(output).Should(ContainSubstring("a sync failure")) + Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a sync panic`)) + Ω(output).Should(ContainSubstring("a sync FAIL failure")) + Ω(output).Should(ContainSubstring("async timeout [It]")) + Ω(output).Should(ContainSubstring("Timed out")) + Ω(output).Should(ContainSubstring("an async failure")) + Ω(output).Should(MatchRegexp(`Test Panicked\n\s+an async panic`)) + Ω(output).Should(ContainSubstring("an async FAIL failure")) + Ω(output).Should(ContainSubstring("a goroutine FAIL failure")) + Ω(output).Should(ContainSubstring("a goroutine failure")) + Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a goroutine panic`)) + Ω(output).Should(ContainSubstring("a measure failure")) + Ω(output).Should(ContainSubstring("a measure FAIL failure")) + Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a measure panic`)) + + Ω(output).Should(ContainSubstring("0 Passed | 16 Failed")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/flags_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/flags_test.go new file mode 100644 index 00000000000..0a23f5964d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/flags_test.go @@ -0,0 +1,176 @@ +package integration_test + +import ( + "os" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Flags Specs", func() { + var pathToTest string + + BeforeEach(func() { + pathToTest = tmpPath("flags") + copyIn("flags_tests", pathToTest) + }) + + getRandomOrders := func(output string) []int { + return []int{strings.Index(output, "RANDOM_A"), strings.Index(output, "RANDOM_B"), strings.Index(output, "RANDOM_C")} + } + + It("normally passes, runs measurements, prints out noisy pendings, does not randomize tests, and honors the programmatic focus", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Ran 3 samples:"), "has a measurement") + Ω(output).Should(ContainSubstring("10 Passed")) + Ω(output).Should(ContainSubstring("0 Failed")) + Ω(output).Should(ContainSubstring("1 Pending")) + Ω(output).Should(ContainSubstring("2 Skipped")) + Ω(output).Should(ContainSubstring("[PENDING]")) + Ω(output).Should(ContainSubstring("marshmallow")) + Ω(output).Should(ContainSubstring("chocolate")) + Ω(output).Should(ContainSubstring("CUSTOM_FLAG: default")) + Ω(output).Should(ContainSubstring("Detected Programmatic Focus - setting exit status to %d", types.GINKGO_FOCUS_EXIT_CODE)) + Ω(output).ShouldNot(ContainSubstring("smores")) + Ω(output).ShouldNot(ContainSubstring("SLOW TEST")) + Ω(output).ShouldNot(ContainSubstring("should honor -slowSpecThreshold")) + + orders := getRandomOrders(output) + Ω(orders[0]).Should(BeNumerically("<", orders[1])) + Ω(orders[1]).Should(BeNumerically("<", orders[2])) + }) + + It("should run a coverprofile when passed -cover", func() { + session := startGinkgo(pathToTest, "--noColor", "--cover", "--focus=the focused set") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + _, err := os.Stat(filepath.Join(pathToTest, "flags.coverprofile")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(output).Should(ContainSubstring("coverage: ")) + }) + + It("should fail when there are pending tests and it is passed --failOnPending", func() { + session := startGinkgo(pathToTest, "--noColor", "--failOnPending") + Eventually(session).Should(gexec.Exit(1)) + }) + + It("should not print out pendings when --noisyPendings=false", func() { + session := startGinkgo(pathToTest, "--noColor", "--noisyPendings=false") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).ShouldNot(ContainSubstring("[PENDING]")) + Ω(output).Should(ContainSubstring("1 Pending")) + }) + + It("should override the programmatic focus when told to focus", func() { + session := startGinkgo(pathToTest, "--noColor", "--focus=smores") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("marshmallow")) + Ω(output).Should(ContainSubstring("chocolate")) + Ω(output).Should(ContainSubstring("smores")) + Ω(output).Should(ContainSubstring("3 Passed")) + Ω(output).Should(ContainSubstring("0 Failed")) + Ω(output).Should(ContainSubstring("0 Pending")) + Ω(output).Should(ContainSubstring("10 Skipped")) + }) + + It("should override the programmatic focus when told to skip", func() { + session := startGinkgo(pathToTest, "--noColor", "--skip=marshmallow|failing") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).ShouldNot(ContainSubstring("marshmallow")) + Ω(output).Should(ContainSubstring("chocolate")) + Ω(output).Should(ContainSubstring("smores")) + Ω(output).Should(ContainSubstring("10 Passed")) + Ω(output).Should(ContainSubstring("0 Failed")) + Ω(output).Should(ContainSubstring("1 Pending")) + Ω(output).Should(ContainSubstring("2 Skipped")) + }) + + It("should run the race detector when told to", func() { + session := startGinkgo(pathToTest, "--noColor", "--race") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("WARNING: DATA RACE")) + }) + + It("should randomize tests when told to", func() { + session := startGinkgo(pathToTest, "--noColor", "--randomizeAllSpecs", "--seed=21") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + orders := getRandomOrders(output) + Ω(orders[0]).ShouldNot(BeNumerically("<", orders[1])) + }) + + It("should skip measurements when told to", func() { + session := startGinkgo(pathToTest, "--skipMeasurements") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).ShouldNot(ContainSubstring("Ran 3 samples:"), "has a measurement") + Ω(output).Should(ContainSubstring("3 Skipped")) + }) + + It("should watch for slow specs", func() { + session := startGinkgo(pathToTest, "--slowSpecThreshold=0.05") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("SLOW TEST")) + Ω(output).Should(ContainSubstring("should honor -slowSpecThreshold")) + }) + + It("should pass additional arguments in", func() { + session := startGinkgo(pathToTest, "--", "--customFlag=madagascar") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("CUSTOM_FLAG: madagascar")) + }) + + It("should print out full stack traces for failures when told to", func() { + session := startGinkgo(pathToTest, "--focus=a failing test", "--trace") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Full Stack Trace")) + }) + + It("should fail fast when told to", func() { + pathToTest = tmpPath("fail") + copyIn("fail_fixture", pathToTest) + session := startGinkgo(pathToTest, "--failFast") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("1 Failed")) + Ω(output).Should(ContainSubstring("15 Skipped")) + }) + + It("should perform a dry run when told to", func() { + pathToTest = tmpPath("fail") + copyIn("fail_fixture", pathToTest) + session := startGinkgo(pathToTest, "--dryRun", "-v") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("synchronous failures")) + Ω(output).Should(ContainSubstring("16 Specs")) + Ω(output).Should(ContainSubstring("0 Passed")) + Ω(output).Should(ContainSubstring("0 Failed")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration.go new file mode 100644 index 00000000000..76ab1b7282d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration.go @@ -0,0 +1 @@ +package integration diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration_suite_test.go new file mode 100644 index 00000000000..0ad407c8453 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/integration_suite_test.go @@ -0,0 +1,89 @@ +package integration_test + +import ( + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + + "testing" + "time" +) + +var tmpDir string +var pathToGinkgo string + +func TestIntegration(t *testing.T) { + SetDefaultEventuallyTimeout(15 * time.Second) + RegisterFailHandler(Fail) + RunSpecs(t, "Integration Suite") +} + +var _ = SynchronizedBeforeSuite(func() []byte { + pathToGinkgo, err := gexec.Build("github.com/onsi/ginkgo/ginkgo") + Ω(err).ShouldNot(HaveOccurred()) + return []byte(pathToGinkgo) +}, func(computedPathToGinkgo []byte) { + pathToGinkgo = string(computedPathToGinkgo) +}) + +var _ = BeforeEach(func() { + var err error + tmpDir, err = ioutil.TempDir("", "ginkgo-run") + Ω(err).ShouldNot(HaveOccurred()) +}) + +var _ = AfterEach(func() { + err := os.RemoveAll(tmpDir) + Ω(err).ShouldNot(HaveOccurred()) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() +}) + +func tmpPath(destination string) string { + return filepath.Join(tmpDir, destination) +} + +func copyIn(fixture string, destination string) { + err := os.MkdirAll(destination, 0777) + Ω(err).ShouldNot(HaveOccurred()) + + filepath.Walk(filepath.Join("_fixtures", fixture), func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + base := filepath.Base(path) + + src, err := os.Open(path) + Ω(err).ShouldNot(HaveOccurred()) + + dst, err := os.Create(filepath.Join(destination, base)) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = io.Copy(dst, src) + Ω(err).ShouldNot(HaveOccurred()) + return nil + }) +} + +func ginkgoCommand(dir string, args ...string) *exec.Cmd { + cmd := exec.Command(pathToGinkgo, args...) + cmd.Dir = dir + + return cmd +} + +func startGinkgo(dir string, args ...string) *gexec.Session { + cmd := ginkgoCommand(dir, args...) + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + return session +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/interrupt_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/interrupt_test.go new file mode 100644 index 00000000000..dc3bf2842b1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/interrupt_test.go @@ -0,0 +1,51 @@ +package integration_test + +import ( + "os/exec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Interrupt", func() { + var pathToTest string + BeforeEach(func() { + pathToTest = tmpPath("hanging") + copyIn("hanging_suite", pathToTest) + }) + + Context("when interrupting a suite", func() { + var session *gexec.Session + BeforeEach(func() { + //we need to signal the actual process, so we must compile the test first + var err error + cmd := exec.Command("go", "test", "-c") + cmd.Dir = pathToTest + session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + //then run the compiled test directly + cmd = exec.Command("./hanging.test", "--test.v=true", "--ginkgo.noColor") + cmd.Dir = pathToTest + session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gbytes.Say("Sleeping...")) + session.Interrupt() + Eventually(session, 1000).Should(gexec.Exit(1)) + }) + + It("should emit the contents of the GinkgoWriter", func() { + Ω(session).Should(gbytes.Say("Just beginning")) + Ω(session).Should(gbytes.Say("Almost there...")) + Ω(session).Should(gbytes.Say("Hanging Out")) + }) + + It("should run the AfterSuite", func() { + Ω(session).Should(gbytes.Say("Heading Out After Suite")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/precompiled_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/precompiled_test.go new file mode 100644 index 00000000000..d9b78e0b275 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/precompiled_test.go @@ -0,0 +1,53 @@ +package integration_test + +import ( + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("ginkgo build", func() { + var pathToTest string + + BeforeEach(func() { + pathToTest = tmpPath("passing_ginkgo_tests") + copyIn("passing_ginkgo_tests", pathToTest) + session := startGinkgo(pathToTest, "build") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + Ω(output).Should(ContainSubstring("Compiling passing_ginkgo_tests")) + Ω(output).Should(ContainSubstring("compiled passing_ginkgo_tests.test")) + }) + + It("should build a test binary", func() { + _, err := os.Stat(filepath.Join(pathToTest, "passing_ginkgo_tests.test")) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should be possible to run the test binary directly", func() { + cmd := exec.Command("./passing_ginkgo_tests.test") + cmd.Dir = pathToTest + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Ω(session).Should(gbytes.Say("Running Suite: Passing_ginkgo_tests Suite")) + }) + + It("should be possible to run the test binary via ginkgo", func() { + session := startGinkgo(pathToTest, "./passing_ginkgo_tests.test") + Eventually(session).Should(gexec.Exit(0)) + Ω(session).Should(gbytes.Say("Running Suite: Passing_ginkgo_tests Suite")) + }) + + It("should be possible to run the test binary in parallel", func() { + session := startGinkgo(pathToTest, "--nodes=4", "--noColor", "./passing_ginkgo_tests.test") + Eventually(session).Should(gexec.Exit(0)) + Ω(session).Should(gbytes.Say("Running Suite: Passing_ginkgo_tests Suite")) + Ω(session).Should(gbytes.Say("Running in parallel across 4 nodes")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/progress_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/progress_test.go new file mode 100644 index 00000000000..8589c338a84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/progress_test.go @@ -0,0 +1,75 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Emitting progress", func() { + var pathToTest string + var session *gexec.Session + var args []string + + BeforeEach(func() { + args = []string{"--noColor"} + pathToTest = tmpPath("progress") + copyIn("progress_fixture", pathToTest) + }) + + JustBeforeEach(func() { + session = startGinkgo(pathToTest, args...) + Eventually(session).Should(gexec.Exit(0)) + }) + + Context("with the -progress flag, but no -v flag", func() { + BeforeEach(func() { + args = append(args, "-progress") + }) + + It("should not emit progress", func() { + Ω(session).ShouldNot(gbytes.Say("[bB]efore")) + }) + }) + + Context("with the -v flag", func() { + BeforeEach(func() { + args = append(args, "-v") + }) + + It("should not emit progress", func() { + Ω(session).ShouldNot(gbytes.Say(`\[BeforeEach\]`)) + Ω(session).Should(gbytes.Say(`>outer before<`)) + }) + }) + + Context("with the -progress flag and the -v flag", func() { + BeforeEach(func() { + args = append(args, "-progress", "-v") + }) + + It("should emit progress (by writing to the GinkgoWriter)", func() { + Ω(session).Should(gbytes.Say(`\[BeforeEach\] ProgressFixture`)) + Ω(session).Should(gbytes.Say(`>outer before<`)) + + Ω(session).Should(gbytes.Say(`\[BeforeEach\] Inner Context`)) + Ω(session).Should(gbytes.Say(`>inner before<`)) + + Ω(session).Should(gbytes.Say(`\[JustBeforeEach\] ProgressFixture`)) + Ω(session).Should(gbytes.Say(`>outer just before<`)) + + Ω(session).Should(gbytes.Say(`\[JustBeforeEach\] Inner Context`)) + Ω(session).Should(gbytes.Say(`>inner just before<`)) + + Ω(session).Should(gbytes.Say(`\[It\] should emit progress as it goes`)) + Ω(session).Should(gbytes.Say(`>it<`)) + + Ω(session).Should(gbytes.Say(`\[AfterEach\] Inner Context`)) + Ω(session).Should(gbytes.Say(`>inner after<`)) + + Ω(session).Should(gbytes.Say(`\[AfterEach\] ProgressFixture`)) + Ω(session).Should(gbytes.Say(`>outer after<`)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/run_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/run_test.go new file mode 100644 index 00000000000..5d2e924d9e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/run_test.go @@ -0,0 +1,373 @@ +package integration_test + +import ( + "runtime" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Running Specs", func() { + var pathToTest string + + Context("when pointed at the current directory", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + }) + + It("should run the tests in the working directory", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("••••")) + Ω(output).Should(ContainSubstring("SUCCESS! -- 4 Passed")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when passed an explicit package to run", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + }) + + It("should run the ginkgo style tests", func() { + session := startGinkgo(tmpDir, "--noColor", pathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("••••")) + Ω(output).Should(ContainSubstring("SUCCESS! -- 4 Passed")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when passed a number of packages to run", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + otherPathToTest := tmpPath("other") + copyIn("passing_ginkgo_tests", pathToTest) + copyIn("more_ginkgo_tests", otherPathToTest) + }) + + It("should run the ginkgo style tests", func() { + session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "ginkgo", "./other") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Running Suite: More_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when passed a number of packages to run, some of which have focused tests", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + otherPathToTest := tmpPath("other") + focusedPathToTest := tmpPath("focused") + copyIn("passing_ginkgo_tests", pathToTest) + copyIn("more_ginkgo_tests", otherPathToTest) + copyIn("focused_fixture", focusedPathToTest) + }) + + It("should exit with a status code of 2 and explain why", func() { + session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "-r") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Running Suite: More_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + Ω(output).Should(ContainSubstring("Detected Programmatic Focus - setting exit status to %d", types.GINKGO_FOCUS_EXIT_CODE)) + }) + }) + + Context("when told to skipPackages", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + otherPathToTest := tmpPath("other") + focusedPathToTest := tmpPath("focused") + copyIn("passing_ginkgo_tests", pathToTest) + copyIn("more_ginkgo_tests", otherPathToTest) + copyIn("focused_fixture", focusedPathToTest) + }) + + It("should skip packages that match the list", func() { + session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Passing_ginkgo_tests Suite")) + Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) + Ω(output).ShouldNot(ContainSubstring("Focused_fixture Suite")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + + Context("when all packages are skipped", func() { + It("should not run anything, but still exit 0", func() { + session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused,ginkgo", "-r") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("All tests skipped!")) + Ω(output).ShouldNot(ContainSubstring("Passing_ginkgo_tests Suite")) + Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) + Ω(output).ShouldNot(ContainSubstring("Focused_fixture Suite")) + Ω(output).ShouldNot(ContainSubstring("Test Suite Passed")) + }) + }) + }) + + Context("when there are no tests to run", func() { + It("should exit 1", func() { + session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Err.Contents()) + + Ω(output).Should(ContainSubstring("Found no test suites")) + }) + }) + + Context("when told to randomizeSuites", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + otherPathToTest := tmpPath("other") + copyIn("passing_ginkgo_tests", pathToTest) + copyIn("more_ginkgo_tests", otherPathToTest) + }) + + It("should skip packages that match the regexp", func() { + session := startGinkgo(tmpDir, "--noColor", "--randomizeSuites", "-r", "--seed=2") + Eventually(session).Should(gexec.Exit(0)) + + Ω(session).Should(gbytes.Say("More_ginkgo_tests Suite")) + Ω(session).Should(gbytes.Say("Passing_ginkgo_tests Suite")) + + session = startGinkgo(tmpDir, "--noColor", "--randomizeSuites", "-r", "--seed=3") + Eventually(session).Should(gexec.Exit(0)) + + Ω(session).Should(gbytes.Say("Passing_ginkgo_tests Suite")) + Ω(session).Should(gbytes.Say("More_ginkgo_tests Suite")) + }) + }) + + Context("when pointed at a package with xunit style tests", func() { + BeforeEach(func() { + pathToTest = tmpPath("xunit") + copyIn("xunit_tests", pathToTest) + }) + + It("should run the xunit style tests", func() { + session := startGinkgo(pathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("--- PASS: TestAlwaysTrue")) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when pointed at a package with no tests", func() { + BeforeEach(func() { + pathToTest = tmpPath("no_tests") + copyIn("no_tests", pathToTest) + }) + + It("should fail", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(1)) + + Ω(session.Err.Contents()).Should(ContainSubstring("Found no test suites")) + }) + }) + + Context("when pointed at a package that fails to compile", func() { + BeforeEach(func() { + pathToTest = tmpPath("does_not_compile") + copyIn("does_not_compile", pathToTest) + }) + + It("should fail", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Failed to compile")) + }) + }) + + Context("when running in parallel", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + }) + + Context("with a specific number of -nodes", func() { + It("should use the specified number of nodes", func() { + session := startGinkgo(pathToTest, "--noColor", "-succinct", "-nodes=2") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs - 2 nodes •••• SUCCESS! [\d.mus]+`)) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("with -p", func() { + It("it should autocompute the number of nodes", func() { + session := startGinkgo(pathToTest, "--noColor", "-succinct", "-p") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + nodes := runtime.NumCPU() + if nodes > 4 { + nodes = nodes - 1 + } + Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs - %d nodes •••• SUCCESS! [\d.mus]+`, nodes)) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + }) + + Context("when streaming in parallel", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + }) + + It("should print output in realtime", func() { + session := startGinkgo(pathToTest, "--noColor", "-stream", "-nodes=2") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring(`[1] Parallel test node 1/2.`)) + Ω(output).Should(ContainSubstring(`[2] Parallel test node 2/2.`)) + Ω(output).Should(ContainSubstring(`[1] SUCCESS!`)) + Ω(output).Should(ContainSubstring(`[2] SUCCESS!`)) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when running recursively", func() { + BeforeEach(func() { + passingTest := tmpPath("A") + otherPassingTest := tmpPath("E") + copyIn("passing_ginkgo_tests", passingTest) + copyIn("more_ginkgo_tests", otherPassingTest) + }) + + Context("when all the tests pass", func() { + It("should run all the tests (in succinct mode) and succeed", func() { + session := startGinkgo(tmpDir, "--noColor", "-r") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + outputLines := strings.Split(output, "\n") + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs •••• SUCCESS! [\d.mus]+ PASS`)) + Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs •• SUCCESS! [\d.mus]+ PASS`)) + Ω(output).Should(ContainSubstring("Test Suite Passed")) + }) + }) + + Context("when one of the packages has a failing tests", func() { + BeforeEach(func() { + failingTest := tmpPath("C") + copyIn("failing_ginkgo_tests", failingTest) + }) + + It("should fail and stop running tests", func() { + session := startGinkgo(tmpDir, "--noColor", "-r") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + outputLines := strings.Split(output, "\n") + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs •••• SUCCESS! [\d.mus]+ PASS`)) + Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] Failing_ginkgo_tests Suite - 2/2 specs`)) + Ω(output).Should(ContainSubstring("• Failure")) + Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Test Suite Failed")) + + Ω(output).Should(ContainSubstring("Summarizing 1 Failure:")) + Ω(output).Should(ContainSubstring("[Fail] FailingGinkgoTests [It] should fail")) + }) + }) + + Context("when one of the packages fails to compile", func() { + BeforeEach(func() { + doesNotCompileTest := tmpPath("C") + copyIn("does_not_compile", doesNotCompileTest) + }) + + It("should fail and stop running tests", func() { + session := startGinkgo(tmpDir, "--noColor", "-r") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + outputLines := strings.Split(output, "\n") + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs •••• SUCCESS! [\d.mus]+ PASS`)) + Ω(outputLines[1]).Should(ContainSubstring("Failed to compile C:")) + Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Test Suite Failed")) + }) + }) + + Context("when either is the case, but the keepGoing flag is set", func() { + BeforeEach(func() { + doesNotCompileTest := tmpPath("B") + copyIn("does_not_compile", doesNotCompileTest) + + failingTest := tmpPath("C") + copyIn("failing_ginkgo_tests", failingTest) + }) + + It("should soldier on", func() { + session := startGinkgo(tmpDir, "--noColor", "-r", "-keepGoing") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + outputLines := strings.Split(output, "\n") + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs •••• SUCCESS! [\d.mus]+ PASS`)) + Ω(outputLines[1]).Should(ContainSubstring("Failed to compile B:")) + Ω(output).Should(MatchRegexp(`\[\d+\] Failing_ginkgo_tests Suite - 2/2 specs`)) + Ω(output).Should(ContainSubstring("• Failure")) + Ω(output).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs •• SUCCESS! [\d.mus]+ PASS`)) + Ω(output).Should(ContainSubstring("Test Suite Failed")) + }) + }) + }) + + Context("when told to keep going --untilItFails", func() { + BeforeEach(func() { + copyIn("eventually_failing", tmpDir) + }) + + It("should keep rerunning the tests, until a failure occurs", func() { + session := startGinkgo(tmpDir, "--untilItFails", "--noColor") + Eventually(session).Should(gexec.Exit(1)) + Ω(session).Should(gbytes.Say("This was attempt #1")) + Ω(session).Should(gbytes.Say("This was attempt #2")) + Ω(session).Should(gbytes.Say("Tests failed on attempt #3")) + + //it should change the random seed between each test + lines := strings.Split(string(session.Out.Contents()), "\n") + randomSeeds := []string{} + for _, line := range lines { + if strings.Contains(line, "Random Seed:") { + randomSeeds = append(randomSeeds, strings.Split(line, ": ")[1]) + } + } + Ω(randomSeeds[0]).ShouldNot(Equal(randomSeeds[1])) + Ω(randomSeeds[1]).ShouldNot(Equal(randomSeeds[2])) + Ω(randomSeeds[0]).ShouldNot(Equal(randomSeeds[2])) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/subcommand_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/subcommand_test.go new file mode 100644 index 00000000000..069bb3011e1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/subcommand_test.go @@ -0,0 +1,300 @@ +package integration_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Subcommand", func() { + Describe("ginkgo bootstrap", func() { + It("should generate a bootstrap file, as long as one does not exist", func() { + pkgPath := tmpPath("foo") + os.Mkdir(pkgPath, 0777) + session := startGinkgo(pkgPath, "bootstrap") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_suite_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring("func TestFoo(t *testing.T) {")) + Ω(content).Should(ContainSubstring("RegisterFailHandler")) + Ω(content).Should(ContainSubstring("RunSpecs")) + + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) + + session = startGinkgo(pkgPath, "bootstrap") + Eventually(session).Should(gexec.Exit(1)) + output = session.Out.Contents() + Ω(output).Should(ContainSubstring("foo_suite_test.go already exists")) + }) + + It("should import nodot declarations when told to", func() { + pkgPath := tmpPath("foo") + os.Mkdir(pkgPath, 0777) + session := startGinkgo(pkgPath, "bootstrap", "--nodot") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_suite_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring("func TestFoo(t *testing.T) {")) + Ω(content).Should(ContainSubstring("RegisterFailHandler")) + Ω(content).Should(ContainSubstring("RunSpecs")) + + Ω(content).Should(ContainSubstring("var It = ginkgo.It")) + Ω(content).Should(ContainSubstring("var Ω = gomega.Ω")) + + Ω(content).Should(ContainSubstring("\t" + `"github.com/onsi/ginkgo"`)) + Ω(content).Should(ContainSubstring("\t" + `"github.com/onsi/gomega"`)) + }) + + It("should generate an agouti bootstrap file when told to", func() { + pkgPath := tmpPath("foo") + os.Mkdir(pkgPath, 0777) + session := startGinkgo(pkgPath, "bootstrap", "--agouti") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_suite_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring("func TestFoo(t *testing.T) {")) + Ω(content).Should(ContainSubstring("RegisterFailHandler")) + Ω(content).Should(ContainSubstring("RunSpecs")) + + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/sclevine/agouti/core"`)) + }) + }) + + Describe("nodot", func() { + It("should update the declarations in the bootstrap file", func() { + pkgPath := tmpPath("foo") + os.Mkdir(pkgPath, 0777) + + session := startGinkgo(pkgPath, "bootstrap", "--nodot") + Eventually(session).Should(gexec.Exit(0)) + + byteContent, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + + content := string(byteContent) + content = strings.Replace(content, "var It =", "var MyIt =", -1) + content = strings.Replace(content, "var Ω = gomega.Ω\n", "", -1) + + err = ioutil.WriteFile(filepath.Join(pkgPath, "foo_suite_test.go"), []byte(content), os.ModePerm) + Ω(err).ShouldNot(HaveOccurred()) + + session = startGinkgo(pkgPath, "nodot") + Eventually(session).Should(gexec.Exit(0)) + + byteContent, err = ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(byteContent).Should(ContainSubstring("var MyIt = ginkgo.It")) + Ω(byteContent).ShouldNot(ContainSubstring("var It = ginkgo.It")) + Ω(byteContent).Should(ContainSubstring("var Ω = gomega.Ω")) + }) + }) + + Describe("ginkgo generate", func() { + var pkgPath string + + BeforeEach(func() { + pkgPath = tmpPath("foo_bar") + os.Mkdir(pkgPath, 0777) + }) + + Context("with no arguments", func() { + It("should generate a test file named after the package", func() { + session := startGinkgo(pkgPath, "generate") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_bar_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_bar_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("FooBar", func() {`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) + + session = startGinkgo(pkgPath, "generate") + Eventually(session).Should(gexec.Exit(1)) + output = session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_bar_test.go already exists")) + }) + }) + + Context("with an argument of the form: foo", func() { + It("should generate a test file named after the argument", func() { + session := startGinkgo(pkgPath, "generate", "baz_buzz") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("baz_buzz_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "baz_buzz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("BazBuzz", func() {`)) + }) + }) + + Context("with an argument of the form: foo.go", func() { + It("should generate a test file named after the argument", func() { + session := startGinkgo(pkgPath, "generate", "baz_buzz.go") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("baz_buzz_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "baz_buzz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("BazBuzz", func() {`)) + + }) + }) + + Context("with an argument of the form: foo_test", func() { + It("should generate a test file named after the argument", func() { + session := startGinkgo(pkgPath, "generate", "baz_buzz_test") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("baz_buzz_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "baz_buzz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("BazBuzz", func() {`)) + }) + }) + + Context("with an argument of the form: foo_test.go", func() { + It("should generate a test file named after the argument", func() { + session := startGinkgo(pkgPath, "generate", "baz_buzz_test.go") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("baz_buzz_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "baz_buzz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("BazBuzz", func() {`)) + }) + }) + + Context("with multiple arguments", func() { + It("should generate a test file named after the argument", func() { + session := startGinkgo(pkgPath, "generate", "baz", "buzz") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("baz_test.go")) + Ω(output).Should(ContainSubstring("buzz_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "baz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("Baz", func() {`)) + + content, err = ioutil.ReadFile(filepath.Join(pkgPath, "buzz_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring(`var _ = Describe("Buzz", func() {`)) + }) + }) + + Context("with nodot", func() { + It("should not import ginkgo or gomega", func() { + session := startGinkgo(pkgPath, "generate", "--nodot") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_bar_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_bar_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).ShouldNot(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) + Ω(content).ShouldNot(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) + }) + }) + + Context("with agouti", func() { + It("should generate an agouti test file", func() { + session := startGinkgo(pkgPath, "generate", "--agouti") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("foo_bar_test.go")) + + content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_bar_test.go")) + Ω(err).ShouldNot(HaveOccurred()) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/sclevine/agouti/core"`)) + Ω(content).Should(ContainSubstring("\t" + `. "github.com/sclevine/agouti/matchers"`)) + Ω(content).Should(ContainSubstring("page, err = agoutiDriver.Page()")) + }) + }) + }) + + Describe("ginkgo blur", func() { + It("should unfocus tests", func() { + pathToTest := tmpPath("focused") + copyIn("focused_fixture", pathToTest) + + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("3 Passed")) + Ω(output).Should(ContainSubstring("3 Skipped")) + + session = startGinkgo(pathToTest, "blur") + Eventually(session).Should(gexec.Exit(0)) + + session = startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output = session.Out.Contents() + Ω(output).Should(ContainSubstring("6 Passed")) + Ω(output).Should(ContainSubstring("0 Skipped")) + }) + }) + + Describe("ginkgo version", func() { + It("should print out the version info", func() { + session := startGinkgo("", "version") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(MatchRegexp(`Ginkgo Version \d+\.\d+\.\d+`)) + }) + }) + + Describe("ginkgo help", func() { + It("should print out usage information", func() { + session := startGinkgo("", "help") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Err.Contents()) + + Ω(output).Should(MatchRegexp(`Ginkgo Version \d+\.\d+\.\d+`)) + Ω(output).Should(ContainSubstring("ginkgo watch")) + Ω(output).Should(ContainSubstring("-succinct")) + Ω(output).Should(ContainSubstring("-nodes")) + Ω(output).Should(ContainSubstring("ginkgo generate")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/suite_setup_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/suite_setup_test.go new file mode 100644 index 00000000000..2b9d9268bb1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/suite_setup_test.go @@ -0,0 +1,177 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "strings" +) + +var _ = Describe("SuiteSetup", func() { + var pathToTest string + + Context("when the BeforeSuite and AfterSuite pass", func() { + BeforeEach(func() { + pathToTest = tmpPath("suite_setup") + copyIn("passing_suite_setup", pathToTest) + }) + + It("should run the BeforeSuite once, then run all the tests", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) + }) + + It("should run the BeforeSuite once per parallel node, then run all the tests", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=2") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) + }) + }) + + Context("when the BeforeSuite fails", func() { + BeforeEach(func() { + pathToTest = tmpPath("suite_setup") + copyIn("failing_before_suite", pathToTest) + }) + + It("should run the BeforeSuite once, none of the tests, but it should run the AfterSuite", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) + Ω(strings.Count(output, "Test Panicked")).Should(Equal(1)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) + Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) + }) + + It("should run the BeforeSuite once per parallel node, none of the tests, but it should run the AfterSuite for each node", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=2") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) + Ω(strings.Count(output, "Test Panicked")).Should(Equal(2)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) + Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) + }) + }) + + Context("when the AfterSuite fails", func() { + BeforeEach(func() { + pathToTest = tmpPath("suite_setup") + copyIn("failing_after_suite", pathToTest) + }) + + It("should run the BeforeSuite once, none of the tests, but it should run the AfterSuite", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) + Ω(strings.Count(output, "Test Panicked")).Should(Equal(1)) + Ω(strings.Count(output, "A TEST")).Should(Equal(2)) + }) + + It("should run the BeforeSuite once per parallel node, none of the tests, but it should run the AfterSuite for each node", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=2") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) + Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) + Ω(strings.Count(output, "Test Panicked")).Should(Equal(2)) + Ω(strings.Count(output, "A TEST")).Should(Equal(2)) + }) + }) + + Context("With passing synchronized before and after suites", func() { + BeforeEach(func() { + pathToTest = tmpPath("suite_setup") + copyIn("synchronized_setup_tests", pathToTest) + }) + + Context("when run with one node", func() { + It("should do all the work on that one node", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("BEFORE_A_1\nBEFORE_B_1: DATA")) + Ω(output).Should(ContainSubstring("AFTER_A_1\nAFTER_B_1")) + }) + }) + + Context("when run across multiple nodes", func() { + It("should run the first BeforeSuite function (BEFORE_A) on node 1, the second (BEFORE_B) on all the nodes, the first AfterSuite (AFTER_A) on all the nodes, and then the second (AFTER_B) on Node 1 *after* everything else is finished", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=3") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("BEFORE_A_1")) + Ω(output).Should(ContainSubstring("BEFORE_B_1: DATA")) + Ω(output).Should(ContainSubstring("BEFORE_B_2: DATA")) + Ω(output).Should(ContainSubstring("BEFORE_B_3: DATA")) + + Ω(output).ShouldNot(ContainSubstring("BEFORE_A_2")) + Ω(output).ShouldNot(ContainSubstring("BEFORE_A_3")) + + Ω(output).Should(ContainSubstring("AFTER_A_1")) + Ω(output).Should(ContainSubstring("AFTER_A_2")) + Ω(output).Should(ContainSubstring("AFTER_A_3")) + Ω(output).Should(ContainSubstring("AFTER_B_1")) + + Ω(output).ShouldNot(ContainSubstring("AFTER_B_2")) + Ω(output).ShouldNot(ContainSubstring("AFTER_B_3")) + }) + }) + + Context("when streaming across multiple nodes", func() { + It("should run the first BeforeSuite function (BEFORE_A) on node 1, the second (BEFORE_B) on all the nodes, the first AfterSuite (AFTER_A) on all the nodes, and then the second (AFTER_B) on Node 1 *after* everything else is finished", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=3", "--stream") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("[1] BEFORE_A_1")) + Ω(output).Should(ContainSubstring("[1] BEFORE_B_1: DATA")) + Ω(output).Should(ContainSubstring("[2] BEFORE_B_2: DATA")) + Ω(output).Should(ContainSubstring("[3] BEFORE_B_3: DATA")) + + Ω(output).ShouldNot(ContainSubstring("BEFORE_A_2")) + Ω(output).ShouldNot(ContainSubstring("BEFORE_A_3")) + + Ω(output).Should(ContainSubstring("[1] AFTER_A_1")) + Ω(output).Should(ContainSubstring("[2] AFTER_A_2")) + Ω(output).Should(ContainSubstring("[3] AFTER_A_3")) + Ω(output).Should(ContainSubstring("[1] AFTER_B_1")) + + Ω(output).ShouldNot(ContainSubstring("AFTER_B_2")) + Ω(output).ShouldNot(ContainSubstring("AFTER_B_3")) + }) + }) + }) + + Context("With a failing synchronized before suite", func() { + BeforeEach(func() { + pathToTest = tmpPath("suite_setup") + copyIn("exiting_synchronized_setup_tests", pathToTest) + }) + + It("should fail and let the user know that node 1 disappeared prematurely", func() { + session := startGinkgo(pathToTest, "--noColor", "--nodes=3") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Node 1 disappeared before completing BeforeSuite")) + Ω(output).Should(ContainSubstring("Ginkgo timed out waiting for all parallel nodes to end")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/tags_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/tags_test.go new file mode 100644 index 00000000000..626635bf5c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/tags_test.go @@ -0,0 +1,27 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Tags", func() { + var pathToTest string + BeforeEach(func() { + pathToTest = tmpPath("tags") + copyIn("tags_tests", pathToTest) + }) + + It("should honor the passed in -tags flag", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + Ω(output).Should(ContainSubstring("Ran 1 of 1 Specs")) + + session = startGinkgo(pathToTest, "--noColor", "-tags=complex_tests") + Eventually(session).Should(gexec.Exit(0)) + output = string(session.Out.Contents()) + Ω(output).Should(ContainSubstring("Ran 3 of 3 Specs")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/verbose_and_succinct_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/verbose_and_succinct_test.go new file mode 100644 index 00000000000..470affdf7c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/verbose_and_succinct_test.go @@ -0,0 +1,80 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Verbose And Succinct Mode", func() { + var pathToTest string + var otherPathToTest string + + Context("when running one package", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + }) + + It("should default to non-succinct mode", func() { + session := startGinkgo(pathToTest, "--noColor") + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + }) + }) + + Context("when running more than one package", func() { + BeforeEach(func() { + pathToTest = tmpPath("ginkgo") + copyIn("passing_ginkgo_tests", pathToTest) + otherPathToTest = tmpPath("more_ginkgo") + copyIn("more_ginkgo_tests", otherPathToTest) + }) + + Context("with no flags set", func() { + It("should default to succinct mode", func() { + session := startGinkgo(pathToTest, "--noColor", pathToTest, otherPathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("] Passing_ginkgo_tests Suite - 4/4 specs •••• SUCCESS!")) + Ω(output).Should(ContainSubstring("] More_ginkgo_tests Suite - 2/2 specs •• SUCCESS!")) + }) + }) + + Context("with --succinct=false", func() { + It("should not be in succinct mode", func() { + session := startGinkgo(pathToTest, "--noColor", "--succinct=false", pathToTest, otherPathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Running Suite: More_ginkgo_tests Suite")) + }) + }) + + Context("with -v", func() { + It("should not be in succinct mode, but should be verbose", func() { + session := startGinkgo(pathToTest, "--noColor", "-v", pathToTest, otherPathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("Running Suite: Passing_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("Running Suite: More_ginkgo_tests Suite")) + Ω(output).Should(ContainSubstring("should proxy strings")) + Ω(output).Should(ContainSubstring("should always pass")) + }) + + It("should emit output from Bys", func() { + session := startGinkgo(pathToTest, "--noColor", "-v", pathToTest) + Eventually(session).Should(gexec.Exit(0)) + output := session.Out.Contents() + + Ω(output).Should(ContainSubstring("emitting one By")) + Ω(output).Should(ContainSubstring("emitting another By")) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/watch_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/watch_test.go new file mode 100644 index 00000000000..bbbcf36aa98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/integration/watch_test.go @@ -0,0 +1,239 @@ +package integration_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Watch", func() { + var rootPath string + var pathA string + var pathB string + var pathC string + var session *gexec.Session + + BeforeEach(func() { + rootPath = tmpPath("root") + pathA = filepath.Join(rootPath, "src", "github.com", "onsi", "A") + pathB = filepath.Join(rootPath, "src", "github.com", "onsi", "B") + pathC = filepath.Join(rootPath, "src", "github.com", "onsi", "C") + + err := os.MkdirAll(pathA, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + err = os.MkdirAll(pathB, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + err = os.MkdirAll(pathC, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + copyIn(filepath.Join("watch_fixtures", "A"), pathA) + copyIn(filepath.Join("watch_fixtures", "B"), pathB) + copyIn(filepath.Join("watch_fixtures", "C"), pathC) + }) + + startGinkgoWithGopath := func(args ...string) *gexec.Session { + cmd := ginkgoCommand(rootPath, args...) + cmd.Env = append([]string{"GOPATH=" + rootPath + ":" + os.Getenv("GOPATH")}, os.Environ()...) + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + return session + } + + modifyFile := func(path string) { + time.Sleep(time.Second) + content, err := ioutil.ReadFile(path) + Ω(err).ShouldNot(HaveOccurred()) + content = append(content, []byte("//")...) + err = ioutil.WriteFile(path, content, 0666) + Ω(err).ShouldNot(HaveOccurred()) + } + + modifyCode := func(pkgToModify string) { + modifyFile(filepath.Join(rootPath, "src", "github.com", "onsi", pkgToModify, pkgToModify+".go")) + } + + modifyTest := func(pkgToModify string) { + modifyFile(filepath.Join(rootPath, "src", "github.com", "onsi", pkgToModify, pkgToModify+"_test.go")) + } + + AfterEach(func() { + if session != nil { + session.Kill().Wait() + } + }) + + It("should be set up correctly", func() { + session = startGinkgoWithGopath("-r") + Eventually(session).Should(gexec.Exit(0)) + Ω(session.Out.Contents()).Should(ContainSubstring("A Suite")) + Ω(session.Out.Contents()).Should(ContainSubstring("B Suite")) + Ω(session.Out.Contents()).Should(ContainSubstring("C Suite")) + Ω(session.Out.Contents()).Should(ContainSubstring("Ginkgo ran 3 suites")) + }) + + Context("when watching just one test suite", func() { + It("should immediately run, and should rerun when the test suite changes", func() { + session = startGinkgoWithGopath("watch", "-succinct", pathA) + Eventually(session).Should(gbytes.Say("A Suite")) + modifyCode("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + session.Kill().Wait() + }) + }) + + Context("when watching several test suites", func() { + It("should not immediately run, but should rerun a test when its code changes", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r") + Eventually(session).Should(gbytes.Say("Identified 3 test suites")) + Consistently(session).ShouldNot(gbytes.Say("A Suite|B Suite|C Suite")) + modifyCode("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("B Suite|C Suite")) + session.Kill().Wait() + }) + }) + + Describe("watching dependencies", func() { + Context("with a depth of 2", func() { + It("should watch down to that depth", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r", "-depth=2") + Eventually(session).Should(gbytes.Say("Identified 3 test suites")) + Eventually(session).Should(gbytes.Say(`A \[2 dependencies\]`)) + Eventually(session).Should(gbytes.Say(`B \[1 dependency\]`)) + Eventually(session).Should(gbytes.Say(`C \[0 dependencies\]`)) + + modifyCode("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("B Suite|C Suite")) + + modifyCode("B") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("B Suite")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("C Suite")) + + modifyCode("C") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("C Suite")) + Eventually(session).Should(gbytes.Say("B Suite")) + Eventually(session).Should(gbytes.Say("A Suite")) + }) + }) + + Context("with a depth of 1", func() { + It("should watch down to that depth", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r", "-depth=1") + Eventually(session).Should(gbytes.Say("Identified 3 test suites")) + Eventually(session).Should(gbytes.Say(`A \[1 dependency\]`)) + Eventually(session).Should(gbytes.Say(`B \[1 dependency\]`)) + Eventually(session).Should(gbytes.Say(`C \[0 dependencies\]`)) + + modifyCode("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("B Suite|C Suite")) + + modifyCode("B") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("B Suite")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("C Suite")) + + modifyCode("C") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("C Suite")) + Eventually(session).Should(gbytes.Say("B Suite")) + Consistently(session).ShouldNot(gbytes.Say("A Suite")) + }) + }) + + Context("with a depth of 0", func() { + It("should not watch any dependencies", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r", "-depth=0") + Eventually(session).Should(gbytes.Say("Identified 3 test suites")) + Eventually(session).Should(gbytes.Say(`A \[0 dependencies\]`)) + Eventually(session).Should(gbytes.Say(`B \[0 dependencies\]`)) + Eventually(session).Should(gbytes.Say(`C \[0 dependencies\]`)) + + modifyCode("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("B Suite|C Suite")) + + modifyCode("B") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("B Suite")) + Consistently(session).ShouldNot(gbytes.Say("A Suite|C Suite")) + + modifyCode("C") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("C Suite")) + Consistently(session).ShouldNot(gbytes.Say("A Suite|B Suite")) + }) + }) + + It("should not trigger dependents when tests are changed", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r", "-depth=2") + Eventually(session).Should(gbytes.Say("Identified 3 test suites")) + Eventually(session).Should(gbytes.Say(`A \[2 dependencies\]`)) + Eventually(session).Should(gbytes.Say(`B \[1 dependency\]`)) + Eventually(session).Should(gbytes.Say(`C \[0 dependencies\]`)) + + modifyTest("A") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("A Suite")) + Consistently(session).ShouldNot(gbytes.Say("B Suite|C Suite")) + + modifyTest("B") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("B Suite")) + Consistently(session).ShouldNot(gbytes.Say("A Suite|C Suite")) + + modifyTest("C") + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("C Suite")) + Consistently(session).ShouldNot(gbytes.Say("A Suite|B Suite")) + }) + }) + + Describe("when new test suite is added", func() { + It("should start monitoring that test suite", func() { + session = startGinkgoWithGopath("watch", "-succinct", "-r") + + Eventually(session).Should(gbytes.Say("Watching 3 suites")) + + pathD := filepath.Join(rootPath, "src", "github.com", "onsi", "D") + + err := os.MkdirAll(pathD, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + copyIn(filepath.Join("watch_fixtures", "D"), pathD) + + Eventually(session).Should(gbytes.Say("Detected 1 new suite")) + Eventually(session).Should(gbytes.Say(`D \[1 dependency\]`)) + Eventually(session).Should(gbytes.Say("D Suite")) + + modifyCode("D") + + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("D Suite")) + + modifyCode("C") + + Eventually(session).Should(gbytes.Say("Detected changes in")) + Eventually(session).Should(gbytes.Say("C Suite")) + Eventually(session).Should(gbytes.Say("D Suite")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location.go new file mode 100644 index 00000000000..fa2f0bf730c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location.go @@ -0,0 +1,32 @@ +package codelocation + +import ( + "regexp" + "runtime" + "runtime/debug" + "strings" + + "github.com/onsi/ginkgo/types" +) + +func New(skip int) types.CodeLocation { + _, file, line, _ := runtime.Caller(skip + 1) + stackTrace := PruneStack(string(debug.Stack()), skip) + return types.CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace} +} + +func PruneStack(fullStackTrace string, skip int) string { + stack := strings.Split(fullStackTrace, "\n") + if len(stack) > 2*(skip+1) { + stack = stack[2*(skip+1):] + } + prunedStack := []string{} + re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) + for i := 0; i < len(stack)/2; i++ { + if !re.Match([]byte(stack[i*2])) { + prunedStack = append(prunedStack, stack[i*2]) + prunedStack = append(prunedStack, stack[i*2+1]) + } + } + return strings.Join(prunedStack, "\n") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_suite_test.go new file mode 100644 index 00000000000..f06abf3c560 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_suite_test.go @@ -0,0 +1,13 @@ +package codelocation_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCodelocation(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CodeLocation Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_test.go new file mode 100644 index 00000000000..9f63f735289 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/codelocation/code_location_test.go @@ -0,0 +1,79 @@ +package codelocation_test + +import ( + "runtime" + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("CodeLocation", func() { + var ( + codeLocation types.CodeLocation + expectedFileName string + expectedLineNumber int + ) + + caller0 := func() { + codeLocation = codelocation.New(1) + } + + caller1 := func() { + _, expectedFileName, expectedLineNumber, _ = runtime.Caller(0) + expectedLineNumber += 2 + caller0() + } + + BeforeEach(func() { + caller1() + }) + + It("should use the passed in skip parameter to pick out the correct file & line number", func() { + Ω(codeLocation.FileName).Should(Equal(expectedFileName)) + Ω(codeLocation.LineNumber).Should(Equal(expectedLineNumber)) + }) + + Describe("stringer behavior", func() { + It("should stringify nicely", func() { + Ω(codeLocation.String()).Should(ContainSubstring("code_location_test.go:%d", expectedLineNumber)) + }) + }) + + //There's no better way than to test this private method as it + //goes out of its way to prune out ginkgo related code in the stack trace + Describe("PruneStack", func() { + It("should remove any references to ginkgo and pkg/testing and pkg/runtime", func() { + input := `/Skip/me +Skip: skip() +/Skip/me +Skip: skip() +/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever.go:10 (0x12314) +Something: Func() +/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever_else.go:10 (0x12314) +SomethingInternalToGinkgo: Func() +/usr/goroot/pkg/strings/oops.go:10 (0x12341) +Oops: BlowUp() +/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) +MyCode: Func() +/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) +MyCodeTest: Func() +/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08) +TestFoo: RunSpecs(t, "Foo Suite") +/usr/goroot/pkg/testing/testing.go:12 (0x37f08) +TestingT: Blah() +/usr/goroot/pkg/runtime/runtime.go:12 (0x37f08) +Something: Func() +` + prunedStack := codelocation.PruneStack(input, 1) + Ω(prunedStack).Should(Equal(`/usr/goroot/pkg/strings/oops.go:10 (0x12341) +Oops: BlowUp() +/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) +MyCode: Func() +/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) +MyCodeTest: Func() +/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08) +TestFoo: RunSpecs(t, "Foo Suite")`)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node.go new file mode 100644 index 00000000000..0737746dcfe --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node.go @@ -0,0 +1,151 @@ +package containernode + +import ( + "math/rand" + "sort" + + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/types" +) + +type subjectOrContainerNode struct { + containerNode *ContainerNode + subjectNode leafnodes.SubjectNode +} + +func (n subjectOrContainerNode) text() string { + if n.containerNode != nil { + return n.containerNode.Text() + } else { + return n.subjectNode.Text() + } +} + +type CollatedNodes struct { + Containers []*ContainerNode + Subject leafnodes.SubjectNode +} + +type ContainerNode struct { + text string + flag types.FlagType + codeLocation types.CodeLocation + + setupNodes []leafnodes.BasicNode + subjectAndContainerNodes []subjectOrContainerNode +} + +func New(text string, flag types.FlagType, codeLocation types.CodeLocation) *ContainerNode { + return &ContainerNode{ + text: text, + flag: flag, + codeLocation: codeLocation, + } +} + +func (container *ContainerNode) Shuffle(r *rand.Rand) { + sort.Sort(container) + permutation := r.Perm(len(container.subjectAndContainerNodes)) + shuffledNodes := make([]subjectOrContainerNode, len(container.subjectAndContainerNodes)) + for i, j := range permutation { + shuffledNodes[i] = container.subjectAndContainerNodes[j] + } + container.subjectAndContainerNodes = shuffledNodes +} + +func (node *ContainerNode) BackPropagateProgrammaticFocus() bool { + if node.flag == types.FlagTypePending { + return false + } + + shouldUnfocus := false + for _, subjectOrContainerNode := range node.subjectAndContainerNodes { + if subjectOrContainerNode.containerNode != nil { + shouldUnfocus = subjectOrContainerNode.containerNode.BackPropagateProgrammaticFocus() || shouldUnfocus + } else { + shouldUnfocus = (subjectOrContainerNode.subjectNode.Flag() == types.FlagTypeFocused) || shouldUnfocus + } + } + + if shouldUnfocus { + if node.flag == types.FlagTypeFocused { + node.flag = types.FlagTypeNone + } + return true + } + + return node.flag == types.FlagTypeFocused +} + +func (node *ContainerNode) Collate() []CollatedNodes { + return node.collate([]*ContainerNode{}) +} + +func (node *ContainerNode) collate(enclosingContainers []*ContainerNode) []CollatedNodes { + collated := make([]CollatedNodes, 0) + + containers := make([]*ContainerNode, len(enclosingContainers)) + copy(containers, enclosingContainers) + containers = append(containers, node) + + for _, subjectOrContainer := range node.subjectAndContainerNodes { + if subjectOrContainer.containerNode != nil { + collated = append(collated, subjectOrContainer.containerNode.collate(containers)...) + } else { + collated = append(collated, CollatedNodes{ + Containers: containers, + Subject: subjectOrContainer.subjectNode, + }) + } + } + + return collated +} + +func (node *ContainerNode) PushContainerNode(container *ContainerNode) { + node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{containerNode: container}) +} + +func (node *ContainerNode) PushSubjectNode(subject leafnodes.SubjectNode) { + node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{subjectNode: subject}) +} + +func (node *ContainerNode) PushSetupNode(setupNode leafnodes.BasicNode) { + node.setupNodes = append(node.setupNodes, setupNode) +} + +func (node *ContainerNode) SetupNodesOfType(nodeType types.SpecComponentType) []leafnodes.BasicNode { + nodes := []leafnodes.BasicNode{} + for _, setupNode := range node.setupNodes { + if setupNode.Type() == nodeType { + nodes = append(nodes, setupNode) + } + } + return nodes +} + +func (node *ContainerNode) Text() string { + return node.text +} + +func (node *ContainerNode) CodeLocation() types.CodeLocation { + return node.codeLocation +} + +func (node *ContainerNode) Flag() types.FlagType { + return node.flag +} + +//sort.Interface + +func (node *ContainerNode) Len() int { + return len(node.subjectAndContainerNodes) +} + +func (node *ContainerNode) Less(i, j int) bool { + return node.subjectAndContainerNodes[i].text() < node.subjectAndContainerNodes[j].text() +} + +func (node *ContainerNode) Swap(i, j int) { + node.subjectAndContainerNodes[i], node.subjectAndContainerNodes[j] = node.subjectAndContainerNodes[j], node.subjectAndContainerNodes[i] +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_suite_test.go new file mode 100644 index 00000000000..c6fc314ff57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_suite_test.go @@ -0,0 +1,13 @@ +package containernode_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestContainernode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Containernode Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_test.go new file mode 100644 index 00000000000..b83844ac810 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/containernode/container_node_test.go @@ -0,0 +1,212 @@ +package containernode_test + +import ( + "math/rand" + "github.com/onsi/ginkgo/internal/leafnodes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal/codelocation" + . "github.com/onsi/ginkgo/internal/containernode" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Container Node", func() { + var ( + codeLocation types.CodeLocation + container *ContainerNode + ) + + BeforeEach(func() { + codeLocation = codelocation.New(0) + container = New("description text", types.FlagTypeFocused, codeLocation) + }) + + Describe("creating a container node", func() { + It("can answer questions about itself", func() { + Ω(container.Text()).Should(Equal("description text")) + Ω(container.Flag()).Should(Equal(types.FlagTypeFocused)) + Ω(container.CodeLocation()).Should(Equal(codeLocation)) + }) + }) + + Describe("pushing setup nodes", func() { + It("can append setup nodes of various types and fetch them by type", func() { + befA := leafnodes.NewBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) + befB := leafnodes.NewBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) + aftA := leafnodes.NewAfterEachNode(func() {}, codelocation.New(0), 0, nil, 0) + aftB := leafnodes.NewAfterEachNode(func() {}, codelocation.New(0), 0, nil, 0) + jusBefA := leafnodes.NewJustBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) + jusBefB := leafnodes.NewJustBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) + + container.PushSetupNode(befA) + container.PushSetupNode(befB) + container.PushSetupNode(aftA) + container.PushSetupNode(aftB) + container.PushSetupNode(jusBefA) + container.PushSetupNode(jusBefB) + + subject := leafnodes.NewItNode("subject", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + container.PushSubjectNode(subject) + + Ω(container.SetupNodesOfType(types.SpecComponentTypeBeforeEach)).Should(Equal([]leafnodes.BasicNode{befA, befB})) + Ω(container.SetupNodesOfType(types.SpecComponentTypeAfterEach)).Should(Equal([]leafnodes.BasicNode{aftA, aftB})) + Ω(container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach)).Should(Equal([]leafnodes.BasicNode{jusBefA, jusBefB})) + Ω(container.SetupNodesOfType(types.SpecComponentTypeIt)).Should(BeEmpty()) //subjects are not setup nodes + }) + }) + + Context("With appended containers and subject nodes", func() { + var ( + itA, itB, innerItA, innerItB leafnodes.SubjectNode + innerContainer *ContainerNode + ) + + BeforeEach(func() { + itA = leafnodes.NewItNode("Banana", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + itB = leafnodes.NewItNode("Apple", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + + innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + innerItB = leafnodes.NewItNode("inner B", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + + innerContainer = New("Orange", types.FlagTypeNone, codelocation.New(0)) + + container.PushSubjectNode(itA) + container.PushContainerNode(innerContainer) + innerContainer.PushSubjectNode(innerItA) + innerContainer.PushSubjectNode(innerItB) + container.PushSubjectNode(itB) + }) + + Describe("Collating", func() { + It("should return a collated set of containers and subject nodes in the correct order", func() { + collated := container.Collate() + Ω(collated).Should(HaveLen(4)) + + Ω(collated[0]).Should(Equal(CollatedNodes{ + Containers: []*ContainerNode{container}, + Subject: itA, + })) + + Ω(collated[1]).Should(Equal(CollatedNodes{ + Containers: []*ContainerNode{container, innerContainer}, + Subject: innerItA, + })) + + Ω(collated[2]).Should(Equal(CollatedNodes{ + Containers: []*ContainerNode{container, innerContainer}, + Subject: innerItB, + })) + + Ω(collated[3]).Should(Equal(CollatedNodes{ + Containers: []*ContainerNode{container}, + Subject: itB, + })) + }) + }) + + Describe("Backpropagating Programmatic Focus", func() { + //This allows inner focused specs to override the focus of outer focussed + //specs and more closely maps to what a developer wants to happen + //when debugging a test suite + + Context("when a parent is focused *and* an inner subject is focused", func() { + BeforeEach(func() { + container = New("description text", types.FlagTypeFocused, codeLocation) + itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + container.PushSubjectNode(itA) + + innerContainer = New("Orange", types.FlagTypeNone, codelocation.New(0)) + container.PushContainerNode(innerContainer) + innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeFocused, codelocation.New(0), 0, nil, 0) + innerContainer.PushSubjectNode(innerItA) + }) + + It("should unfocus the parent", func() { + container.BackPropagateProgrammaticFocus() + + Ω(container.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(innerContainer.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(innerItA.Flag()).Should(Equal(types.FlagTypeFocused)) + }) + }) + + Context("when a parent is focused *and* an inner container is focused", func() { + BeforeEach(func() { + container = New("description text", types.FlagTypeFocused, codeLocation) + itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + container.PushSubjectNode(itA) + + innerContainer = New("Orange", types.FlagTypeFocused, codelocation.New(0)) + container.PushContainerNode(innerContainer) + innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + innerContainer.PushSubjectNode(innerItA) + }) + + It("should unfocus the parent", func() { + container.BackPropagateProgrammaticFocus() + + Ω(container.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(innerContainer.Flag()).Should(Equal(types.FlagTypeFocused)) + Ω(innerItA.Flag()).Should(Equal(types.FlagTypeNone)) + }) + }) + + Context("when a parent is pending and a child is focused", func() { + BeforeEach(func() { + container = New("description text", types.FlagTypeFocused, codeLocation) + itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) + container.PushSubjectNode(itA) + + innerContainer = New("Orange", types.FlagTypePending, codelocation.New(0)) + container.PushContainerNode(innerContainer) + innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeFocused, codelocation.New(0), 0, nil, 0) + innerContainer.PushSubjectNode(innerItA) + }) + + It("should not do anything", func() { + container.BackPropagateProgrammaticFocus() + + Ω(container.Flag()).Should(Equal(types.FlagTypeFocused)) + Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) + Ω(innerContainer.Flag()).Should(Equal(types.FlagTypePending)) + Ω(innerItA.Flag()).Should(Equal(types.FlagTypeFocused)) + }) + }) + }) + + Describe("Shuffling", func() { + var unshuffledCollation []CollatedNodes + BeforeEach(func() { + unshuffledCollation = container.Collate() + + r := rand.New(rand.NewSource(17)) + container.Shuffle(r) + }) + + It("should sort, and then shuffle, the top level contents of the container", func() { + shuffledCollation := container.Collate() + Ω(shuffledCollation).Should(HaveLen(len(unshuffledCollation))) + Ω(shuffledCollation).ShouldNot(Equal(unshuffledCollation)) + + for _, entry := range unshuffledCollation { + Ω(shuffledCollation).Should(ContainElement(entry)) + } + + innerAIndex, innerBIndex := 0, 0 + for i, entry := range shuffledCollation { + if entry.Subject == innerItA { + innerAIndex = i + } else if entry.Subject == innerItB { + innerBIndex = i + } + } + + Ω(innerAIndex).Should(Equal(innerBIndex - 1)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer.go new file mode 100644 index 00000000000..24bbfb23608 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer.go @@ -0,0 +1,77 @@ +package failer + +import ( + "github.com/onsi/ginkgo/types" + "sync" +) + +type Failer struct { + lock *sync.Mutex + failure types.SpecFailure + state types.SpecState +} + +func New() *Failer { + return &Failer{ + lock: &sync.Mutex{}, + state: types.SpecStatePassed, + } +} + +func (f *Failer) Panic(location types.CodeLocation, forwardedPanic interface{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.state == types.SpecStatePassed { + f.state = types.SpecStatePanicked + f.failure = types.SpecFailure{ + Message: "Test Panicked", + Location: location, + ForwardedPanic: forwardedPanic, + } + } +} + +func (f *Failer) Timeout(location types.CodeLocation) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.state == types.SpecStatePassed { + f.state = types.SpecStateTimedOut + f.failure = types.SpecFailure{ + Message: "Timed out", + Location: location, + } + } +} + +func (f *Failer) Fail(message string, location types.CodeLocation) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.state == types.SpecStatePassed { + f.state = types.SpecStateFailed + f.failure = types.SpecFailure{ + Message: message, + Location: location, + } + } +} + +func (f *Failer) Drain(componentType types.SpecComponentType, componentIndex int, componentCodeLocation types.CodeLocation) (types.SpecFailure, types.SpecState) { + f.lock.Lock() + defer f.lock.Unlock() + + failure := f.failure + outcome := f.state + if outcome != types.SpecStatePassed { + failure.ComponentType = componentType + failure.ComponentIndex = componentIndex + failure.ComponentCodeLocation = componentCodeLocation + } + + f.state = types.SpecStatePassed + f.failure = types.SpecFailure{} + + return failure, outcome +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_suite_test.go new file mode 100644 index 00000000000..8dce7be9ac5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_suite_test.go @@ -0,0 +1,13 @@ +package failer_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFailer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Failer Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_test.go new file mode 100644 index 00000000000..465b2ccc29e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/failer/failer_test.go @@ -0,0 +1,125 @@ +package failer_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/failer" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Failer", func() { + var ( + failer *Failer + codeLocationA types.CodeLocation + codeLocationB types.CodeLocation + ) + + BeforeEach(func() { + codeLocationA = codelocation.New(0) + codeLocationB = codelocation.New(0) + failer = New() + }) + + Context("with no failures", func() { + It("should return success when drained", func() { + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + Ω(failure).Should(BeZero()) + Ω(state).Should(Equal(types.SpecStatePassed)) + }) + }) + + Describe("Fail", func() { + It("should handle failures", func() { + failer.Fail("something failed", codeLocationA) + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "something failed", + Location: codeLocationA, + ForwardedPanic: nil, + ComponentType: types.SpecComponentTypeIt, + ComponentIndex: 3, + ComponentCodeLocation: codeLocationB, + })) + Ω(state).Should(Equal(types.SpecStateFailed)) + }) + }) + + Describe("Panic", func() { + It("should handle panics", func() { + failer.Panic(codeLocationA, "some forwarded panic") + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "Test Panicked", + Location: codeLocationA, + ForwardedPanic: "some forwarded panic", + ComponentType: types.SpecComponentTypeIt, + ComponentIndex: 3, + ComponentCodeLocation: codeLocationB, + })) + Ω(state).Should(Equal(types.SpecStatePanicked)) + }) + }) + + Describe("Timeout", func() { + It("should handle timeouts", func() { + failer.Timeout(codeLocationA) + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "Timed out", + Location: codeLocationA, + ForwardedPanic: nil, + ComponentType: types.SpecComponentTypeIt, + ComponentIndex: 3, + ComponentCodeLocation: codeLocationB, + })) + Ω(state).Should(Equal(types.SpecStateTimedOut)) + }) + }) + + Context("when multiple failures are registered", func() { + BeforeEach(func() { + failer.Fail("something failed", codeLocationA) + failer.Fail("something else failed", codeLocationA) + }) + + It("should only report the first one when drained", func() { + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "something failed", + Location: codeLocationA, + ForwardedPanic: nil, + ComponentType: types.SpecComponentTypeIt, + ComponentIndex: 3, + ComponentCodeLocation: codeLocationB, + })) + Ω(state).Should(Equal(types.SpecStateFailed)) + }) + + It("should report subsequent failures after being drained", func() { + failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + failer.Fail("yet another thing failed", codeLocationA) + + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "yet another thing failed", + Location: codeLocationA, + ForwardedPanic: nil, + ComponentType: types.SpecComponentTypeIt, + ComponentIndex: 3, + ComponentCodeLocation: codeLocationB, + })) + Ω(state).Should(Equal(types.SpecStateFailed)) + }) + + It("should report sucess on subsequent drains if no errors occur", func() { + failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) + Ω(failure).Should(BeZero()) + Ω(state).Should(Equal(types.SpecStatePassed)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/benchmarker.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/benchmarker.go new file mode 100644 index 00000000000..1c0ce0b1156 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/benchmarker.go @@ -0,0 +1,86 @@ +package leafnodes + +import ( + "math" + "time" + + "github.com/onsi/ginkgo/types" +) + +type benchmarker struct { + measurements map[string]*types.SpecMeasurement + orderCounter int +} + +func newBenchmarker() *benchmarker { + return &benchmarker{ + measurements: make(map[string]*types.SpecMeasurement, 0), + } +} + +func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) { + t := time.Now() + body() + elapsedTime = time.Since(t) + + measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", info...) + measurement.Results = append(measurement.Results, elapsedTime.Seconds()) + + return +} + +func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) { + measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", info...) + measurement.Results = append(measurement.Results, value) +} + +func (b *benchmarker) getMeasurement(name string, smallestLabel string, largestLabel string, averageLabel string, units string, info ...interface{}) *types.SpecMeasurement { + measurement, ok := b.measurements[name] + if !ok { + var computedInfo interface{} + computedInfo = nil + if len(info) > 0 { + computedInfo = info[0] + } + measurement = &types.SpecMeasurement{ + Name: name, + Info: computedInfo, + Order: b.orderCounter, + SmallestLabel: smallestLabel, + LargestLabel: largestLabel, + AverageLabel: averageLabel, + Units: units, + Results: make([]float64, 0), + } + b.measurements[name] = measurement + b.orderCounter++ + } + + return measurement +} + +func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement { + for _, measurement := range b.measurements { + measurement.Smallest = math.MaxFloat64 + measurement.Largest = -math.MaxFloat64 + sum := float64(0) + sumOfSquares := float64(0) + + for _, result := range measurement.Results { + if result > measurement.Largest { + measurement.Largest = result + } + if result < measurement.Smallest { + measurement.Smallest = result + } + sum += result + sumOfSquares += result * result + } + + n := float64(len(measurement.Results)) + measurement.Average = sum / n + measurement.StdDeviation = math.Sqrt(sumOfSquares/n - (sum/n)*(sum/n)) + } + + return b.measurements +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/interfaces.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/interfaces.go new file mode 100644 index 00000000000..8c3902d601c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/interfaces.go @@ -0,0 +1,19 @@ +package leafnodes + +import ( + "github.com/onsi/ginkgo/types" +) + +type BasicNode interface { + Type() types.SpecComponentType + Run() (types.SpecState, types.SpecFailure) + CodeLocation() types.CodeLocation +} + +type SubjectNode interface { + BasicNode + + Text() string + Flag() types.FlagType + Samples() int +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node.go new file mode 100644 index 00000000000..c76fe3a4512 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node.go @@ -0,0 +1,46 @@ +package leafnodes + +import ( + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "time" +) + +type ItNode struct { + runner *runner + + flag types.FlagType + text string +} + +func NewItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *ItNode { + return &ItNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeIt, componentIndex), + flag: flag, + text: text, + } +} + +func (node *ItNode) Run() (outcome types.SpecState, failure types.SpecFailure) { + return node.runner.run() +} + +func (node *ItNode) Type() types.SpecComponentType { + return types.SpecComponentTypeIt +} + +func (node *ItNode) Text() string { + return node.text +} + +func (node *ItNode) Flag() types.FlagType { + return node.flag +} + +func (node *ItNode) CodeLocation() types.CodeLocation { + return node.runner.codeLocation +} + +func (node *ItNode) Samples() int { + return 1 +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node_test.go new file mode 100644 index 00000000000..29fa0c6e2a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/it_node_test.go @@ -0,0 +1,22 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/leafnodes" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("It Nodes", func() { + It("should report the correct type, text, flag, and code location", func() { + codeLocation := codelocation.New(0) + it := NewItNode("my it node", func() {}, types.FlagTypeFocused, codeLocation, 0, nil, 3) + Ω(it.Type()).Should(Equal(types.SpecComponentTypeIt)) + Ω(it.Flag()).Should(Equal(types.FlagTypeFocused)) + Ω(it.Text()).Should(Equal("my it node")) + Ω(it.CodeLocation()).Should(Equal(codeLocation)) + Ω(it.Samples()).Should(Equal(1)) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/leaf_node_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/leaf_node_suite_test.go new file mode 100644 index 00000000000..a7ba9e006ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/leaf_node_suite_test.go @@ -0,0 +1,13 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLeafNode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "LeafNode Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node.go new file mode 100644 index 00000000000..efc3348c1b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node.go @@ -0,0 +1,61 @@ +package leafnodes + +import ( + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "reflect" +) + +type MeasureNode struct { + runner *runner + + text string + flag types.FlagType + samples int + benchmarker *benchmarker +} + +func NewMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int, failer *failer.Failer, componentIndex int) *MeasureNode { + benchmarker := newBenchmarker() + + wrappedBody := func() { + reflect.ValueOf(body).Call([]reflect.Value{reflect.ValueOf(benchmarker)}) + } + + return &MeasureNode{ + runner: newRunner(wrappedBody, codeLocation, 0, failer, types.SpecComponentTypeMeasure, componentIndex), + + text: text, + flag: flag, + samples: samples, + benchmarker: benchmarker, + } +} + +func (node *MeasureNode) Run() (outcome types.SpecState, failure types.SpecFailure) { + return node.runner.run() +} + +func (node *MeasureNode) MeasurementsReport() map[string]*types.SpecMeasurement { + return node.benchmarker.measurementsReport() +} + +func (node *MeasureNode) Type() types.SpecComponentType { + return types.SpecComponentTypeMeasure +} + +func (node *MeasureNode) Text() string { + return node.text +} + +func (node *MeasureNode) Flag() types.FlagType { + return node.flag +} + +func (node *MeasureNode) CodeLocation() types.CodeLocation { + return node.runner.codeLocation +} + +func (node *MeasureNode) Samples() int { + return node.samples +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node_test.go new file mode 100644 index 00000000000..ee4f70bf0be --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/measure_node_test.go @@ -0,0 +1,109 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/leafnodes" + . "github.com/onsi/gomega" + + "time" + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Measure Nodes", func() { + It("should report the correct type, text, flag, and code location", func() { + codeLocation := codelocation.New(0) + measure := NewMeasureNode("my measure node", func(b Benchmarker) {}, types.FlagTypeFocused, codeLocation, 10, nil, 3) + Ω(measure.Type()).Should(Equal(types.SpecComponentTypeMeasure)) + Ω(measure.Flag()).Should(Equal(types.FlagTypeFocused)) + Ω(measure.Text()).Should(Equal("my measure node")) + Ω(measure.CodeLocation()).Should(Equal(codeLocation)) + Ω(measure.Samples()).Should(Equal(10)) + }) + + Describe("benchmarking", func() { + var measure *MeasureNode + + Describe("Value", func() { + BeforeEach(func() { + measure = NewMeasureNode("the measurement", func(b Benchmarker) { + b.RecordValue("foo", 7, "info!") + b.RecordValue("foo", 2) + b.RecordValue("foo", 3) + b.RecordValue("bar", 0.3) + b.RecordValue("bar", 0.1) + b.RecordValue("bar", 0.5) + b.RecordValue("bar", 0.7) + }, types.FlagTypeFocused, codelocation.New(0), 1, Failer.New(), 3) + Ω(measure.Run()).Should(Equal(types.SpecStatePassed)) + }) + + It("records passed in values and reports on them", func() { + report := measure.MeasurementsReport() + Ω(report).Should(HaveLen(2)) + Ω(report["foo"].Name).Should(Equal("foo")) + Ω(report["foo"].Info).Should(Equal("info!")) + Ω(report["foo"].Order).Should(Equal(0)) + Ω(report["foo"].SmallestLabel).Should(Equal("Smallest")) + Ω(report["foo"].LargestLabel).Should(Equal(" Largest")) + Ω(report["foo"].AverageLabel).Should(Equal(" Average")) + Ω(report["foo"].Units).Should(Equal("")) + Ω(report["foo"].Results).Should(Equal([]float64{7, 2, 3})) + Ω(report["foo"].Smallest).Should(BeNumerically("==", 2)) + Ω(report["foo"].Largest).Should(BeNumerically("==", 7)) + Ω(report["foo"].Average).Should(BeNumerically("==", 4)) + Ω(report["foo"].StdDeviation).Should(BeNumerically("~", 2.16, 0.01)) + + Ω(report["bar"].Name).Should(Equal("bar")) + Ω(report["bar"].Info).Should(BeNil()) + Ω(report["bar"].SmallestLabel).Should(Equal("Smallest")) + Ω(report["bar"].Order).Should(Equal(1)) + Ω(report["bar"].LargestLabel).Should(Equal(" Largest")) + Ω(report["bar"].AverageLabel).Should(Equal(" Average")) + Ω(report["bar"].Units).Should(Equal("")) + Ω(report["bar"].Results).Should(Equal([]float64{0.3, 0.1, 0.5, 0.7})) + Ω(report["bar"].Smallest).Should(BeNumerically("==", 0.1)) + Ω(report["bar"].Largest).Should(BeNumerically("==", 0.7)) + Ω(report["bar"].Average).Should(BeNumerically("==", 0.4)) + Ω(report["bar"].StdDeviation).Should(BeNumerically("~", 0.22, 0.01)) + }) + }) + + Describe("Time", func() { + BeforeEach(func() { + measure = NewMeasureNode("the measurement", func(b Benchmarker) { + b.Time("foo", func() { + time.Sleep(100 * time.Millisecond) + }, "info!") + b.Time("foo", func() { + time.Sleep(200 * time.Millisecond) + }) + b.Time("foo", func() { + time.Sleep(170 * time.Millisecond) + }) + }, types.FlagTypeFocused, codelocation.New(0), 1, Failer.New(), 3) + Ω(measure.Run()).Should(Equal(types.SpecStatePassed)) + }) + + It("records passed in values and reports on them", func() { + report := measure.MeasurementsReport() + Ω(report).Should(HaveLen(1)) + Ω(report["foo"].Name).Should(Equal("foo")) + Ω(report["foo"].Info).Should(Equal("info!")) + Ω(report["foo"].SmallestLabel).Should(Equal("Fastest Time")) + Ω(report["foo"].LargestLabel).Should(Equal("Slowest Time")) + Ω(report["foo"].AverageLabel).Should(Equal("Average Time")) + Ω(report["foo"].Units).Should(Equal("s")) + Ω(report["foo"].Results).Should(HaveLen(3)) + Ω(report["foo"].Results[0]).Should(BeNumerically("~", 0.1, 0.01)) + Ω(report["foo"].Results[1]).Should(BeNumerically("~", 0.2, 0.01)) + Ω(report["foo"].Results[2]).Should(BeNumerically("~", 0.17, 0.01)) + Ω(report["foo"].Smallest).Should(BeNumerically("~", 0.1, 0.01)) + Ω(report["foo"].Largest).Should(BeNumerically("~", 0.2, 0.01)) + Ω(report["foo"].Average).Should(BeNumerically("~", 0.16, 0.01)) + Ω(report["foo"].StdDeviation).Should(BeNumerically("~", 0.04, 0.01)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/runner.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/runner.go new file mode 100644 index 00000000000..04ec6dbf8bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/runner.go @@ -0,0 +1,107 @@ +package leafnodes + +import ( + "fmt" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "reflect" + "time" +) + +type runner struct { + isAsync bool + asyncFunc func(chan<- interface{}) + syncFunc func() + codeLocation types.CodeLocation + timeoutThreshold time.Duration + nodeType types.SpecComponentType + componentIndex int + failer *failer.Failer +} + +func newRunner(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, nodeType types.SpecComponentType, componentIndex int) *runner { + bodyType := reflect.TypeOf(body) + if bodyType.Kind() != reflect.Func { + panic(fmt.Sprintf("Expected a function but got something else at %v", codeLocation)) + } + + runner := &runner{ + codeLocation: codeLocation, + timeoutThreshold: timeout, + failer: failer, + nodeType: nodeType, + componentIndex: componentIndex, + } + + switch bodyType.NumIn() { + case 0: + runner.syncFunc = body.(func()) + return runner + case 1: + if !(bodyType.In(0).Kind() == reflect.Chan && bodyType.In(0).Elem().Kind() == reflect.Interface) { + panic(fmt.Sprintf("Must pass a Done channel to function at %v", codeLocation)) + } + + wrappedBody := func(done chan<- interface{}) { + bodyValue := reflect.ValueOf(body) + bodyValue.Call([]reflect.Value{reflect.ValueOf(done)}) + } + + runner.isAsync = true + runner.asyncFunc = wrappedBody + return runner + } + + panic(fmt.Sprintf("Too many arguments to function at %v", codeLocation)) +} + +func (r *runner) run() (outcome types.SpecState, failure types.SpecFailure) { + if r.isAsync { + return r.runAsync() + } else { + return r.runSync() + } +} + +func (r *runner) runAsync() (outcome types.SpecState, failure types.SpecFailure) { + done := make(chan interface{}, 1) + + go func() { + defer func() { + if e := recover(); e != nil { + r.failer.Panic(codelocation.New(2), e) + select { + case <-done: + break + default: + close(done) + } + } + }() + + r.asyncFunc(done) + }() + + select { + case <-done: + case <-time.After(r.timeoutThreshold): + r.failer.Timeout(r.codeLocation) + } + + failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) + return +} +func (r *runner) runSync() (outcome types.SpecState, failure types.SpecFailure) { + defer func() { + if e := recover(); e != nil { + r.failer.Panic(codelocation.New(2), e) + } + + failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) + }() + + r.syncFunc() + + return +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes.go new file mode 100644 index 00000000000..6b725a63153 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes.go @@ -0,0 +1,41 @@ +package leafnodes + +import ( + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "time" +) + +type SetupNode struct { + runner *runner +} + +func (node *SetupNode) Run() (outcome types.SpecState, failure types.SpecFailure) { + return node.runner.run() +} + +func (node *SetupNode) Type() types.SpecComponentType { + return node.runner.nodeType +} + +func (node *SetupNode) CodeLocation() types.CodeLocation { + return node.runner.codeLocation +} + +func NewBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { + return &SetupNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeEach, componentIndex), + } +} + +func NewAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { + return &SetupNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterEach, componentIndex), + } +} + +func NewJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { + return &SetupNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustBeforeEach, componentIndex), + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes_test.go new file mode 100644 index 00000000000..d5b9251f652 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/setup_nodes_test.go @@ -0,0 +1,40 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + . "github.com/onsi/ginkgo/internal/leafnodes" + + "github.com/onsi/ginkgo/internal/codelocation" +) + +var _ = Describe("Setup Nodes", func() { + Describe("BeforeEachNodes", func() { + It("should report the correct type and code location", func() { + codeLocation := codelocation.New(0) + beforeEach := NewBeforeEachNode(func() {}, codeLocation, 0, nil, 3) + Ω(beforeEach.Type()).Should(Equal(types.SpecComponentTypeBeforeEach)) + Ω(beforeEach.CodeLocation()).Should(Equal(codeLocation)) + }) + }) + + Describe("AfterEachNodes", func() { + It("should report the correct type and code location", func() { + codeLocation := codelocation.New(0) + afterEach := NewAfterEachNode(func() {}, codeLocation, 0, nil, 3) + Ω(afterEach.Type()).Should(Equal(types.SpecComponentTypeAfterEach)) + Ω(afterEach.CodeLocation()).Should(Equal(codeLocation)) + }) + }) + + Describe("JustBeforeEachNodes", func() { + It("should report the correct type and code location", func() { + codeLocation := codelocation.New(0) + justBeforeEach := NewJustBeforeEachNode(func() {}, codeLocation, 0, nil, 3) + Ω(justBeforeEach.Type()).Should(Equal(types.SpecComponentTypeJustBeforeEach)) + Ω(justBeforeEach.CodeLocation()).Should(Equal(codeLocation)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/shared_runner_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/shared_runner_test.go new file mode 100644 index 00000000000..1293d240a66 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/shared_runner_test.go @@ -0,0 +1,342 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/leafnodes" + . "github.com/onsi/gomega" + + "reflect" + "runtime" + "time" + + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" +) + +type runnable interface { + Run() (outcome types.SpecState, failure types.SpecFailure) + CodeLocation() types.CodeLocation +} + +func SynchronousSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType, componentIndex int) { + var ( + outcome types.SpecState + failure types.SpecFailure + + failer *Failer.Failer + + componentCodeLocation types.CodeLocation + innerCodeLocation types.CodeLocation + + didRun bool + ) + + BeforeEach(func() { + failer = Failer.New() + componentCodeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + + didRun = false + }) + + Describe("synchronous functions", func() { + Context("when the function passes", func() { + BeforeEach(func() { + outcome, failure = build(func() { + didRun = true + }, 0, failer, componentCodeLocation).Run() + }) + + It("should have a succesful outcome", func() { + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStatePassed)) + Ω(failure).Should(BeZero()) + }) + }) + + Context("when a failure occurs", func() { + BeforeEach(func() { + outcome, failure = build(func() { + didRun = true + failer.Fail("bam", innerCodeLocation) + panic("should not matter") + }, 0, failer, componentCodeLocation).Run() + }) + + It("should return the failure", func() { + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "bam", + Location: innerCodeLocation, + ForwardedPanic: nil, + ComponentIndex: componentIndex, + ComponentType: componentType, + ComponentCodeLocation: componentCodeLocation, + })) + }) + }) + + Context("when a panic occurs", func() { + BeforeEach(func() { + outcome, failure = build(func() { + didRun = true + innerCodeLocation = codelocation.New(0) + panic("ack!") + }, 0, failer, componentCodeLocation).Run() + }) + + It("should return the panic", func() { + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStatePanicked)) + innerCodeLocation.LineNumber++ + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "Test Panicked", + Location: innerCodeLocation, + ForwardedPanic: "ack!", + ComponentIndex: componentIndex, + ComponentType: componentType, + ComponentCodeLocation: componentCodeLocation, + })) + }) + }) + }) +} + +func AsynchronousSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType, componentIndex int) { + var ( + outcome types.SpecState + failure types.SpecFailure + + failer *Failer.Failer + + componentCodeLocation types.CodeLocation + innerCodeLocation types.CodeLocation + + didRun bool + ) + + BeforeEach(func() { + failer = Failer.New() + componentCodeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + + didRun = false + }) + + Describe("asynchronous functions", func() { + var timeoutDuration time.Duration + + BeforeEach(func() { + timeoutDuration = time.Duration(1 * float64(time.Second)) + }) + + Context("when running", func() { + It("should run the function as a goroutine, and block until it's done", func() { + initialNumberOfGoRoutines := runtime.NumGoroutine() + numberOfGoRoutines := 0 + + build(func(done Done) { + didRun = true + numberOfGoRoutines = runtime.NumGoroutine() + close(done) + }, timeoutDuration, failer, componentCodeLocation).Run() + + Ω(didRun).Should(BeTrue()) + Ω(numberOfGoRoutines).Should(BeNumerically(">=", initialNumberOfGoRoutines+1)) + }) + }) + + Context("when the function passes", func() { + BeforeEach(func() { + outcome, failure = build(func(done Done) { + didRun = true + close(done) + }, timeoutDuration, failer, componentCodeLocation).Run() + }) + + It("should have a succesful outcome", func() { + Ω(didRun).Should(BeTrue()) + Ω(outcome).Should(Equal(types.SpecStatePassed)) + Ω(failure).Should(BeZero()) + }) + }) + + Context("when the function fails", func() { + BeforeEach(func() { + outcome, failure = build(func(done Done) { + didRun = true + failer.Fail("bam", innerCodeLocation) + time.Sleep(20 * time.Millisecond) + panic("doesn't matter") + close(done) + }, 10*time.Millisecond, failer, componentCodeLocation).Run() + }) + + It("should return the failure", func() { + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "bam", + Location: innerCodeLocation, + ForwardedPanic: nil, + ComponentIndex: componentIndex, + ComponentType: componentType, + ComponentCodeLocation: componentCodeLocation, + })) + }) + }) + + Context("when the function times out", func() { + var guard chan struct{} + + BeforeEach(func() { + guard = make(chan struct{}) + outcome, failure = build(func(done Done) { + didRun = true + time.Sleep(20 * time.Millisecond) + close(guard) + panic("doesn't matter") + close(done) + }, 10*time.Millisecond, failer, componentCodeLocation).Run() + }) + + It("should return the timeout", func() { + <-guard + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStateTimedOut)) + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "Timed out", + Location: componentCodeLocation, + ForwardedPanic: nil, + ComponentIndex: componentIndex, + ComponentType: componentType, + ComponentCodeLocation: componentCodeLocation, + })) + }) + }) + + Context("when the function panics", func() { + BeforeEach(func() { + outcome, failure = build(func(done Done) { + didRun = true + innerCodeLocation = codelocation.New(0) + panic("ack!") + }, 100*time.Millisecond, failer, componentCodeLocation).Run() + }) + + It("should return the panic", func() { + Ω(didRun).Should(BeTrue()) + + Ω(outcome).Should(Equal(types.SpecStatePanicked)) + innerCodeLocation.LineNumber++ + Ω(failure).Should(Equal(types.SpecFailure{ + Message: "Test Panicked", + Location: innerCodeLocation, + ForwardedPanic: "ack!", + ComponentIndex: componentIndex, + ComponentType: componentType, + ComponentCodeLocation: componentCodeLocation, + })) + }) + }) + }) +} + +func InvalidSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType) { + var ( + failer *Failer.Failer + componentCodeLocation types.CodeLocation + innerCodeLocation types.CodeLocation + ) + + BeforeEach(func() { + failer = Failer.New() + componentCodeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + }) + + Describe("invalid functions", func() { + Context("when passed something that's not a function", func() { + It("should panic", func() { + Ω(func() { + build("not a function", 0, failer, componentCodeLocation) + }).Should(Panic()) + }) + }) + + Context("when the function takes the wrong kind of argument", func() { + It("should panic", func() { + Ω(func() { + build(func(oops string) {}, 0, failer, componentCodeLocation) + }).Should(Panic()) + }) + }) + + Context("when the function takes more than one argument", func() { + It("should panic", func() { + Ω(func() { + build(func(done Done, oops string) {}, 0, failer, componentCodeLocation) + }).Should(Panic()) + }) + }) + }) +} + +var _ = Describe("Shared RunnableNode behavior", func() { + Describe("It Nodes", func() { + build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { + return NewItNode("", body, types.FlagTypeFocused, componentCodeLocation, timeout, failer, 3) + } + + SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeIt, 3) + AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeIt, 3) + InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeIt) + }) + + Describe("Measure Nodes", func() { + build := func(body interface{}, _ time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { + return NewMeasureNode("", func(Benchmarker) { + reflect.ValueOf(body).Call([]reflect.Value{}) + }, types.FlagTypeFocused, componentCodeLocation, 10, failer, 3) + } + + SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeMeasure, 3) + }) + + Describe("BeforeEach Nodes", func() { + build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { + return NewBeforeEachNode(body, componentCodeLocation, timeout, failer, 3) + } + + SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach, 3) + AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach, 3) + InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach) + }) + + Describe("AfterEach Nodes", func() { + build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { + return NewAfterEachNode(body, componentCodeLocation, timeout, failer, 3) + } + + SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach, 3) + AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach, 3) + InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach) + }) + + Describe("JustBeforeEach Nodes", func() { + build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { + return NewJustBeforeEachNode(body, componentCodeLocation, timeout, failer, 3) + } + + SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach, 3) + AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach, 3) + InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes.go new file mode 100644 index 00000000000..2ccc7dc0fb0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes.go @@ -0,0 +1,54 @@ +package leafnodes + +import ( + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "time" +) + +type SuiteNode interface { + Run(parallelNode int, parallelTotal int, syncHost string) bool + Passed() bool + Summary() *types.SetupSummary +} + +type simpleSuiteNode struct { + runner *runner + outcome types.SpecState + failure types.SpecFailure + runTime time.Duration +} + +func (node *simpleSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { + t := time.Now() + node.outcome, node.failure = node.runner.run() + node.runTime = time.Since(t) + + return node.outcome == types.SpecStatePassed +} + +func (node *simpleSuiteNode) Passed() bool { + return node.outcome == types.SpecStatePassed +} + +func (node *simpleSuiteNode) Summary() *types.SetupSummary { + return &types.SetupSummary{ + ComponentType: node.runner.nodeType, + CodeLocation: node.runner.codeLocation, + State: node.outcome, + RunTime: node.runTime, + Failure: node.failure, + } +} + +func NewBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { + return &simpleSuiteNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0), + } +} + +func NewAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { + return &simpleSuiteNode{ + runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes_test.go new file mode 100644 index 00000000000..33411579be0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/suite_nodes_test.go @@ -0,0 +1,229 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/onsi/ginkgo/internal/leafnodes" + + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "time" +) + +var _ = Describe("SuiteNodes", func() { + Describe("BeforeSuite nodes", func() { + var befSuite SuiteNode + var failer *Failer.Failer + var codeLocation types.CodeLocation + var innerCodeLocation types.CodeLocation + var outcome bool + + BeforeEach(func() { + failer = Failer.New() + codeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + }) + + Context("when the body passes", func() { + BeforeEach(func() { + befSuite = NewBeforeSuiteNode(func() { + time.Sleep(10 * time.Millisecond) + }, codeLocation, 0, failer) + outcome = befSuite.Run(0, 0, "") + }) + + It("should return true when run and report as passed", func() { + Ω(outcome).Should(BeTrue()) + Ω(befSuite.Passed()).Should(BeTrue()) + }) + + It("should have the correct summary", func() { + summary := befSuite.Summary() + Ω(summary.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.CodeLocation).Should(Equal(codeLocation)) + Ω(summary.State).Should(Equal(types.SpecStatePassed)) + Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) + Ω(summary.Failure).Should(BeZero()) + }) + }) + + Context("when the body fails", func() { + BeforeEach(func() { + befSuite = NewBeforeSuiteNode(func() { + failer.Fail("oops", innerCodeLocation) + }, codeLocation, 0, failer) + outcome = befSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(befSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := befSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStateFailed)) + Ω(summary.Failure.Message).Should(Equal("oops")) + Ω(summary.Failure.Location).Should(Equal(innerCodeLocation)) + Ω(summary.Failure.ForwardedPanic).Should(BeNil()) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + + Context("when the body times out", func() { + BeforeEach(func() { + befSuite = NewBeforeSuiteNode(func(done Done) { + }, codeLocation, time.Millisecond, failer) + outcome = befSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(befSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := befSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStateTimedOut)) + Ω(summary.Failure.ForwardedPanic).Should(BeNil()) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + + Context("when the body panics", func() { + BeforeEach(func() { + befSuite = NewBeforeSuiteNode(func() { + panic("bam") + }, codeLocation, 0, failer) + outcome = befSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(befSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := befSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStatePanicked)) + Ω(summary.Failure.ForwardedPanic).Should(Equal("bam")) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + }) + + Describe("AfterSuite nodes", func() { + var aftSuite SuiteNode + var failer *Failer.Failer + var codeLocation types.CodeLocation + var innerCodeLocation types.CodeLocation + var outcome bool + + BeforeEach(func() { + failer = Failer.New() + codeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + }) + + Context("when the body passes", func() { + BeforeEach(func() { + aftSuite = NewAfterSuiteNode(func() { + time.Sleep(10 * time.Millisecond) + }, codeLocation, 0, failer) + outcome = aftSuite.Run(0, 0, "") + }) + + It("should return true when run and report as passed", func() { + Ω(outcome).Should(BeTrue()) + Ω(aftSuite.Passed()).Should(BeTrue()) + }) + + It("should have the correct summary", func() { + summary := aftSuite.Summary() + Ω(summary.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) + Ω(summary.CodeLocation).Should(Equal(codeLocation)) + Ω(summary.State).Should(Equal(types.SpecStatePassed)) + Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) + Ω(summary.Failure).Should(BeZero()) + }) + }) + + Context("when the body fails", func() { + BeforeEach(func() { + aftSuite = NewAfterSuiteNode(func() { + failer.Fail("oops", innerCodeLocation) + }, codeLocation, 0, failer) + outcome = aftSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(aftSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := aftSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStateFailed)) + Ω(summary.Failure.Message).Should(Equal("oops")) + Ω(summary.Failure.Location).Should(Equal(innerCodeLocation)) + Ω(summary.Failure.ForwardedPanic).Should(BeNil()) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + + Context("when the body times out", func() { + BeforeEach(func() { + aftSuite = NewAfterSuiteNode(func(done Done) { + }, codeLocation, time.Millisecond, failer) + outcome = aftSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(aftSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := aftSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStateTimedOut)) + Ω(summary.Failure.ForwardedPanic).Should(BeNil()) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + + Context("when the body panics", func() { + BeforeEach(func() { + aftSuite = NewAfterSuiteNode(func() { + panic("bam") + }, codeLocation, 0, failer) + outcome = aftSuite.Run(0, 0, "") + }) + + It("should return false when run and report as failed", func() { + Ω(outcome).Should(BeFalse()) + Ω(aftSuite.Passed()).Should(BeFalse()) + }) + + It("should have the correct summary", func() { + summary := aftSuite.Summary() + Ω(summary.State).Should(Equal(types.SpecStatePanicked)) + Ω(summary.Failure.ForwardedPanic).Should(Equal("bam")) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node.go new file mode 100644 index 00000000000..e7030d9149a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node.go @@ -0,0 +1,89 @@ +package leafnodes + +import ( + "encoding/json" + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "io/ioutil" + "net/http" + "time" +) + +type synchronizedAfterSuiteNode struct { + runnerA *runner + runnerB *runner + + outcome types.SpecState + failure types.SpecFailure + runTime time.Duration +} + +func NewSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { + return &synchronizedAfterSuiteNode{ + runnerA: newRunner(bodyA, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), + runnerB: newRunner(bodyB, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), + } +} + +func (node *synchronizedAfterSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { + node.outcome, node.failure = node.runnerA.run() + + if parallelNode == 1 { + if parallelTotal > 1 { + node.waitUntilOtherNodesAreDone(syncHost) + } + + outcome, failure := node.runnerB.run() + + if node.outcome == types.SpecStatePassed { + node.outcome, node.failure = outcome, failure + } + } + + return node.outcome == types.SpecStatePassed +} + +func (node *synchronizedAfterSuiteNode) Passed() bool { + return node.outcome == types.SpecStatePassed +} + +func (node *synchronizedAfterSuiteNode) Summary() *types.SetupSummary { + return &types.SetupSummary{ + ComponentType: node.runnerA.nodeType, + CodeLocation: node.runnerA.codeLocation, + State: node.outcome, + RunTime: node.runTime, + Failure: node.failure, + } +} + +func (node *synchronizedAfterSuiteNode) waitUntilOtherNodesAreDone(syncHost string) { + for { + if node.canRun(syncHost) { + return + } + + time.Sleep(50 * time.Millisecond) + } +} + +func (node *synchronizedAfterSuiteNode) canRun(syncHost string) bool { + resp, err := http.Get(syncHost + "/RemoteAfterSuiteData") + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false + } + resp.Body.Close() + + afterSuiteData := types.RemoteAfterSuiteData{} + err = json.Unmarshal(body, &afterSuiteData) + if err != nil { + return false + } + + return afterSuiteData.CanRun +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node_test.go new file mode 100644 index 00000000000..4266a4bce6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_after_suite_node_test.go @@ -0,0 +1,196 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "sync" + + "github.com/onsi/gomega/ghttp" + "net/http" + + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + "time" +) + +var _ = Describe("SynchronizedAfterSuiteNode", func() { + var failer *Failer.Failer + var node SuiteNode + var codeLocation types.CodeLocation + var innerCodeLocation types.CodeLocation + var outcome bool + var server *ghttp.Server + var things []string + var lock *sync.Mutex + + BeforeEach(func() { + things = []string{} + server = ghttp.NewServer() + codeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + failer = Failer.New() + lock = &sync.Mutex{} + }) + + AfterEach(func() { + server.Close() + }) + + newNode := func(bodyA interface{}, bodyB interface{}) SuiteNode { + return NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, time.Millisecond, failer) + } + + ranThing := func(thing string) { + lock.Lock() + defer lock.Unlock() + things = append(things, thing) + } + + thingsThatRan := func() []string { + lock.Lock() + defer lock.Unlock() + return things + } + + Context("when not running in parallel", func() { + Context("when all is well", func() { + BeforeEach(func() { + node = newNode(func() { + ranThing("A") + }, func() { + ranThing("B") + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should run A, then B", func() { + Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) + }) + + It("should report success", func() { + Ω(outcome).Should(BeTrue()) + Ω(node.Passed()).Should(BeTrue()) + Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) + }) + }) + + Context("when A fails", func() { + BeforeEach(func() { + node = newNode(func() { + ranThing("A") + failer.Fail("bam", innerCodeLocation) + }, func() { + ranThing("B") + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should still run B", func() { + Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) + }) + }) + + Context("when B fails", func() { + BeforeEach(func() { + node = newNode(func() { + ranThing("A") + }, func() { + ranThing("B") + failer.Fail("bam", innerCodeLocation) + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should run all the things", func() { + Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) + }) + }) + }) + + Context("when running in parallel", func() { + Context("as the first node", func() { + BeforeEach(func() { + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), + func(writer http.ResponseWriter, request *http.Request) { + ranThing("Request1") + }, + ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{false}), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), + func(writer http.ResponseWriter, request *http.Request) { + ranThing("Request2") + }, + ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{false}), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), + func(writer http.ResponseWriter, request *http.Request) { + ranThing("Request3") + }, + ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{true}), + )) + + node = newNode(func() { + ranThing("A") + }, func() { + ranThing("B") + }) + + outcome = node.Run(1, 3, server.URL()) + }) + + It("should run A and, when the server says its time, run B", func() { + Ω(thingsThatRan()).Should(Equal([]string{"A", "Request1", "Request2", "Request3", "B"})) + }) + + It("should report success", func() { + Ω(outcome).Should(BeTrue()) + Ω(node.Passed()).Should(BeTrue()) + Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) + }) + }) + + Context("as any other node", func() { + BeforeEach(func() { + node = newNode(func() { + ranThing("A") + }, func() { + ranThing("B") + }) + + outcome = node.Run(2, 3, server.URL()) + }) + + It("should run A, and not run B", func() { + Ω(thingsThatRan()).Should(Equal([]string{"A"})) + }) + + It("should not talk to the server", func() { + Ω(server.ReceivedRequests()).Should(BeEmpty()) + }) + + It("should report success", func() { + Ω(outcome).Should(BeTrue()) + Ω(node.Passed()).Should(BeTrue()) + Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node.go new file mode 100644 index 00000000000..76a9679813f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node.go @@ -0,0 +1,182 @@ +package leafnodes + +import ( + "bytes" + "encoding/json" + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "io/ioutil" + "net/http" + "reflect" + "time" +) + +type synchronizedBeforeSuiteNode struct { + runnerA *runner + runnerB *runner + + data []byte + + outcome types.SpecState + failure types.SpecFailure + runTime time.Duration +} + +func NewSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { + node := &synchronizedBeforeSuiteNode{} + + node.runnerA = newRunner(node.wrapA(bodyA), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) + node.runnerB = newRunner(node.wrapB(bodyB), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) + + return node +} + +func (node *synchronizedBeforeSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { + t := time.Now() + defer func() { + node.runTime = time.Since(t) + }() + + if parallelNode == 1 { + node.outcome, node.failure = node.runA(parallelTotal, syncHost) + } else { + node.outcome, node.failure = node.waitForA(syncHost) + } + + if node.outcome != types.SpecStatePassed { + return false + } + node.outcome, node.failure = node.runnerB.run() + + return node.outcome == types.SpecStatePassed +} + +func (node *synchronizedBeforeSuiteNode) runA(parallelTotal int, syncHost string) (types.SpecState, types.SpecFailure) { + outcome, failure := node.runnerA.run() + + if parallelTotal > 1 { + state := types.RemoteBeforeSuiteStatePassed + if outcome != types.SpecStatePassed { + state = types.RemoteBeforeSuiteStateFailed + } + json := (types.RemoteBeforeSuiteData{ + Data: node.data, + State: state, + }).ToJSON() + http.Post(syncHost+"/BeforeSuiteState", "application/json", bytes.NewBuffer(json)) + } + + return outcome, failure +} + +func (node *synchronizedBeforeSuiteNode) waitForA(syncHost string) (types.SpecState, types.SpecFailure) { + failure := func(message string) types.SpecFailure { + return types.SpecFailure{ + Message: message, + Location: node.runnerA.codeLocation, + ComponentType: node.runnerA.nodeType, + ComponentIndex: node.runnerA.componentIndex, + ComponentCodeLocation: node.runnerA.codeLocation, + } + } + for { + resp, err := http.Get(syncHost + "/BeforeSuiteState") + if err != nil || resp.StatusCode != http.StatusOK { + return types.SpecStateFailed, failure("Failed to fetch BeforeSuite state") + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return types.SpecStateFailed, failure("Failed to read BeforeSuite state") + } + resp.Body.Close() + + beforeSuiteData := types.RemoteBeforeSuiteData{} + err = json.Unmarshal(body, &beforeSuiteData) + if err != nil { + return types.SpecStateFailed, failure("Failed to decode BeforeSuite state") + } + + switch beforeSuiteData.State { + case types.RemoteBeforeSuiteStatePassed: + node.data = beforeSuiteData.Data + return types.SpecStatePassed, types.SpecFailure{} + case types.RemoteBeforeSuiteStateFailed: + return types.SpecStateFailed, failure("BeforeSuite on Node 1 failed") + case types.RemoteBeforeSuiteStateDisappeared: + return types.SpecStateFailed, failure("Node 1 disappeared before completing BeforeSuite") + } + + time.Sleep(50 * time.Millisecond) + } + + return types.SpecStateFailed, failure("Shouldn't get here!") +} + +func (node *synchronizedBeforeSuiteNode) Passed() bool { + return node.outcome == types.SpecStatePassed +} + +func (node *synchronizedBeforeSuiteNode) Summary() *types.SetupSummary { + return &types.SetupSummary{ + ComponentType: node.runnerA.nodeType, + CodeLocation: node.runnerA.codeLocation, + State: node.outcome, + RunTime: node.runTime, + Failure: node.failure, + } +} + +func (node *synchronizedBeforeSuiteNode) wrapA(bodyA interface{}) interface{} { + typeA := reflect.TypeOf(bodyA) + if typeA.Kind() != reflect.Func { + panic("SynchronizedBeforeSuite expects a function as its first argument") + } + + takesNothing := typeA.NumIn() == 0 + takesADoneChannel := typeA.NumIn() == 1 && typeA.In(0).Kind() == reflect.Chan && typeA.In(0).Elem().Kind() == reflect.Interface + returnsBytes := typeA.NumOut() == 1 && typeA.Out(0).Kind() == reflect.Slice && typeA.Out(0).Elem().Kind() == reflect.Uint8 + + if !((takesNothing || takesADoneChannel) && returnsBytes) { + panic("SynchronizedBeforeSuite's first argument should be a function that returns []byte and either takes no arguments or takes a Done channel.") + } + + if takesADoneChannel { + return func(done chan<- interface{}) { + out := reflect.ValueOf(bodyA).Call([]reflect.Value{reflect.ValueOf(done)}) + node.data = out[0].Interface().([]byte) + } + } + + return func() { + out := reflect.ValueOf(bodyA).Call([]reflect.Value{}) + node.data = out[0].Interface().([]byte) + } +} + +func (node *synchronizedBeforeSuiteNode) wrapB(bodyB interface{}) interface{} { + typeB := reflect.TypeOf(bodyB) + if typeB.Kind() != reflect.Func { + panic("SynchronizedBeforeSuite expects a function as its second argument") + } + + returnsNothing := typeB.NumOut() == 0 + takesBytesOnly := typeB.NumIn() == 1 && typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 + takesBytesAndDone := typeB.NumIn() == 2 && + typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 && + typeB.In(1).Kind() == reflect.Chan && typeB.In(1).Elem().Kind() == reflect.Interface + + if !((takesBytesOnly || takesBytesAndDone) && returnsNothing) { + panic("SynchronizedBeforeSuite's second argument should be a function that returns nothing and either takes []byte or ([]byte, Done)") + } + + if takesBytesAndDone { + return func(done chan<- interface{}) { + reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data), reflect.ValueOf(done)}) + } + } + + return func() { + reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data)}) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node_test.go new file mode 100644 index 00000000000..dbf2426748a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/leafnodes/synchronized_before_suite_node_test.go @@ -0,0 +1,445 @@ +package leafnodes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/leafnodes" + . "github.com/onsi/gomega" + + "github.com/onsi/gomega/ghttp" + "net/http" + + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/types" + "time" +) + +var _ = Describe("SynchronizedBeforeSuiteNode", func() { + var failer *Failer.Failer + var node SuiteNode + var codeLocation types.CodeLocation + var innerCodeLocation types.CodeLocation + var outcome bool + var server *ghttp.Server + + BeforeEach(func() { + server = ghttp.NewServer() + codeLocation = codelocation.New(0) + innerCodeLocation = codelocation.New(0) + failer = Failer.New() + }) + + AfterEach(func() { + server.Close() + }) + + newNode := func(bodyA interface{}, bodyB interface{}) SuiteNode { + return NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, time.Millisecond, failer) + } + + Describe("when not running in parallel", func() { + Context("when all is well", func() { + var data []byte + BeforeEach(func() { + data = nil + + node = newNode(func() []byte { + return []byte("my data") + }, func(d []byte) { + data = d + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should run A, then B passing the output from A to B", func() { + Ω(data).Should(Equal([]byte("my data"))) + }) + + It("should report success", func() { + Ω(outcome).Should(BeTrue()) + Ω(node.Passed()).Should(BeTrue()) + Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) + }) + }) + + Context("when A fails", func() { + var ranB bool + BeforeEach(func() { + ranB = false + node = newNode(func() []byte { + failer.Fail("boom", innerCodeLocation) + return nil + }, func([]byte) { + ranB = true + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should not run B", func() { + Ω(ranB).Should(BeFalse()) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) + }) + }) + + Context("when B fails", func() { + BeforeEach(func() { + node = newNode(func() []byte { + return nil + }, func([]byte) { + failer.Fail("boom", innerCodeLocation) + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) + }) + }) + + Context("when A times out", func() { + var ranB bool + BeforeEach(func() { + ranB = false + node = newNode(func(Done) []byte { + time.Sleep(time.Second) + return nil + }, func([]byte) { + ranB = true + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should not run B", func() { + Ω(ranB).Should(BeFalse()) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateTimedOut)) + }) + }) + + Context("when B times out", func() { + BeforeEach(func() { + node = newNode(func() []byte { + return nil + }, func([]byte, Done) { + time.Sleep(time.Second) + }) + + outcome = node.Run(1, 1, server.URL()) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + Ω(node.Summary().State).Should(Equal(types.SpecStateTimedOut)) + }) + }) + }) + + Describe("when running in parallel", func() { + var ranB bool + var parallelNode, parallelTotal int + BeforeEach(func() { + ranB = false + parallelNode, parallelTotal = 1, 3 + }) + + Context("as the first node, it runs A", func() { + var expectedState types.RemoteBeforeSuiteData + + BeforeEach(func() { + parallelNode, parallelTotal = 1, 3 + }) + + JustBeforeEach(func() { + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/BeforeSuiteState"), + ghttp.VerifyJSONRepresenting(expectedState), + )) + + outcome = node.Run(parallelNode, parallelTotal, server.URL()) + }) + + Context("when A succeeds", func() { + BeforeEach(func() { + expectedState = types.RemoteBeforeSuiteData{[]byte("my data"), types.RemoteBeforeSuiteStatePassed} + + node = newNode(func() []byte { + return []byte("my data") + }, func([]byte) { + ranB = true + }) + }) + + It("should post about A succeeding", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("should run B", func() { + Ω(ranB).Should(BeTrue()) + }) + + It("should report success", func() { + Ω(outcome).Should(BeTrue()) + }) + }) + + Context("when A fails", func() { + BeforeEach(func() { + expectedState = types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStateFailed} + + node = newNode(func() []byte { + panic("BAM") + return []byte("my data") + }, func([]byte) { + ranB = true + }) + }) + + It("should post about A failing", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("should not run B", func() { + Ω(ranB).Should(BeFalse()) + }) + + It("should report failure", func() { + Ω(outcome).Should(BeFalse()) + }) + }) + }) + + Context("as the Nth node", func() { + var statusCode int + var response interface{} + var ranA bool + var bData []byte + + BeforeEach(func() { + ranA = false + bData = nil + + statusCode = http.StatusOK + + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/BeforeSuiteState"), + ghttp.RespondWith(http.StatusOK, string((types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending}).ToJSON())), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/BeforeSuiteState"), + ghttp.RespondWith(http.StatusOK, string((types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending}).ToJSON())), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/BeforeSuiteState"), + ghttp.RespondWithJSONEncodedPtr(&statusCode, &response), + )) + + node = newNode(func() []byte { + ranA = true + return nil + }, func(data []byte) { + bData = data + }) + + parallelNode, parallelTotal = 2, 3 + }) + + Context("when A on node1 succeeds", func() { + BeforeEach(func() { + response = types.RemoteBeforeSuiteData{[]byte("my data"), types.RemoteBeforeSuiteStatePassed} + outcome = node.Run(parallelNode, parallelTotal, server.URL()) + }) + + It("should not run A", func() { + Ω(ranA).Should(BeFalse()) + }) + + It("should poll for A", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(3)) + }) + + It("should run B when the polling succeeds", func() { + Ω(bData).Should(Equal([]byte("my data"))) + }) + + It("should succeed", func() { + Ω(outcome).Should(BeTrue()) + Ω(node.Passed()).Should(BeTrue()) + }) + }) + + Context("when A on node1 fails", func() { + BeforeEach(func() { + response = types.RemoteBeforeSuiteData{[]byte("my data"), types.RemoteBeforeSuiteStateFailed} + outcome = node.Run(parallelNode, parallelTotal, server.URL()) + }) + + It("should not run A", func() { + Ω(ranA).Should(BeFalse()) + }) + + It("should poll for A", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(3)) + }) + + It("should not run B", func() { + Ω(bData).Should(BeNil()) + }) + + It("should fail", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + + summary := node.Summary() + Ω(summary.State).Should(Equal(types.SpecStateFailed)) + Ω(summary.Failure.Message).Should(Equal("BeforeSuite on Node 1 failed")) + Ω(summary.Failure.Location).Should(Equal(codeLocation)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + + Context("when node1 disappears", func() { + BeforeEach(func() { + response = types.RemoteBeforeSuiteData{[]byte("my data"), types.RemoteBeforeSuiteStateDisappeared} + outcome = node.Run(parallelNode, parallelTotal, server.URL()) + }) + + It("should not run A", func() { + Ω(ranA).Should(BeFalse()) + }) + + It("should poll for A", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(3)) + }) + + It("should not run B", func() { + Ω(bData).Should(BeNil()) + }) + + It("should fail", func() { + Ω(outcome).Should(BeFalse()) + Ω(node.Passed()).Should(BeFalse()) + + summary := node.Summary() + Ω(summary.State).Should(Equal(types.SpecStateFailed)) + Ω(summary.Failure.Message).Should(Equal("Node 1 disappeared before completing BeforeSuite")) + Ω(summary.Failure.Location).Should(Equal(codeLocation)) + Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) + Ω(summary.Failure.ComponentIndex).Should(Equal(0)) + Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) + }) + }) + }) + }) + + Describe("construction", func() { + Describe("the first function", func() { + Context("when the first function returns a byte array", func() { + Context("and takes nothing", func() { + It("should be fine", func() { + Ω(func() { + newNode(func() []byte { return nil }, func([]byte) {}) + }).ShouldNot(Panic()) + }) + }) + + Context("and takes a done function", func() { + It("should be fine", func() { + Ω(func() { + newNode(func(Done) []byte { return nil }, func([]byte) {}) + }).ShouldNot(Panic()) + }) + }) + + Context("and takes more than one thing", func() { + It("should panic", func() { + Ω(func() { + newNode(func(Done, Done) []byte { return nil }, func([]byte) {}) + }).Should(Panic()) + }) + }) + + Context("and takes something else", func() { + It("should panic", func() { + Ω(func() { + newNode(func(bool) []byte { return nil }, func([]byte) {}) + }).Should(Panic()) + }) + }) + }) + + Context("when the first function does not return a byte array", func() { + It("should panic", func() { + Ω(func() { + newNode(func() {}, func([]byte) {}) + }).Should(Panic()) + + Ω(func() { + newNode(func() []int { return nil }, func([]byte) {}) + }).Should(Panic()) + }) + }) + }) + + Describe("the second function", func() { + Context("when the second function takes a byte array", func() { + It("should be fine", func() { + Ω(func() { + newNode(func() []byte { return nil }, func([]byte) {}) + }).ShouldNot(Panic()) + }) + }) + + Context("when it also takes a done channel", func() { + It("should be fine", func() { + Ω(func() { + newNode(func() []byte { return nil }, func([]byte, Done) {}) + }).ShouldNot(Panic()) + }) + }) + + Context("if it takes anything else", func() { + It("should panic", func() { + Ω(func() { + newNode(func() []byte { return nil }, func([]byte, chan bool) {}) + }).Should(Panic()) + + Ω(func() { + newNode(func() []byte { return nil }, func(string) {}) + }).Should(Panic()) + }) + }) + + Context("if it takes nothing at all", func() { + It("should panic", func() { + Ω(func() { + newNode(func() []byte { return nil }, func() {}) + }).Should(Panic()) + }) + }) + + Context("if it returns something", func() { + It("should panic", func() { + Ω(func() { + newNode(func() []byte { return nil }, func([]byte) []byte { return nil }) + }).Should(Panic()) + }) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator.go new file mode 100644 index 00000000000..9dcfb5fe8bf --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator.go @@ -0,0 +1,250 @@ +/* + +Aggregator is a reporter used by the Ginkgo CLI to aggregate and present parallel test output +coherently as tests complete. You shouldn't need to use this in your code. To run tests in parallel: + + ginkgo -nodes=N + +where N is the number of nodes you desire. +*/ +package remote + +import ( + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" +) + +type configAndSuite struct { + config config.GinkgoConfigType + summary *types.SuiteSummary +} + +type Aggregator struct { + nodeCount int + config config.DefaultReporterConfigType + stenographer stenographer.Stenographer + result chan bool + + suiteBeginnings chan configAndSuite + aggregatedSuiteBeginnings []configAndSuite + + beforeSuites chan *types.SetupSummary + aggregatedBeforeSuites []*types.SetupSummary + + afterSuites chan *types.SetupSummary + aggregatedAfterSuites []*types.SetupSummary + + specCompletions chan *types.SpecSummary + completedSpecs []*types.SpecSummary + + suiteEndings chan *types.SuiteSummary + aggregatedSuiteEndings []*types.SuiteSummary + specs []*types.SpecSummary + + startTime time.Time +} + +func NewAggregator(nodeCount int, result chan bool, config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *Aggregator { + aggregator := &Aggregator{ + nodeCount: nodeCount, + result: result, + config: config, + stenographer: stenographer, + + suiteBeginnings: make(chan configAndSuite, 0), + beforeSuites: make(chan *types.SetupSummary, 0), + afterSuites: make(chan *types.SetupSummary, 0), + specCompletions: make(chan *types.SpecSummary, 0), + suiteEndings: make(chan *types.SuiteSummary, 0), + } + + go aggregator.mux() + + return aggregator +} + +func (aggregator *Aggregator) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + aggregator.suiteBeginnings <- configAndSuite{config, summary} +} + +func (aggregator *Aggregator) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + aggregator.beforeSuites <- setupSummary +} + +func (aggregator *Aggregator) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + aggregator.afterSuites <- setupSummary +} + +func (aggregator *Aggregator) SpecWillRun(specSummary *types.SpecSummary) { + //noop +} + +func (aggregator *Aggregator) SpecDidComplete(specSummary *types.SpecSummary) { + aggregator.specCompletions <- specSummary +} + +func (aggregator *Aggregator) SpecSuiteDidEnd(summary *types.SuiteSummary) { + aggregator.suiteEndings <- summary +} + +func (aggregator *Aggregator) mux() { +loop: + for { + select { + case configAndSuite := <-aggregator.suiteBeginnings: + aggregator.registerSuiteBeginning(configAndSuite) + case setupSummary := <-aggregator.beforeSuites: + aggregator.registerBeforeSuite(setupSummary) + case setupSummary := <-aggregator.afterSuites: + aggregator.registerAfterSuite(setupSummary) + case specSummary := <-aggregator.specCompletions: + aggregator.registerSpecCompletion(specSummary) + case suite := <-aggregator.suiteEndings: + finished, passed := aggregator.registerSuiteEnding(suite) + if finished { + aggregator.result <- passed + break loop + } + } + } +} + +func (aggregator *Aggregator) registerSuiteBeginning(configAndSuite configAndSuite) { + aggregator.aggregatedSuiteBeginnings = append(aggregator.aggregatedSuiteBeginnings, configAndSuite) + + if len(aggregator.aggregatedSuiteBeginnings) == 1 { + aggregator.startTime = time.Now() + } + + if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { + return + } + + aggregator.stenographer.AnnounceSuite(configAndSuite.summary.SuiteDescription, configAndSuite.config.RandomSeed, configAndSuite.config.RandomizeAllSpecs, aggregator.config.Succinct) + + numberOfSpecsToRun := 0 + totalNumberOfSpecs := 0 + for _, configAndSuite := range aggregator.aggregatedSuiteBeginnings { + numberOfSpecsToRun += configAndSuite.summary.NumberOfSpecsThatWillBeRun + totalNumberOfSpecs += configAndSuite.summary.NumberOfTotalSpecs + } + + aggregator.stenographer.AnnounceNumberOfSpecs(numberOfSpecsToRun, totalNumberOfSpecs, aggregator.config.Succinct) + aggregator.stenographer.AnnounceAggregatedParallelRun(aggregator.nodeCount, aggregator.config.Succinct) + aggregator.flushCompletedSpecs() +} + +func (aggregator *Aggregator) registerBeforeSuite(setupSummary *types.SetupSummary) { + aggregator.aggregatedBeforeSuites = append(aggregator.aggregatedBeforeSuites, setupSummary) + aggregator.flushCompletedSpecs() +} + +func (aggregator *Aggregator) registerAfterSuite(setupSummary *types.SetupSummary) { + aggregator.aggregatedAfterSuites = append(aggregator.aggregatedAfterSuites, setupSummary) + aggregator.flushCompletedSpecs() +} + +func (aggregator *Aggregator) registerSpecCompletion(specSummary *types.SpecSummary) { + aggregator.completedSpecs = append(aggregator.completedSpecs, specSummary) + aggregator.specs = append(aggregator.specs, specSummary) + aggregator.flushCompletedSpecs() +} + +func (aggregator *Aggregator) flushCompletedSpecs() { + if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { + return + } + + for _, setupSummary := range aggregator.aggregatedBeforeSuites { + aggregator.announceBeforeSuite(setupSummary) + } + + for _, specSummary := range aggregator.completedSpecs { + aggregator.announceSpec(specSummary) + } + + for _, setupSummary := range aggregator.aggregatedAfterSuites { + aggregator.announceAfterSuite(setupSummary) + } + + aggregator.aggregatedBeforeSuites = []*types.SetupSummary{} + aggregator.completedSpecs = []*types.SpecSummary{} + aggregator.aggregatedAfterSuites = []*types.SetupSummary{} +} + +func (aggregator *Aggregator) announceBeforeSuite(setupSummary *types.SetupSummary) { + aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) + if setupSummary.State != types.SpecStatePassed { + aggregator.stenographer.AnnounceBeforeSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) + } +} + +func (aggregator *Aggregator) announceAfterSuite(setupSummary *types.SetupSummary) { + aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) + if setupSummary.State != types.SpecStatePassed { + aggregator.stenographer.AnnounceAfterSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) + } +} + +func (aggregator *Aggregator) announceSpec(specSummary *types.SpecSummary) { + if aggregator.config.Verbose && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { + aggregator.stenographer.AnnounceSpecWillRun(specSummary) + } + + aggregator.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput) + + switch specSummary.State { + case types.SpecStatePassed: + if specSummary.IsMeasurement { + aggregator.stenographer.AnnounceSuccesfulMeasurement(specSummary, aggregator.config.Succinct) + } else if specSummary.RunTime.Seconds() >= aggregator.config.SlowSpecThreshold { + aggregator.stenographer.AnnounceSuccesfulSlowSpec(specSummary, aggregator.config.Succinct) + } else { + aggregator.stenographer.AnnounceSuccesfulSpec(specSummary) + } + + case types.SpecStatePending: + aggregator.stenographer.AnnouncePendingSpec(specSummary, aggregator.config.NoisyPendings && !aggregator.config.Succinct) + case types.SpecStateSkipped: + aggregator.stenographer.AnnounceSkippedSpec(specSummary) + case types.SpecStateTimedOut: + aggregator.stenographer.AnnounceSpecTimedOut(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) + case types.SpecStatePanicked: + aggregator.stenographer.AnnounceSpecPanicked(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) + case types.SpecStateFailed: + aggregator.stenographer.AnnounceSpecFailed(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) + } +} + +func (aggregator *Aggregator) registerSuiteEnding(suite *types.SuiteSummary) (finished bool, passed bool) { + aggregator.aggregatedSuiteEndings = append(aggregator.aggregatedSuiteEndings, suite) + if len(aggregator.aggregatedSuiteEndings) < aggregator.nodeCount { + return false, false + } + + aggregatedSuiteSummary := &types.SuiteSummary{} + aggregatedSuiteSummary.SuiteSucceeded = true + + for _, suiteSummary := range aggregator.aggregatedSuiteEndings { + if suiteSummary.SuiteSucceeded == false { + aggregatedSuiteSummary.SuiteSucceeded = false + } + + aggregatedSuiteSummary.NumberOfSpecsThatWillBeRun += suiteSummary.NumberOfSpecsThatWillBeRun + aggregatedSuiteSummary.NumberOfTotalSpecs += suiteSummary.NumberOfTotalSpecs + aggregatedSuiteSummary.NumberOfPassedSpecs += suiteSummary.NumberOfPassedSpecs + aggregatedSuiteSummary.NumberOfFailedSpecs += suiteSummary.NumberOfFailedSpecs + aggregatedSuiteSummary.NumberOfPendingSpecs += suiteSummary.NumberOfPendingSpecs + aggregatedSuiteSummary.NumberOfSkippedSpecs += suiteSummary.NumberOfSkippedSpecs + } + + aggregatedSuiteSummary.RunTime = time.Since(aggregator.startTime) + + aggregator.stenographer.SummarizeFailures(aggregator.specs) + aggregator.stenographer.AnnounceSpecRunCompletion(aggregatedSuiteSummary, aggregator.config.Succinct) + + return true, aggregatedSuiteSummary.SuiteSucceeded +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator_test.go new file mode 100644 index 00000000000..d8499cf378a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/aggregator_test.go @@ -0,0 +1,311 @@ +package remote_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "time" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/internal/remote" + st "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Aggregator", func() { + var ( + aggregator *Aggregator + reporterConfig config.DefaultReporterConfigType + stenographer *st.FakeStenographer + result chan bool + + ginkgoConfig1 config.GinkgoConfigType + ginkgoConfig2 config.GinkgoConfigType + + suiteSummary1 *types.SuiteSummary + suiteSummary2 *types.SuiteSummary + + beforeSummary *types.SetupSummary + afterSummary *types.SetupSummary + specSummary *types.SpecSummary + + suiteDescription string + ) + + BeforeEach(func() { + reporterConfig = config.DefaultReporterConfigType{ + NoColor: false, + SlowSpecThreshold: 0.1, + NoisyPendings: true, + Succinct: false, + Verbose: true, + } + stenographer = st.NewFakeStenographer() + result = make(chan bool, 1) + aggregator = NewAggregator(2, result, reporterConfig, stenographer) + + // + // now set up some fixture data + // + + ginkgoConfig1 = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + ParallelNode: 1, + ParallelTotal: 2, + } + + ginkgoConfig2 = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + ParallelNode: 2, + ParallelTotal: 2, + } + + suiteDescription = "My Parallel Suite" + + suiteSummary1 = &types.SuiteSummary{ + SuiteDescription: suiteDescription, + + NumberOfSpecsBeforeParallelization: 30, + NumberOfTotalSpecs: 17, + NumberOfSpecsThatWillBeRun: 15, + NumberOfPendingSpecs: 1, + NumberOfSkippedSpecs: 1, + } + + suiteSummary2 = &types.SuiteSummary{ + SuiteDescription: suiteDescription, + + NumberOfSpecsBeforeParallelization: 30, + NumberOfTotalSpecs: 13, + NumberOfSpecsThatWillBeRun: 8, + NumberOfPendingSpecs: 2, + NumberOfSkippedSpecs: 3, + } + + beforeSummary = &types.SetupSummary{ + State: types.SpecStatePassed, + CapturedOutput: "BeforeSuiteOutput", + } + + afterSummary = &types.SetupSummary{ + State: types.SpecStatePassed, + CapturedOutput: "AfterSuiteOutput", + } + + specSummary = &types.SpecSummary{ + State: types.SpecStatePassed, + CapturedOutput: "SpecOutput", + } + }) + + call := func(method string, args ...interface{}) st.FakeStenographerCall { + return st.NewFakeStenographerCall(method, args...) + } + + beginSuite := func() { + stenographer.Reset() + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) + Eventually(func() interface{} { + return len(stenographer.Calls()) + }).Should(BeNumerically(">=", 3)) + } + + Describe("Announcing the beginning of the suite", func() { + Context("When one of the parallel-suites starts", func() { + BeforeEach(func() { + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + }) + + It("should be silent", func() { + Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) + }) + }) + + Context("once all of the parallel-suites have started", func() { + BeforeEach(func() { + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(HaveLen(3)) + }) + + It("should announce the beginning of the suite", func() { + Ω(stenographer.Calls()).Should(HaveLen(3)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", suiteDescription, ginkgoConfig1.RandomSeed, true, false))) + Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceNumberOfSpecs", 23, 30, false))) + Ω(stenographer.Calls()[2]).Should(Equal(call("AnnounceAggregatedParallelRun", 2, false))) + }) + }) + }) + + Describe("Announcing specs and before suites", func() { + Context("when the parallel-suites have not all started", func() { + BeforeEach(func() { + aggregator.BeforeSuiteDidRun(beforeSummary) + aggregator.AfterSuiteDidRun(afterSummary) + aggregator.SpecDidComplete(specSummary) + }) + + It("should not announce any specs", func() { + Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) + }) + + Context("when the parallel-suites subsequently start", func() { + BeforeEach(func() { + beginSuite() + }) + + It("should announce the specs, the before suites and the after suites", func() { + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(ContainElement(call("AnnounceSuccesfulSpec", specSummary))) + + Ω(stenographer.Calls()).Should(ContainElement(call("AnnounceCapturedOutput", beforeSummary.CapturedOutput))) + Ω(stenographer.Calls()).Should(ContainElement(call("AnnounceCapturedOutput", afterSummary.CapturedOutput))) + }) + }) + }) + + Context("When the parallel-suites have all started", func() { + BeforeEach(func() { + beginSuite() + stenographer.Reset() + }) + + Context("When a spec completes", func() { + BeforeEach(func() { + aggregator.BeforeSuiteDidRun(beforeSummary) + aggregator.SpecDidComplete(specSummary) + aggregator.AfterSuiteDidRun(afterSummary) + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(HaveLen(5)) + }) + + It("should announce the captured output of the BeforeSuite", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceCapturedOutput", beforeSummary.CapturedOutput))) + }) + + It("should announce that the spec will run (when in verbose mode)", func() { + Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceSpecWillRun", specSummary))) + }) + + It("should announce the captured stdout of the spec", func() { + Ω(stenographer.Calls()[2]).Should(Equal(call("AnnounceCapturedOutput", specSummary.CapturedOutput))) + }) + + It("should announce completion", func() { + Ω(stenographer.Calls()[3]).Should(Equal(call("AnnounceSuccesfulSpec", specSummary))) + }) + + It("should announce the captured output of the AfterSuite", func() { + Ω(stenographer.Calls()[4]).Should(Equal(call("AnnounceCapturedOutput", afterSummary.CapturedOutput))) + }) + }) + }) + }) + + Describe("Announcing the end of the suite", func() { + BeforeEach(func() { + beginSuite() + stenographer.Reset() + }) + + Context("When one of the parallel-suites ends", func() { + BeforeEach(func() { + aggregator.SpecSuiteDidEnd(suiteSummary2) + }) + + It("should be silent", func() { + Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) + }) + + It("should not notify the channel", func() { + Ω(result).Should(BeEmpty()) + }) + }) + + Context("once all of the parallel-suites end", func() { + BeforeEach(func() { + time.Sleep(200 * time.Millisecond) + + suiteSummary1.SuiteSucceeded = true + suiteSummary1.NumberOfPassedSpecs = 15 + suiteSummary1.NumberOfFailedSpecs = 0 + suiteSummary2.SuiteSucceeded = false + suiteSummary2.NumberOfPassedSpecs = 5 + suiteSummary2.NumberOfFailedSpecs = 3 + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(HaveLen(2)) + }) + + It("should announce the end of the suite", func() { + compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) + Ω(compositeSummary.NumberOfSpecsThatWillBeRun).Should(Equal(23)) + Ω(compositeSummary.NumberOfTotalSpecs).Should(Equal(30)) + Ω(compositeSummary.NumberOfPassedSpecs).Should(Equal(20)) + Ω(compositeSummary.NumberOfFailedSpecs).Should(Equal(3)) + Ω(compositeSummary.NumberOfPendingSpecs).Should(Equal(3)) + Ω(compositeSummary.NumberOfSkippedSpecs).Should(Equal(4)) + Ω(compositeSummary.RunTime.Seconds()).Should(BeNumerically(">", 0.2)) + }) + }) + + Context("when all the parallel-suites pass", func() { + BeforeEach(func() { + suiteSummary1.SuiteSucceeded = true + suiteSummary2.SuiteSucceeded = true + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(HaveLen(2)) + }) + + It("should report success", func() { + compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeTrue()) + }) + + It("should notify the channel that it succeded", func(done Done) { + Ω(<-result).Should(BeTrue()) + close(done) + }) + }) + + Context("when one of the parallel-suites fails", func() { + BeforeEach(func() { + suiteSummary1.SuiteSucceeded = true + suiteSummary2.SuiteSucceeded = false + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls() + }).Should(HaveLen(2)) + }) + + It("should report failure", func() { + compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) + }) + + It("should notify the channel that it failed", func(done Done) { + Ω(<-result).Should(BeFalse()) + close(done) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_output_interceptor_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_output_interceptor_test.go new file mode 100644 index 00000000000..a928f93d311 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_output_interceptor_test.go @@ -0,0 +1,17 @@ +package remote_test + +type fakeOutputInterceptor struct { + DidStartInterceptingOutput bool + DidStopInterceptingOutput bool + InterceptedOutput string +} + +func (interceptor *fakeOutputInterceptor) StartInterceptingOutput() error { + interceptor.DidStartInterceptingOutput = true + return nil +} + +func (interceptor *fakeOutputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + interceptor.DidStopInterceptingOutput = true + return interceptor.InterceptedOutput, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_poster_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_poster_test.go new file mode 100644 index 00000000000..3543c59c64c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/fake_poster_test.go @@ -0,0 +1,33 @@ +package remote_test + +import ( + "io" + "io/ioutil" + "net/http" +) + +type post struct { + url string + bodyType string + bodyContent []byte +} + +type fakePoster struct { + posts []post +} + +func newFakePoster() *fakePoster { + return &fakePoster{ + posts: make([]post, 0), + } +} + +func (poster *fakePoster) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + bodyContent, _ := ioutil.ReadAll(body) + poster.posts = append(poster.posts, post{ + url: url, + bodyType: bodyType, + bodyContent: bodyContent, + }) + return nil, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter.go new file mode 100644 index 00000000000..025eb506448 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter.go @@ -0,0 +1,90 @@ +package remote + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +//An interface to net/http's client to allow the injection of fakes under test +type Poster interface { + Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) +} + +/* +The ForwardingReporter is a Ginkgo reporter that forwards information to +a Ginkgo remote server. + +When streaming parallel test output, this repoter is automatically installed by Ginkgo. + +This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner +detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter +in place of Ginkgo's DefaultReporter. +*/ + +type ForwardingReporter struct { + serverHost string + poster Poster + outputInterceptor OutputInterceptor +} + +func NewForwardingReporter(serverHost string, poster Poster, outputInterceptor OutputInterceptor) *ForwardingReporter { + return &ForwardingReporter{ + serverHost: serverHost, + poster: poster, + outputInterceptor: outputInterceptor, + } +} + +func (reporter *ForwardingReporter) post(path string, data interface{}) { + encoded, _ := json.Marshal(data) + buffer := bytes.NewBuffer(encoded) + reporter.poster.Post(reporter.serverHost+path, "application/json", buffer) +} + +func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary *types.SuiteSummary) { + data := struct { + Config config.GinkgoConfigType `json:"config"` + Summary *types.SuiteSummary `json:"suite-summary"` + }{ + conf, + summary, + } + + reporter.outputInterceptor.StartInterceptingOutput() + reporter.post("/SpecSuiteWillBegin", data) +} + +func (reporter *ForwardingReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() + reporter.outputInterceptor.StartInterceptingOutput() + setupSummary.CapturedOutput = output + reporter.post("/BeforeSuiteDidRun", setupSummary) +} + +func (reporter *ForwardingReporter) SpecWillRun(specSummary *types.SpecSummary) { + reporter.post("/SpecWillRun", specSummary) +} + +func (reporter *ForwardingReporter) SpecDidComplete(specSummary *types.SpecSummary) { + output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() + reporter.outputInterceptor.StartInterceptingOutput() + specSummary.CapturedOutput = output + reporter.post("/SpecDidComplete", specSummary) +} + +func (reporter *ForwardingReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() + reporter.outputInterceptor.StartInterceptingOutput() + setupSummary.CapturedOutput = output + reporter.post("/AfterSuiteDidRun", setupSummary) +} + +func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + reporter.outputInterceptor.StopInterceptingAndReturnOutput() + reporter.post("/SpecSuiteDidEnd", summary) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter_test.go new file mode 100644 index 00000000000..e5f3b1e3071 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/forwarding_reporter_test.go @@ -0,0 +1,180 @@ +package remote_test + +import ( + "encoding/json" + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/internal/remote" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("ForwardingReporter", func() { + var ( + reporter *ForwardingReporter + interceptor *fakeOutputInterceptor + poster *fakePoster + suiteSummary *types.SuiteSummary + specSummary *types.SpecSummary + setupSummary *types.SetupSummary + serverHost string + ) + + BeforeEach(func() { + serverHost = "http://127.0.0.1:7788" + + poster = newFakePoster() + + interceptor = &fakeOutputInterceptor{ + InterceptedOutput: "The intercepted output!", + } + + reporter = NewForwardingReporter(serverHost, poster, interceptor) + + suiteSummary = &types.SuiteSummary{ + SuiteDescription: "My Test Suite", + } + + setupSummary = &types.SetupSummary{ + State: types.SpecStatePassed, + } + + specSummary = &types.SpecSummary{ + ComponentTexts: []string{"My", "Spec"}, + State: types.SpecStatePassed, + } + }) + + Context("When a suite begins", func() { + BeforeEach(func() { + reporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) + }) + + It("should start intercepting output", func() { + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + It("should POST the SuiteSummary and Ginkgo Config to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteWillBegin")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var sentData struct { + SentConfig config.GinkgoConfigType `json:"config"` + SentSuiteSummary *types.SuiteSummary `json:"suite-summary"` + } + + err := json.Unmarshal(poster.posts[0].bodyContent, &sentData) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(sentData.SentConfig).Should(Equal(config.GinkgoConfig)) + Ω(sentData.SentSuiteSummary).Should(Equal(suiteSummary)) + }) + }) + + Context("when a BeforeSuite completes", func() { + BeforeEach(func() { + reporter.BeforeSuiteDidRun(setupSummary) + }) + + It("should stop, then start intercepting output", func() { + Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + It("should POST the SetupSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/BeforeSuiteDidRun")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.SetupSummary + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + setupSummary.CapturedOutput = interceptor.InterceptedOutput + Ω(summary).Should(Equal(setupSummary)) + }) + }) + + Context("when an AfterSuite completes", func() { + BeforeEach(func() { + reporter.AfterSuiteDidRun(setupSummary) + }) + + It("should stop, then start intercepting output", func() { + Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + It("should POST the SetupSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/AfterSuiteDidRun")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.SetupSummary + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + setupSummary.CapturedOutput = interceptor.InterceptedOutput + Ω(summary).Should(Equal(setupSummary)) + }) + }) + + Context("When a spec will run", func() { + BeforeEach(func() { + reporter.SpecWillRun(specSummary) + }) + + It("should POST the SpecSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecWillRun")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.SpecSummary + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + Ω(summary).Should(Equal(specSummary)) + }) + + Context("When a spec completes", func() { + BeforeEach(func() { + specSummary.State = types.SpecStatePanicked + reporter.SpecDidComplete(specSummary) + }) + + It("should POST the SpecSummary to the Ginkgo server and include any intercepted output", func() { + Ω(poster.posts).Should(HaveLen(2)) + Ω(poster.posts[1].url).Should(Equal("http://127.0.0.1:7788/SpecDidComplete")) + Ω(poster.posts[1].bodyType).Should(Equal("application/json")) + + var summary *types.SpecSummary + err := json.Unmarshal(poster.posts[1].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + specSummary.CapturedOutput = interceptor.InterceptedOutput + Ω(summary).Should(Equal(specSummary)) + }) + + It("should stop, then start intercepting output", func() { + Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + }) + }) + + Context("When a suite ends", func() { + BeforeEach(func() { + reporter.SpecSuiteDidEnd(suiteSummary) + }) + + It("should POST the SuiteSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteDidEnd")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.SuiteSummary + + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(summary).Should(Equal(suiteSummary)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor.go new file mode 100644 index 00000000000..093f4513c0b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor.go @@ -0,0 +1,10 @@ +package remote + +/* +The OutputInterceptor is used by the ForwardingReporter to +intercept and capture all stdin and stderr output during a test run. +*/ +type OutputInterceptor interface { + StartInterceptingOutput() error + StopInterceptingAndReturnOutput() (string, error) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_test.go new file mode 100644 index 00000000000..014fdf1acda --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_test.go @@ -0,0 +1,64 @@ +package remote_test + +import ( + "fmt" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/remote" + . "github.com/onsi/gomega" + "os" +) + +var _ = Describe("OutputInterceptor", func() { + var interceptor OutputInterceptor + + BeforeEach(func() { + interceptor = NewOutputInterceptor() + }) + + It("should capture all stdout/stderr output", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + + fmt.Fprint(os.Stdout, "STDOUT") + fmt.Fprint(os.Stderr, "STDERR") + print("PRINT") + + output, err := interceptor.StopInterceptingAndReturnOutput() + + Ω(output).Should(Equal("STDOUTSTDERRPRINT")) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should error if told to intercept output twice", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + + print("A") + + err = interceptor.StartInterceptingOutput() + Ω(err).Should(HaveOccurred()) + + print("B") + + output, err := interceptor.StopInterceptingAndReturnOutput() + + Ω(output).Should(Equal("AB")) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should allow multiple interception sessions", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + print("A") + output, err := interceptor.StopInterceptingAndReturnOutput() + Ω(output).Should(Equal("A")) + Ω(err).ShouldNot(HaveOccurred()) + + err = interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + print("B") + output, err = interceptor.StopInterceptingAndReturnOutput() + Ω(output).Should(Equal("B")) + Ω(err).ShouldNot(HaveOccurred()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_unix.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_unix.go new file mode 100644 index 00000000000..8304cf5a97b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_unix.go @@ -0,0 +1,76 @@ +// +build freebsd openbsd netbsd dragonfly darwin linux + +package remote + +import ( + "errors" + "io/ioutil" + "os" + "syscall" +) + +func NewOutputInterceptor() OutputInterceptor { + return &outputInterceptor{} +} + +type outputInterceptor struct { + stdoutPlaceholder *os.File + stderrPlaceholder *os.File + redirectFile *os.File + intercepting bool +} + +func (interceptor *outputInterceptor) StartInterceptingOutput() error { + if interceptor.intercepting { + return errors.New("Already intercepting output!") + } + interceptor.intercepting = true + + var err error + + interceptor.redirectFile, err = ioutil.TempFile("", "ginkgo-output") + if err != nil { + return err + } + + interceptor.stdoutPlaceholder, err = ioutil.TempFile("", "ginkgo-output") + if err != nil { + return err + } + + interceptor.stderrPlaceholder, err = ioutil.TempFile("", "ginkgo-output") + if err != nil { + return err + } + + syscall.Dup2(1, int(interceptor.stdoutPlaceholder.Fd())) + syscall.Dup2(2, int(interceptor.stderrPlaceholder.Fd())) + + syscall.Dup2(int(interceptor.redirectFile.Fd()), 1) + syscall.Dup2(int(interceptor.redirectFile.Fd()), 2) + + return nil +} + +func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + if !interceptor.intercepting { + return "", errors.New("Not intercepting output!") + } + + syscall.Dup2(int(interceptor.stdoutPlaceholder.Fd()), 1) + syscall.Dup2(int(interceptor.stderrPlaceholder.Fd()), 2) + + for _, f := range []*os.File{interceptor.redirectFile, interceptor.stdoutPlaceholder, interceptor.stderrPlaceholder} { + f.Close() + } + + output, err := ioutil.ReadFile(interceptor.redirectFile.Name()) + + for _, f := range []*os.File{interceptor.redirectFile, interceptor.stdoutPlaceholder, interceptor.stderrPlaceholder} { + os.Remove(f.Name()) + } + + interceptor.intercepting = false + + return string(output), err +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_win.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_win.go new file mode 100644 index 00000000000..c8f97d97f07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/output_interceptor_win.go @@ -0,0 +1,33 @@ +// +build windows + +package remote + +import ( + "errors" +) + +func NewOutputInterceptor() OutputInterceptor { + return &outputInterceptor{} +} + +type outputInterceptor struct { + intercepting bool +} + +func (interceptor *outputInterceptor) StartInterceptingOutput() error { + if interceptor.intercepting { + return errors.New("Already intercepting output!") + } + interceptor.intercepting = true + + // not working on windows... + + return nil +} + +func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + // not working on windows... + interceptor.intercepting = false + + return "", nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/remote_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/remote_suite_test.go new file mode 100644 index 00000000000..e6b4e9f32ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/remote_suite_test.go @@ -0,0 +1,13 @@ +package remote_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRemote(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Remote Spec Forwarding Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server.go new file mode 100644 index 00000000000..b55c681bcff --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server.go @@ -0,0 +1,204 @@ +/* + +The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners. +This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser). + +*/ + +package remote + +import ( + "encoding/json" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + "io/ioutil" + "net" + "net/http" + "sync" +) + +/* +Server spins up on an automatically selected port and listens for communication from the forwarding reporter. +It then forwards that communication to attached reporters. +*/ +type Server struct { + listener net.Listener + reporters []reporters.Reporter + alives []func() bool + lock *sync.Mutex + beforeSuiteData types.RemoteBeforeSuiteData + parallelTotal int +} + +//Create a new server, automatically selecting a port +func NewServer(parallelTotal int) (*Server, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + return &Server{ + listener: listener, + lock: &sync.Mutex{}, + alives: make([]func() bool, parallelTotal), + beforeSuiteData: types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending}, + parallelTotal: parallelTotal, + }, nil +} + +//Start the server. You don't need to `go s.Start()`, just `s.Start()` +func (server *Server) Start() { + httpServer := &http.Server{} + mux := http.NewServeMux() + httpServer.Handler = mux + + //streaming endpoints + mux.HandleFunc("/SpecSuiteWillBegin", server.specSuiteWillBegin) + mux.HandleFunc("/BeforeSuiteDidRun", server.beforeSuiteDidRun) + mux.HandleFunc("/AfterSuiteDidRun", server.afterSuiteDidRun) + mux.HandleFunc("/SpecWillRun", server.specWillRun) + mux.HandleFunc("/SpecDidComplete", server.specDidComplete) + mux.HandleFunc("/SpecSuiteDidEnd", server.specSuiteDidEnd) + + //synchronization endpoints + mux.HandleFunc("/BeforeSuiteState", server.handleBeforeSuiteState) + mux.HandleFunc("/RemoteAfterSuiteData", server.handleRemoteAfterSuiteData) + + go httpServer.Serve(server.listener) +} + +//Stop the server +func (server *Server) Close() { + server.listener.Close() +} + +//The address the server can be reached it. Pass this into the `ForwardingReporter`. +func (server *Server) Address() string { + return "http://" + server.listener.Addr().String() +} + +// +// Streaming Endpoints +// + +//The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters` +func (server *Server) readAll(request *http.Request) []byte { + defer request.Body.Close() + body, _ := ioutil.ReadAll(request.Body) + return body +} + +func (server *Server) RegisterReporters(reporters ...reporters.Reporter) { + server.reporters = reporters +} + +func (server *Server) specSuiteWillBegin(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + + var data struct { + Config config.GinkgoConfigType `json:"config"` + Summary *types.SuiteSummary `json:"suite-summary"` + } + + json.Unmarshal(body, &data) + + for _, reporter := range server.reporters { + reporter.SpecSuiteWillBegin(data.Config, data.Summary) + } +} + +func (server *Server) beforeSuiteDidRun(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + var setupSummary *types.SetupSummary + json.Unmarshal(body, &setupSummary) + + for _, reporter := range server.reporters { + reporter.BeforeSuiteDidRun(setupSummary) + } +} + +func (server *Server) afterSuiteDidRun(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + var setupSummary *types.SetupSummary + json.Unmarshal(body, &setupSummary) + + for _, reporter := range server.reporters { + reporter.AfterSuiteDidRun(setupSummary) + } +} + +func (server *Server) specWillRun(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + var specSummary *types.SpecSummary + json.Unmarshal(body, &specSummary) + + for _, reporter := range server.reporters { + reporter.SpecWillRun(specSummary) + } +} + +func (server *Server) specDidComplete(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + var specSummary *types.SpecSummary + json.Unmarshal(body, &specSummary) + + for _, reporter := range server.reporters { + reporter.SpecDidComplete(specSummary) + } +} + +func (server *Server) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) { + body := server.readAll(request) + var suiteSummary *types.SuiteSummary + json.Unmarshal(body, &suiteSummary) + + for _, reporter := range server.reporters { + reporter.SpecSuiteDidEnd(suiteSummary) + } +} + +// +// Synchronization Endpoints +// + +func (server *Server) RegisterAlive(node int, alive func() bool) { + server.lock.Lock() + defer server.lock.Unlock() + server.alives[node-1] = alive +} + +func (server *Server) nodeIsAlive(node int) bool { + server.lock.Lock() + defer server.lock.Unlock() + alive := server.alives[node-1] + if alive == nil { + return true + } + return alive() +} + +func (server *Server) handleBeforeSuiteState(writer http.ResponseWriter, request *http.Request) { + if request.Method == "POST" { + dec := json.NewDecoder(request.Body) + dec.Decode(&(server.beforeSuiteData)) + } else { + beforeSuiteData := server.beforeSuiteData + if beforeSuiteData.State == types.RemoteBeforeSuiteStatePending && !server.nodeIsAlive(1) { + beforeSuiteData.State = types.RemoteBeforeSuiteStateDisappeared + } + enc := json.NewEncoder(writer) + enc.Encode(beforeSuiteData) + } +} + +func (server *Server) handleRemoteAfterSuiteData(writer http.ResponseWriter, request *http.Request) { + afterSuiteData := types.RemoteAfterSuiteData{ + CanRun: true, + } + for i := 2; i <= server.parallelTotal; i++ { + afterSuiteData.CanRun = afterSuiteData.CanRun && !server.nodeIsAlive(i) + } + + enc := json.NewEncoder(writer) + enc.Encode(afterSuiteData) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server_test.go new file mode 100644 index 00000000000..eb2eefebe02 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/remote/server_test.go @@ -0,0 +1,269 @@ +package remote_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/remote" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + + "bytes" + "encoding/json" + "net/http" +) + +var _ = Describe("Server", func() { + var ( + server *Server + ) + + BeforeEach(func() { + var err error + server, err = NewServer(3) + Ω(err).ShouldNot(HaveOccurred()) + + server.Start() + }) + + AfterEach(func() { + server.Close() + }) + + Describe("Streaming endpoints", func() { + var ( + reporterA, reporterB *reporters.FakeReporter + forwardingReporter *ForwardingReporter + + suiteSummary *types.SuiteSummary + setupSummary *types.SetupSummary + specSummary *types.SpecSummary + ) + + BeforeEach(func() { + reporterA = reporters.NewFakeReporter() + reporterB = reporters.NewFakeReporter() + + server.RegisterReporters(reporterA, reporterB) + + forwardingReporter = NewForwardingReporter(server.Address(), &http.Client{}, &fakeOutputInterceptor{}) + + suiteSummary = &types.SuiteSummary{ + SuiteDescription: "My Test Suite", + } + + setupSummary = &types.SetupSummary{ + State: types.SpecStatePassed, + } + + specSummary = &types.SpecSummary{ + ComponentTexts: []string{"My", "Spec"}, + State: types.SpecStatePassed, + } + }) + + It("should make its address available", func() { + Ω(server.Address()).Should(MatchRegexp(`http://127.0.0.1:\d{2,}`)) + }) + + Describe("/SpecSuiteWillBegin", func() { + It("should decode and forward the Ginkgo config and suite summary", func(done Done) { + forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) + Ω(reporterA.Config).Should(Equal(config.GinkgoConfig)) + Ω(reporterB.Config).Should(Equal(config.GinkgoConfig)) + Ω(reporterA.BeginSummary).Should(Equal(suiteSummary)) + Ω(reporterB.BeginSummary).Should(Equal(suiteSummary)) + close(done) + }) + }) + + Describe("/BeforeSuiteDidRun", func() { + It("should decode and forward the setup summary", func() { + forwardingReporter.BeforeSuiteDidRun(setupSummary) + Ω(reporterA.BeforeSuiteSummary).Should(Equal(setupSummary)) + Ω(reporterB.BeforeSuiteSummary).Should(Equal(setupSummary)) + }) + }) + + Describe("/AfterSuiteDidRun", func() { + It("should decode and forward the setup summary", func() { + forwardingReporter.AfterSuiteDidRun(setupSummary) + Ω(reporterA.AfterSuiteSummary).Should(Equal(setupSummary)) + Ω(reporterB.AfterSuiteSummary).Should(Equal(setupSummary)) + }) + }) + + Describe("/SpecWillRun", func() { + It("should decode and forward the spec summary", func(done Done) { + forwardingReporter.SpecWillRun(specSummary) + Ω(reporterA.SpecWillRunSummaries[0]).Should(Equal(specSummary)) + Ω(reporterB.SpecWillRunSummaries[0]).Should(Equal(specSummary)) + close(done) + }) + }) + + Describe("/SpecDidComplete", func() { + It("should decode and forward the spec summary", func(done Done) { + forwardingReporter.SpecDidComplete(specSummary) + Ω(reporterA.SpecSummaries[0]).Should(Equal(specSummary)) + Ω(reporterB.SpecSummaries[0]).Should(Equal(specSummary)) + close(done) + }) + }) + + Describe("/SpecSuiteDidEnd", func() { + It("should decode and forward the suite summary", func(done Done) { + forwardingReporter.SpecSuiteDidEnd(suiteSummary) + Ω(reporterA.EndSummary).Should(Equal(suiteSummary)) + Ω(reporterB.EndSummary).Should(Equal(suiteSummary)) + close(done) + }) + }) + }) + + Describe("Synchronization endpoints", func() { + Describe("GETting and POSTing BeforeSuiteState", func() { + getBeforeSuite := func() types.RemoteBeforeSuiteData { + resp, err := http.Get(server.Address() + "/BeforeSuiteState") + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusOK)) + + r := types.RemoteBeforeSuiteData{} + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&r) + Ω(err).ShouldNot(HaveOccurred()) + + return r + } + + postBeforeSuite := func(r types.RemoteBeforeSuiteData) { + resp, err := http.Post(server.Address()+"/BeforeSuiteState", "application/json", bytes.NewReader(r.ToJSON())) + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusOK)) + } + + Context("when the first node's Alive has not been registered yet", func() { + It("should return pending", func() { + state := getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending})) + + state = getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending})) + }) + }) + + Context("when the first node is Alive but has not responded yet", func() { + BeforeEach(func() { + server.RegisterAlive(1, func() bool { + return true + }) + }) + + It("should return pending", func() { + state := getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending})) + + state = getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStatePending})) + }) + }) + + Context("when the first node has responded", func() { + var state types.RemoteBeforeSuiteData + BeforeEach(func() { + server.RegisterAlive(1, func() bool { + return false + }) + + state = types.RemoteBeforeSuiteData{ + Data: []byte("my data"), + State: types.RemoteBeforeSuiteStatePassed, + } + postBeforeSuite(state) + }) + + It("should return the passed in state", func() { + returnedState := getBeforeSuite() + Ω(returnedState).Should(Equal(state)) + }) + }) + + Context("when the first node is no longer Alive and has not responded yet", func() { + BeforeEach(func() { + server.RegisterAlive(1, func() bool { + return false + }) + }) + + It("should return disappeared", func() { + state := getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStateDisappeared})) + + state = getBeforeSuite() + Ω(state).Should(Equal(types.RemoteBeforeSuiteData{nil, types.RemoteBeforeSuiteStateDisappeared})) + }) + }) + }) + + Describe("GETting RemoteAfterSuiteData", func() { + getRemoteAfterSuiteData := func() bool { + resp, err := http.Get(server.Address() + "/RemoteAfterSuiteData") + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusOK)) + + a := types.RemoteAfterSuiteData{} + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&a) + Ω(err).ShouldNot(HaveOccurred()) + + return a.CanRun + } + + Context("when there are unregistered nodes", func() { + BeforeEach(func() { + server.RegisterAlive(2, func() bool { + return false + }) + }) + + It("should return false", func() { + Ω(getRemoteAfterSuiteData()).Should(BeFalse()) + }) + }) + + Context("when all none-node-1 nodes are still running", func() { + BeforeEach(func() { + server.RegisterAlive(2, func() bool { + return true + }) + + server.RegisterAlive(3, func() bool { + return false + }) + }) + + It("should return false", func() { + Ω(getRemoteAfterSuiteData()).Should(BeFalse()) + }) + }) + + Context("when all none-1 nodes are done", func() { + BeforeEach(func() { + server.RegisterAlive(2, func() bool { + return false + }) + + server.RegisterAlive(3, func() bool { + return false + }) + }) + + It("should return true", func() { + Ω(getRemoteAfterSuiteData()).Should(BeTrue()) + }) + + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer.go new file mode 100644 index 00000000000..5a67fc7b740 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer.go @@ -0,0 +1,55 @@ +package spec + +func ParallelizedIndexRange(length int, parallelTotal int, parallelNode int) (startIndex int, count int) { + if length == 0 { + return 0, 0 + } + + // We have more nodes than tests. Trivial case. + if parallelTotal >= length { + if parallelNode > length { + return 0, 0 + } else { + return parallelNode - 1, 1 + } + } + + // This is the minimum amount of tests that a node will be required to run + minTestsPerNode := length / parallelTotal + + // This is the maximum amount of tests that a node will be required to run + // The algorithm guarantees that this would be equal to at least the minimum amount + // and at most one more + maxTestsPerNode := minTestsPerNode + if length%parallelTotal != 0 { + maxTestsPerNode++ + } + + // Number of nodes that will have to run the maximum amount of tests per node + numMaxLoadNodes := length % parallelTotal + + // Number of nodes that precede the current node and will have to run the maximum amount of tests per node + var numPrecedingMaxLoadNodes int + if parallelNode > numMaxLoadNodes { + numPrecedingMaxLoadNodes = numMaxLoadNodes + } else { + numPrecedingMaxLoadNodes = parallelNode - 1 + } + + // Number of nodes that precede the current node and will have to run the minimum amount of tests per node + var numPrecedingMinLoadNodes int + if parallelNode <= numMaxLoadNodes { + numPrecedingMinLoadNodes = 0 + } else { + numPrecedingMinLoadNodes = parallelNode - numMaxLoadNodes - 1 + } + + // Evaluate the test start index and number of tests to run + startIndex = numPrecedingMaxLoadNodes*maxTestsPerNode + numPrecedingMinLoadNodes*minTestsPerNode + if parallelNode > numMaxLoadNodes { + count = minTestsPerNode + } else { + count = maxTestsPerNode + } + return +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer_test.go new file mode 100644 index 00000000000..0396d7bde05 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/index_computer_test.go @@ -0,0 +1,149 @@ +package spec_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/spec" + . "github.com/onsi/gomega" +) + +var _ = Describe("ParallelizedIndexRange", func() { + var startIndex, count int + + It("should return the correct index range for 4 tests on 2 nodes", func() { + startIndex, count = ParallelizedIndexRange(4, 2, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(4, 2, 2) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(2)) + }) + + It("should return the correct index range for 5 tests on 2 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 2, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(3)) + + startIndex, count = ParallelizedIndexRange(5, 2, 2) + Ω(startIndex).Should(Equal(3)) + Ω(count).Should(Equal(2)) + }) + + It("should return the correct index range for 5 tests on 3 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 3, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(5, 3, 2) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(5, 3, 3) + Ω(startIndex).Should(Equal(4)) + Ω(count).Should(Equal(1)) + }) + + It("should return the correct index range for 5 tests on 4 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 4, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(5, 4, 2) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 4, 3) + Ω(startIndex).Should(Equal(3)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 4, 4) + Ω(startIndex).Should(Equal(4)) + Ω(count).Should(Equal(1)) + }) + + It("should return the correct index range for 5 tests on 5 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 5, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 5, 2) + Ω(startIndex).Should(Equal(1)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 5, 3) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 5, 4) + Ω(startIndex).Should(Equal(3)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 5, 5) + Ω(startIndex).Should(Equal(4)) + Ω(count).Should(Equal(1)) + }) + + It("should return the correct index range for 5 tests on 6 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 6, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 6, 2) + Ω(startIndex).Should(Equal(1)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 6, 3) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 6, 4) + Ω(startIndex).Should(Equal(3)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 6, 5) + Ω(startIndex).Should(Equal(4)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(5, 6, 6) + Ω(count).Should(Equal(0)) + }) + + It("should return the correct index range for 5 tests on 7 nodes", func() { + startIndex, count = ParallelizedIndexRange(5, 7, 6) + Ω(count).Should(Equal(0)) + + startIndex, count = ParallelizedIndexRange(5, 7, 7) + Ω(count).Should(Equal(0)) + }) + + It("should return the correct index range for 11 tests on 7 nodes", func() { + startIndex, count = ParallelizedIndexRange(11, 7, 1) + Ω(startIndex).Should(Equal(0)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(11, 7, 2) + Ω(startIndex).Should(Equal(2)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(11, 7, 3) + Ω(startIndex).Should(Equal(4)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(11, 7, 4) + Ω(startIndex).Should(Equal(6)) + Ω(count).Should(Equal(2)) + + startIndex, count = ParallelizedIndexRange(11, 7, 5) + Ω(startIndex).Should(Equal(8)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(11, 7, 6) + Ω(startIndex).Should(Equal(9)) + Ω(count).Should(Equal(1)) + + startIndex, count = ParallelizedIndexRange(11, 7, 7) + Ω(startIndex).Should(Equal(10)) + Ω(count).Should(Equal(1)) + }) + +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec.go new file mode 100644 index 00000000000..076fff13698 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec.go @@ -0,0 +1,199 @@ +package spec + +import ( + "fmt" + "io" + "time" + + "github.com/onsi/ginkgo/internal/containernode" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/types" +) + +type Spec struct { + subject leafnodes.SubjectNode + focused bool + announceProgress bool + + containers []*containernode.ContainerNode + + state types.SpecState + runTime time.Duration + failure types.SpecFailure +} + +func New(subject leafnodes.SubjectNode, containers []*containernode.ContainerNode, announceProgress bool) *Spec { + spec := &Spec{ + subject: subject, + containers: containers, + focused: subject.Flag() == types.FlagTypeFocused, + announceProgress: announceProgress, + } + + spec.processFlag(subject.Flag()) + for i := len(containers) - 1; i >= 0; i-- { + spec.processFlag(containers[i].Flag()) + } + + return spec +} + +func (spec *Spec) processFlag(flag types.FlagType) { + if flag == types.FlagTypeFocused { + spec.focused = true + } else if flag == types.FlagTypePending { + spec.state = types.SpecStatePending + } +} + +func (spec *Spec) Skip() { + spec.state = types.SpecStateSkipped +} + +func (spec *Spec) Failed() bool { + return spec.state == types.SpecStateFailed || spec.state == types.SpecStatePanicked || spec.state == types.SpecStateTimedOut +} + +func (spec *Spec) Passed() bool { + return spec.state == types.SpecStatePassed +} + +func (spec *Spec) Pending() bool { + return spec.state == types.SpecStatePending +} + +func (spec *Spec) Skipped() bool { + return spec.state == types.SpecStateSkipped +} + +func (spec *Spec) Focused() bool { + return spec.focused +} + +func (spec *Spec) IsMeasurement() bool { + return spec.subject.Type() == types.SpecComponentTypeMeasure +} + +func (spec *Spec) Summary(suiteID string) *types.SpecSummary { + componentTexts := make([]string, len(spec.containers)+1) + componentCodeLocations := make([]types.CodeLocation, len(spec.containers)+1) + + for i, container := range spec.containers { + componentTexts[i] = container.Text() + componentCodeLocations[i] = container.CodeLocation() + } + + componentTexts[len(spec.containers)] = spec.subject.Text() + componentCodeLocations[len(spec.containers)] = spec.subject.CodeLocation() + + return &types.SpecSummary{ + IsMeasurement: spec.IsMeasurement(), + NumberOfSamples: spec.subject.Samples(), + ComponentTexts: componentTexts, + ComponentCodeLocations: componentCodeLocations, + State: spec.state, + RunTime: spec.runTime, + Failure: spec.failure, + Measurements: spec.measurementsReport(), + SuiteID: suiteID, + } +} + +func (spec *Spec) ConcatenatedString() string { + s := "" + for _, container := range spec.containers { + s += container.Text() + " " + } + + return s + spec.subject.Text() +} + +func (spec *Spec) Run(writer io.Writer) { + startTime := time.Now() + defer func() { + spec.runTime = time.Since(startTime) + }() + + for sample := 0; sample < spec.subject.Samples(); sample++ { + spec.state, spec.failure = spec.runSample(sample, writer) + + if spec.state != types.SpecStatePassed { + return + } + } +} + +func (spec *Spec) runSample(sample int, writer io.Writer) (specState types.SpecState, specFailure types.SpecFailure) { + specState = types.SpecStatePassed + specFailure = types.SpecFailure{} + innerMostContainerIndexToUnwind := -1 + + defer func() { + for i := innerMostContainerIndexToUnwind; i >= 0; i-- { + container := spec.containers[i] + for _, afterEach := range container.SetupNodesOfType(types.SpecComponentTypeAfterEach) { + spec.announceSetupNode(writer, "AfterEach", container, afterEach) + afterEachState, afterEachFailure := afterEach.Run() + if afterEachState != types.SpecStatePassed && specState == types.SpecStatePassed { + specState = afterEachState + specFailure = afterEachFailure + } + } + } + }() + + for i, container := range spec.containers { + innerMostContainerIndexToUnwind = i + for _, beforeEach := range container.SetupNodesOfType(types.SpecComponentTypeBeforeEach) { + spec.announceSetupNode(writer, "BeforeEach", container, beforeEach) + specState, specFailure = beforeEach.Run() + if specState != types.SpecStatePassed { + return + } + } + } + + for _, container := range spec.containers { + for _, justBeforeEach := range container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach) { + spec.announceSetupNode(writer, "JustBeforeEach", container, justBeforeEach) + specState, specFailure = justBeforeEach.Run() + if specState != types.SpecStatePassed { + return + } + } + } + + spec.announceSubject(writer, spec.subject) + specState, specFailure = spec.subject.Run() + + return +} + +func (spec *Spec) announceSetupNode(writer io.Writer, nodeType string, container *containernode.ContainerNode, setupNode leafnodes.BasicNode) { + if spec.announceProgress { + s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, container.Text(), setupNode.CodeLocation().String()) + writer.Write([]byte(s)) + } +} + +func (spec *Spec) announceSubject(writer io.Writer, subject leafnodes.SubjectNode) { + if spec.announceProgress { + nodeType := "" + switch subject.Type() { + case types.SpecComponentTypeIt: + nodeType = "It" + case types.SpecComponentTypeMeasure: + nodeType = "Measure" + } + s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, subject.Text(), subject.CodeLocation().String()) + writer.Write([]byte(s)) + } +} + +func (spec *Spec) measurementsReport() map[string]*types.SpecMeasurement { + if !spec.IsMeasurement() || spec.Failed() { + return map[string]*types.SpecMeasurement{} + } + + return spec.subject.(*leafnodes.MeasureNode).MeasurementsReport() +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_suite_test.go new file mode 100644 index 00000000000..8681a720684 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_suite_test.go @@ -0,0 +1,13 @@ +package spec_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpec(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Spec Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_test.go new file mode 100644 index 00000000000..6d0f58cb044 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/spec_test.go @@ -0,0 +1,626 @@ +package spec_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + + . "github.com/onsi/ginkgo/internal/spec" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/containernode" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/types" +) + +var noneFlag = types.FlagTypeNone +var focusedFlag = types.FlagTypeFocused +var pendingFlag = types.FlagTypePending + +var _ = Describe("Spec", func() { + var ( + failer *Failer.Failer + codeLocation types.CodeLocation + nodesThatRan []string + spec *Spec + buffer *gbytes.Buffer + ) + + newBody := func(text string, fail bool) func() { + return func() { + nodesThatRan = append(nodesThatRan, text) + if fail { + failer.Fail(text, codeLocation) + } + } + } + + newIt := func(text string, flag types.FlagType, fail bool) *leafnodes.ItNode { + return leafnodes.NewItNode(text, newBody(text, fail), flag, codeLocation, 0, failer, 0) + } + + newItWithBody := func(text string, body interface{}) *leafnodes.ItNode { + return leafnodes.NewItNode(text, body, noneFlag, codeLocation, 0, failer, 0) + } + + newMeasure := func(text string, flag types.FlagType, fail bool, samples int) *leafnodes.MeasureNode { + return leafnodes.NewMeasureNode(text, func(Benchmarker) { + nodesThatRan = append(nodesThatRan, text) + if fail { + failer.Fail(text, codeLocation) + } + }, flag, codeLocation, samples, failer, 0) + } + + newBef := func(text string, fail bool) leafnodes.BasicNode { + return leafnodes.NewBeforeEachNode(newBody(text, fail), codeLocation, 0, failer, 0) + } + + newAft := func(text string, fail bool) leafnodes.BasicNode { + return leafnodes.NewAfterEachNode(newBody(text, fail), codeLocation, 0, failer, 0) + } + + newJusBef := func(text string, fail bool) leafnodes.BasicNode { + return leafnodes.NewJustBeforeEachNode(newBody(text, fail), codeLocation, 0, failer, 0) + } + + newContainer := func(text string, flag types.FlagType, setupNodes ...leafnodes.BasicNode) *containernode.ContainerNode { + c := containernode.New(text, flag, codeLocation) + for _, node := range setupNodes { + c.PushSetupNode(node) + } + return c + } + + containers := func(containers ...*containernode.ContainerNode) []*containernode.ContainerNode { + return containers + } + + BeforeEach(func() { + buffer = gbytes.NewBuffer() + failer = Failer.New() + codeLocation = codelocation.New(0) + nodesThatRan = []string{} + }) + + Describe("marking specs focused and pending", func() { + It("should satisfy various caes", func() { + cases := []struct { + ContainerFlags []types.FlagType + SubjectFlag types.FlagType + Pending bool + Focused bool + }{ + {[]types.FlagType{}, noneFlag, false, false}, + {[]types.FlagType{}, focusedFlag, false, true}, + {[]types.FlagType{}, pendingFlag, true, false}, + {[]types.FlagType{noneFlag}, noneFlag, false, false}, + {[]types.FlagType{focusedFlag}, noneFlag, false, true}, + {[]types.FlagType{pendingFlag}, noneFlag, true, false}, + {[]types.FlagType{noneFlag}, focusedFlag, false, true}, + {[]types.FlagType{focusedFlag}, focusedFlag, false, true}, + {[]types.FlagType{pendingFlag}, focusedFlag, true, true}, + {[]types.FlagType{noneFlag}, pendingFlag, true, false}, + {[]types.FlagType{focusedFlag}, pendingFlag, true, true}, + {[]types.FlagType{pendingFlag}, pendingFlag, true, false}, + {[]types.FlagType{focusedFlag, noneFlag}, noneFlag, false, true}, + {[]types.FlagType{noneFlag, focusedFlag}, noneFlag, false, true}, + {[]types.FlagType{pendingFlag, noneFlag}, noneFlag, true, false}, + {[]types.FlagType{noneFlag, pendingFlag}, noneFlag, true, false}, + {[]types.FlagType{focusedFlag, pendingFlag}, noneFlag, true, true}, + } + + for i, c := range cases { + subject := newIt("it node", c.SubjectFlag, false) + containers := []*containernode.ContainerNode{} + for _, flag := range c.ContainerFlags { + containers = append(containers, newContainer("container", flag)) + } + + spec := New(subject, containers, false) + Ω(spec.Pending()).Should(Equal(c.Pending), "Case %d: %#v", i, c) + Ω(spec.Focused()).Should(Equal(c.Focused), "Case %d: %#v", i, c) + + if c.Pending { + Ω(spec.Summary("").State).Should(Equal(types.SpecStatePending)) + } + } + }) + }) + + Describe("Skip", func() { + It("should be skipped", func() { + spec := New(newIt("it node", noneFlag, false), containers(newContainer("container", noneFlag)), false) + Ω(spec.Skipped()).Should(BeFalse()) + spec.Skip() + Ω(spec.Skipped()).Should(BeTrue()) + Ω(spec.Summary("").State).Should(Equal(types.SpecStateSkipped)) + }) + }) + + Describe("IsMeasurement", func() { + It("should be true if the subject is a measurement node", func() { + spec := New(newIt("it node", noneFlag, false), containers(newContainer("container", noneFlag)), false) + Ω(spec.IsMeasurement()).Should(BeFalse()) + Ω(spec.Summary("").IsMeasurement).Should(BeFalse()) + Ω(spec.Summary("").NumberOfSamples).Should(Equal(1)) + + spec = New(newMeasure("measure node", noneFlag, false, 10), containers(newContainer("container", noneFlag)), false) + Ω(spec.IsMeasurement()).Should(BeTrue()) + Ω(spec.Summary("").IsMeasurement).Should(BeTrue()) + Ω(spec.Summary("").NumberOfSamples).Should(Equal(10)) + }) + }) + + Describe("Passed", func() { + It("should pass when the subject passed", func() { + spec := New(newIt("it node", noneFlag, false), containers(), false) + spec.Run(buffer) + + Ω(spec.Passed()).Should(BeTrue()) + Ω(spec.Failed()).Should(BeFalse()) + Ω(spec.Summary("").State).Should(Equal(types.SpecStatePassed)) + Ω(spec.Summary("").Failure).Should(BeZero()) + }) + }) + + Describe("Failed", func() { + It("should be failed if the failure was panic", func() { + spec := New(newItWithBody("panicky it", func() { + panic("bam") + }), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(spec.Summary("").State).Should(Equal(types.SpecStatePanicked)) + Ω(spec.Summary("").Failure.Message).Should(Equal("Test Panicked")) + Ω(spec.Summary("").Failure.ForwardedPanic).Should(Equal("bam")) + }) + + It("should be failed if the failure was a timeout", func() { + spec := New(newItWithBody("sleepy it", func(done Done) {}), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(spec.Summary("").State).Should(Equal(types.SpecStateTimedOut)) + Ω(spec.Summary("").Failure.Message).Should(Equal("Timed out")) + }) + + It("should be failed if the failure was... a failure", func() { + spec := New(newItWithBody("failing it", func() { + failer.Fail("bam", codeLocation) + }), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(spec.Summary("").State).Should(Equal(types.SpecStateFailed)) + Ω(spec.Summary("").Failure.Message).Should(Equal("bam")) + }) + }) + + Describe("Concatenated string", func() { + It("should concatenate the texts of the containers and the subject", func() { + spec := New( + newIt("it node", noneFlag, false), + containers( + newContainer("outer container", noneFlag), + newContainer("inner container", noneFlag), + ), + false, + ) + + Ω(spec.ConcatenatedString()).Should(Equal("outer container inner container it node")) + }) + }) + + Describe("running it specs", func() { + Context("with just an it", func() { + Context("that succeeds", func() { + It("should run the it and report on its success", func() { + spec := New(newIt("it node", noneFlag, false), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeTrue()) + Ω(spec.Failed()).Should(BeFalse()) + Ω(nodesThatRan).Should(Equal([]string{"it node"})) + }) + }) + + Context("that fails", func() { + It("should run the it and report on its success", func() { + spec := New(newIt("it node", noneFlag, true), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) + Ω(nodesThatRan).Should(Equal([]string{"it node"})) + }) + }) + }) + + Context("with a full set of setup nodes", func() { + var failingNodes map[string]bool + + BeforeEach(func() { + failingNodes = map[string]bool{} + }) + + JustBeforeEach(func() { + spec = New( + newIt("it node", noneFlag, failingNodes["it node"]), + containers( + newContainer("outer container", noneFlag, + newBef("outer bef A", failingNodes["outer bef A"]), + newBef("outer bef B", failingNodes["outer bef B"]), + newJusBef("outer jusbef A", failingNodes["outer jusbef A"]), + newJusBef("outer jusbef B", failingNodes["outer jusbef B"]), + newAft("outer aft A", failingNodes["outer aft A"]), + newAft("outer aft B", failingNodes["outer aft B"]), + ), + newContainer("inner container", noneFlag, + newBef("inner bef A", failingNodes["inner bef A"]), + newBef("inner bef B", failingNodes["inner bef B"]), + newJusBef("inner jusbef A", failingNodes["inner jusbef A"]), + newJusBef("inner jusbef B", failingNodes["inner jusbef B"]), + newAft("inner aft A", failingNodes["inner aft A"]), + newAft("inner aft B", failingNodes["inner aft B"]), + ), + ), + false, + ) + spec.Run(buffer) + }) + + Context("that all pass", func() { + It("should walk through the nodes in the correct order", func() { + Ω(spec.Passed()).Should(BeTrue()) + Ω(spec.Failed()).Should(BeFalse()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner bef B", + "outer jusbef A", + "outer jusbef B", + "inner jusbef A", + "inner jusbef B", + "it node", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + }) + }) + + Context("when the subject fails", func() { + BeforeEach(func() { + failingNodes["it node"] = true + }) + + It("should run the afters", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner bef B", + "outer jusbef A", + "outer jusbef B", + "inner jusbef A", + "inner jusbef B", + "it node", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) + }) + }) + + Context("when an inner before fails", func() { + BeforeEach(func() { + failingNodes["inner bef A"] = true + }) + + It("should not run any other befores, but it should run the subsequent afters", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("inner bef A")) + }) + }) + + Context("when an outer before fails", func() { + BeforeEach(func() { + failingNodes["outer bef B"] = true + }) + + It("should not run any other befores, but it should run the subsequent afters", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("outer bef B")) + }) + }) + + Context("when an after fails", func() { + BeforeEach(func() { + failingNodes["inner aft B"] = true + }) + + It("should run all other afters, but mark the test as failed", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner bef B", + "outer jusbef A", + "outer jusbef B", + "inner jusbef A", + "inner jusbef B", + "it node", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("inner aft B")) + }) + }) + + Context("when a just before each fails", func() { + BeforeEach(func() { + failingNodes["outer jusbef B"] = true + }) + + It("should run the afters, but not the subject", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner bef B", + "outer jusbef A", + "outer jusbef B", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("outer jusbef B")) + }) + }) + + Context("when an after fails after an earlier node has failed", func() { + BeforeEach(func() { + failingNodes["it node"] = true + failingNodes["inner aft B"] = true + }) + + It("should record the earlier failure", func() { + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "outer bef A", + "outer bef B", + "inner bef A", + "inner bef B", + "outer jusbef A", + "outer jusbef B", + "inner jusbef A", + "inner jusbef B", + "it node", + "inner aft A", + "inner aft B", + "outer aft A", + "outer aft B", + })) + Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) + }) + }) + }) + }) + + Describe("running measurement specs", func() { + Context("when the measurement succeeds", func() { + It("should run N samples", func() { + spec = New( + newMeasure("measure node", noneFlag, false, 3), + containers( + newContainer("container", noneFlag, + newBef("bef A", false), + newJusBef("jusbef A", false), + newAft("aft A", false), + ), + ), + false, + ) + spec.Run(buffer) + + Ω(spec.Passed()).Should(BeTrue()) + Ω(spec.Failed()).Should(BeFalse()) + Ω(nodesThatRan).Should(Equal([]string{ + "bef A", + "jusbef A", + "measure node", + "aft A", + "bef A", + "jusbef A", + "measure node", + "aft A", + "bef A", + "jusbef A", + "measure node", + "aft A", + })) + }) + }) + + Context("when the measurement fails", func() { + It("should bail after the failure occurs", func() { + spec = New( + newMeasure("measure node", noneFlag, true, 3), + containers( + newContainer("container", noneFlag, + newBef("bef A", false), + newJusBef("jusbef A", false), + newAft("aft A", false), + ), + ), + false, + ) + spec.Run(buffer) + + Ω(spec.Passed()).Should(BeFalse()) + Ω(spec.Failed()).Should(BeTrue()) + Ω(nodesThatRan).Should(Equal([]string{ + "bef A", + "jusbef A", + "measure node", + "aft A", + })) + }) + }) + }) + + Describe("Summary", func() { + var ( + subjectCodeLocation types.CodeLocation + outerContainerCodeLocation types.CodeLocation + innerContainerCodeLocation types.CodeLocation + summary *types.SpecSummary + ) + + BeforeEach(func() { + subjectCodeLocation = codelocation.New(0) + outerContainerCodeLocation = codelocation.New(0) + innerContainerCodeLocation = codelocation.New(0) + + spec = New( + leafnodes.NewItNode("it node", func() { + time.Sleep(10 * time.Millisecond) + }, noneFlag, subjectCodeLocation, 0, failer, 0), + containers( + containernode.New("outer container", noneFlag, outerContainerCodeLocation), + containernode.New("inner container", noneFlag, innerContainerCodeLocation), + ), + false, + ) + + spec.Run(buffer) + Ω(spec.Passed()).Should(BeTrue()) + summary = spec.Summary("suite id") + }) + + It("should have the suite id", func() { + Ω(summary.SuiteID).Should(Equal("suite id")) + }) + + It("should have the component texts and code locations", func() { + Ω(summary.ComponentTexts).Should(Equal([]string{"outer container", "inner container", "it node"})) + Ω(summary.ComponentCodeLocations).Should(Equal([]types.CodeLocation{outerContainerCodeLocation, innerContainerCodeLocation, subjectCodeLocation})) + }) + + It("should have a runtime", func() { + Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) + }) + + It("should not be a measurement, or have a measurement summary", func() { + Ω(summary.IsMeasurement).Should(BeFalse()) + Ω(summary.Measurements).Should(BeEmpty()) + }) + }) + + Describe("Summaries for measurements", func() { + var summary *types.SpecSummary + + BeforeEach(func() { + spec = New(leafnodes.NewMeasureNode("measure node", func(b Benchmarker) { + b.RecordValue("a value", 7, "some info") + }, noneFlag, codeLocation, 4, failer, 0), containers(), false) + spec.Run(buffer) + Ω(spec.Passed()).Should(BeTrue()) + summary = spec.Summary("suite id") + }) + + It("should include the number of samples", func() { + Ω(summary.NumberOfSamples).Should(Equal(4)) + }) + + It("should be a measurement", func() { + Ω(summary.IsMeasurement).Should(BeTrue()) + }) + + It("should have the measurements report", func() { + Ω(summary.Measurements).Should(HaveKey("a value")) + + report := summary.Measurements["a value"] + Ω(report.Name).Should(Equal("a value")) + Ω(report.Info).Should(Equal("some info")) + Ω(report.Results).Should(Equal([]float64{7, 7, 7, 7})) + }) + }) + + Describe("When told to emit progress", func() { + It("should emit progress to the writer as it runs Befores, JustBefores, Afters, and Its", func() { + spec = New( + newIt("it node", noneFlag, false), + containers( + newContainer("outer container", noneFlag, + newBef("outer bef A", false), + newJusBef("outer jusbef A", false), + newAft("outer aft A", false), + ), + newContainer("inner container", noneFlag, + newBef("inner bef A", false), + newJusBef("inner jusbef A", false), + newAft("inner aft A", false), + ), + ), + true, + ) + spec.Run(buffer) + + Ω(buffer).Should(gbytes.Say(`\[BeforeEach\] outer container`)) + Ω(buffer).Should(gbytes.Say(`\[BeforeEach\] inner container`)) + Ω(buffer).Should(gbytes.Say(`\[JustBeforeEach\] outer container`)) + Ω(buffer).Should(gbytes.Say(`\[JustBeforeEach\] inner container`)) + Ω(buffer).Should(gbytes.Say(`\[It\] it node`)) + Ω(buffer).Should(gbytes.Say(`\[AfterEach\] inner container`)) + Ω(buffer).Should(gbytes.Say(`\[AfterEach\] outer container`)) + }) + + It("should emit progress to the writer as it runs Befores, JustBefores, Afters, and Measures", func() { + spec = New( + newMeasure("measure node", noneFlag, false, 2), + containers(), + true, + ) + spec.Run(buffer) + + Ω(buffer).Should(gbytes.Say(`\[Measure\] measure node`)) + Ω(buffer).Should(gbytes.Say(`\[Measure\] measure node`)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs.go new file mode 100644 index 00000000000..a3d545153e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs.go @@ -0,0 +1,122 @@ +package spec + +import ( + "math/rand" + "regexp" + "sort" +) + +type Specs struct { + specs []*Spec + numberOfOriginalSpecs int + hasProgrammaticFocus bool +} + +func NewSpecs(specs []*Spec) *Specs { + return &Specs{ + specs: specs, + numberOfOriginalSpecs: len(specs), + } +} + +func (e *Specs) Specs() []*Spec { + return e.specs +} + +func (e *Specs) NumberOfOriginalSpecs() int { + return e.numberOfOriginalSpecs +} + +func (e *Specs) HasProgrammaticFocus() bool { + return e.hasProgrammaticFocus +} + +func (e *Specs) Shuffle(r *rand.Rand) { + sort.Sort(e) + permutation := r.Perm(len(e.specs)) + shuffledSpecs := make([]*Spec, len(e.specs)) + for i, j := range permutation { + shuffledSpecs[i] = e.specs[j] + } + e.specs = shuffledSpecs +} + +func (e *Specs) ApplyFocus(description string, focusString string, skipString string) { + if focusString == "" && skipString == "" { + e.applyProgrammaticFocus() + } else { + e.applyRegExpFocus(description, focusString, skipString) + } +} + +func (e *Specs) applyProgrammaticFocus() { + e.hasProgrammaticFocus = false + for _, spec := range e.specs { + if spec.Focused() { + e.hasProgrammaticFocus = true + break + } + } + + if e.hasProgrammaticFocus { + for _, spec := range e.specs { + if !spec.Focused() { + spec.Skip() + } + } + } +} + +func (e *Specs) applyRegExpFocus(description string, focusString string, skipString string) { + for _, spec := range e.specs { + matchesFocus := true + matchesSkip := false + + toMatch := []byte(description + " " + spec.ConcatenatedString()) + + if focusString != "" { + focusFilter := regexp.MustCompile(focusString) + matchesFocus = focusFilter.Match([]byte(toMatch)) + } + + if skipString != "" { + skipFilter := regexp.MustCompile(skipString) + matchesSkip = skipFilter.Match([]byte(toMatch)) + } + + if !matchesFocus || matchesSkip { + spec.Skip() + } + } +} + +func (e *Specs) SkipMeasurements() { + for _, spec := range e.specs { + if spec.IsMeasurement() { + spec.Skip() + } + } +} + +func (e *Specs) TrimForParallelization(total int, node int) { + startIndex, count := ParallelizedIndexRange(len(e.specs), total, node) + if count == 0 { + e.specs = make([]*Spec, 0) + } else { + e.specs = e.specs[startIndex : startIndex+count] + } +} + +//sort.Interface + +func (e *Specs) Len() int { + return len(e.specs) +} + +func (e *Specs) Less(i, j int) bool { + return e.specs[i].ConcatenatedString() < e.specs[j].ConcatenatedString() +} + +func (e *Specs) Swap(i, j int) { + e.specs[i], e.specs[j] = e.specs[j], e.specs[i] +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs_test.go new file mode 100644 index 00000000000..b58a3074d58 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/spec/specs_test.go @@ -0,0 +1,305 @@ +package spec_test + +import ( + "math/rand" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/spec" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/containernode" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Specs", func() { + var specs *Specs + + newSpec := func(text string, flag types.FlagType) *Spec { + subject := leafnodes.NewItNode(text, func() {}, flag, codelocation.New(0), 0, nil, 0) + return New(subject, []*containernode.ContainerNode{}, false) + } + + newMeasureSpec := func(text string, flag types.FlagType) *Spec { + subject := leafnodes.NewMeasureNode(text, func(Benchmarker) {}, flag, codelocation.New(0), 0, nil, 0) + return New(subject, []*containernode.ContainerNode{}, false) + } + + newSpecs := func(args ...interface{}) *Specs { + specs := []*Spec{} + for index := 0; index < len(args)-1; index += 2 { + specs = append(specs, newSpec(args[index].(string), args[index+1].(types.FlagType))) + } + return NewSpecs(specs) + } + + specTexts := func(specs *Specs) []string { + texts := []string{} + for _, spec := range specs.Specs() { + texts = append(texts, spec.ConcatenatedString()) + } + return texts + } + + willRunTexts := func(specs *Specs) []string { + texts := []string{} + for _, spec := range specs.Specs() { + if !(spec.Skipped() || spec.Pending()) { + texts = append(texts, spec.ConcatenatedString()) + } + } + return texts + } + + skippedTexts := func(specs *Specs) []string { + texts := []string{} + for _, spec := range specs.Specs() { + if spec.Skipped() { + texts = append(texts, spec.ConcatenatedString()) + } + } + return texts + } + + pendingTexts := func(specs *Specs) []string { + texts := []string{} + for _, spec := range specs.Specs() { + if spec.Pending() { + texts = append(texts, spec.ConcatenatedString()) + } + } + return texts + } + + Describe("Shuffling specs", func() { + It("should shuffle the specs using the passed in randomizer", func() { + specs17 := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) + specs17.Shuffle(rand.New(rand.NewSource(17))) + texts17 := specTexts(specs17) + + specs17Again := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) + specs17Again.Shuffle(rand.New(rand.NewSource(17))) + texts17Again := specTexts(specs17Again) + + specs15 := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) + specs15.Shuffle(rand.New(rand.NewSource(15))) + texts15 := specTexts(specs15) + + specsUnshuffled := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) + textsUnshuffled := specTexts(specsUnshuffled) + + Ω(textsUnshuffled).Should(Equal([]string{"C", "A", "B"})) + + Ω(texts17).Should(Equal(texts17Again)) + Ω(texts17).ShouldNot(Equal(texts15)) + Ω(texts17).ShouldNot(Equal(textsUnshuffled)) + Ω(texts15).ShouldNot(Equal(textsUnshuffled)) + + Ω(texts17).Should(HaveLen(3)) + Ω(texts17).Should(ContainElement("A")) + Ω(texts17).Should(ContainElement("B")) + Ω(texts17).Should(ContainElement("C")) + + Ω(texts15).Should(HaveLen(3)) + Ω(texts15).Should(ContainElement("A")) + Ω(texts15).Should(ContainElement("B")) + Ω(texts15).Should(ContainElement("C")) + }) + }) + + Describe("with no programmatic focus", func() { + BeforeEach(func() { + specs = newSpecs("A1", noneFlag, "A2", noneFlag, "B1", noneFlag, "B2", pendingFlag) + specs.ApplyFocus("", "", "") + }) + + It("should not report as having programmatic specs", func() { + Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) + }) + }) + + Describe("Applying focus/skip", func() { + var description, focusString, skipString string + + BeforeEach(func() { + description, focusString, skipString = "", "", "" + }) + + JustBeforeEach(func() { + specs = newSpecs("A1", focusedFlag, "A2", noneFlag, "B1", focusedFlag, "B2", pendingFlag) + specs.ApplyFocus(description, focusString, skipString) + }) + + Context("with neither a focus string nor a skip string", func() { + It("should apply the programmatic focus", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "B1"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"A2", "B2"})) + Ω(pendingTexts(specs)).Should(BeEmpty()) + }) + + It("should report as having programmatic specs", func() { + Ω(specs.HasProgrammaticFocus()).Should(BeTrue()) + }) + }) + + Context("with a focus regexp", func() { + BeforeEach(func() { + focusString = "A" + }) + + It("should override the programmatic focus", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "A2"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"B1", "B2"})) + Ω(pendingTexts(specs)).Should(BeEmpty()) + }) + + It("should not report as having programmatic specs", func() { + Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) + }) + }) + + Context("with a focus regexp", func() { + BeforeEach(func() { + focusString = "B" + }) + + It("should not override any pendings", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"B1"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2"})) + Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) + }) + }) + + Context("with a description", func() { + BeforeEach(func() { + description = "C" + focusString = "C" + }) + + It("should include the description in the focus determination", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "A2", "B1"})) + Ω(skippedTexts(specs)).Should(BeEmpty()) + Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) + }) + }) + + Context("with a description", func() { + BeforeEach(func() { + description = "C" + skipString = "C" + }) + + It("should include the description in the focus determination", func() { + Ω(willRunTexts(specs)).Should(BeEmpty()) + Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2", "B1", "B2"})) + Ω(pendingTexts(specs)).Should(BeEmpty()) + }) + }) + + Context("with a skip regexp", func() { + BeforeEach(func() { + skipString = "A" + }) + + It("should override the programmatic focus", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"B1"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2"})) + Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) + }) + + It("should not report as having programmatic specs", func() { + Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) + }) + }) + + Context("with both a focus and a skip regexp", func() { + BeforeEach(func() { + focusString = "1" + skipString = "B" + }) + + It("should AND the two", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"A1"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"A2", "B1", "B2"})) + Ω(pendingTexts(specs)).Should(BeEmpty()) + }) + + It("should not report as having programmatic specs", func() { + Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) + }) + }) + }) + + Describe("skipping measurements", func() { + BeforeEach(func() { + specs = NewSpecs([]*Spec{ + newSpec("A", noneFlag), + newSpec("B", noneFlag), + newSpec("C", pendingFlag), + newMeasureSpec("measurementA", noneFlag), + newMeasureSpec("measurementB", pendingFlag), + }) + }) + + It("should skip measurements", func() { + Ω(willRunTexts(specs)).Should(Equal([]string{"A", "B", "measurementA"})) + Ω(skippedTexts(specs)).Should(BeEmpty()) + Ω(pendingTexts(specs)).Should(Equal([]string{"C", "measurementB"})) + + specs.SkipMeasurements() + + Ω(willRunTexts(specs)).Should(Equal([]string{"A", "B"})) + Ω(skippedTexts(specs)).Should(Equal([]string{"measurementA", "measurementB"})) + Ω(pendingTexts(specs)).Should(Equal([]string{"C"})) + }) + }) + + Describe("when running tests in parallel", func() { + It("should select out a subset of the tests", func() { + specsNode1 := newSpecs("A", noneFlag, "B", noneFlag, "C", noneFlag, "D", noneFlag, "E", noneFlag) + specsNode2 := newSpecs("A", noneFlag, "B", noneFlag, "C", noneFlag, "D", noneFlag, "E", noneFlag) + specsNode3 := newSpecs("A", noneFlag, "B", noneFlag, "C", noneFlag, "D", noneFlag, "E", noneFlag) + + specsNode1.TrimForParallelization(3, 1) + specsNode2.TrimForParallelization(3, 2) + specsNode3.TrimForParallelization(3, 3) + + Ω(willRunTexts(specsNode1)).Should(Equal([]string{"A", "B"})) + Ω(willRunTexts(specsNode2)).Should(Equal([]string{"C", "D"})) + Ω(willRunTexts(specsNode3)).Should(Equal([]string{"E"})) + + Ω(specsNode1.Specs()).Should(HaveLen(2)) + Ω(specsNode2.Specs()).Should(HaveLen(2)) + Ω(specsNode3.Specs()).Should(HaveLen(1)) + + Ω(specsNode1.NumberOfOriginalSpecs()).Should(Equal(5)) + Ω(specsNode2.NumberOfOriginalSpecs()).Should(Equal(5)) + Ω(specsNode3.NumberOfOriginalSpecs()).Should(Equal(5)) + }) + + Context("when way too many nodes are used", func() { + It("should return 0 specs", func() { + specsNode1 := newSpecs("A", noneFlag, "B", noneFlag) + specsNode2 := newSpecs("A", noneFlag, "B", noneFlag) + specsNode3 := newSpecs("A", noneFlag, "B", noneFlag) + + specsNode1.TrimForParallelization(3, 1) + specsNode2.TrimForParallelization(3, 2) + specsNode3.TrimForParallelization(3, 3) + + Ω(willRunTexts(specsNode1)).Should(Equal([]string{"A"})) + Ω(willRunTexts(specsNode2)).Should(Equal([]string{"B"})) + Ω(willRunTexts(specsNode3)).Should(BeEmpty()) + + Ω(specsNode1.Specs()).Should(HaveLen(1)) + Ω(specsNode2.Specs()).Should(HaveLen(1)) + Ω(specsNode3.Specs()).Should(HaveLen(0)) + + Ω(specsNode1.NumberOfOriginalSpecs()).Should(Equal(2)) + Ω(specsNode2.NumberOfOriginalSpecs()).Should(Equal(2)) + Ω(specsNode3.NumberOfOriginalSpecs()).Should(Equal(2)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/random_id.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/random_id.go new file mode 100644 index 00000000000..a0b8b62d525 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/random_id.go @@ -0,0 +1,15 @@ +package specrunner + +import ( + "crypto/rand" + "fmt" +) + +func randomID() string { + b := make([]byte, 8) + _, err := rand.Read(b) + if err != nil { + return "" + } + return fmt.Sprintf("%x-%x-%x-%x", b[0:2], b[2:4], b[4:6], b[6:8]) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner.go new file mode 100644 index 00000000000..123fdae28d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner.go @@ -0,0 +1,324 @@ +package specrunner + +import ( + "fmt" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/internal/spec" + Writer "github.com/onsi/ginkgo/internal/writer" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + + "time" +) + +type SpecRunner struct { + description string + beforeSuiteNode leafnodes.SuiteNode + specs *spec.Specs + afterSuiteNode leafnodes.SuiteNode + reporters []reporters.Reporter + startTime time.Time + suiteID string + runningSpec *spec.Spec + writer Writer.WriterInterface + config config.GinkgoConfigType + interrupted bool + lock *sync.Mutex +} + +func New(description string, beforeSuiteNode leafnodes.SuiteNode, specs *spec.Specs, afterSuiteNode leafnodes.SuiteNode, reporters []reporters.Reporter, writer Writer.WriterInterface, config config.GinkgoConfigType) *SpecRunner { + return &SpecRunner{ + description: description, + beforeSuiteNode: beforeSuiteNode, + specs: specs, + afterSuiteNode: afterSuiteNode, + reporters: reporters, + writer: writer, + config: config, + suiteID: randomID(), + lock: &sync.Mutex{}, + } +} + +func (runner *SpecRunner) Run() bool { + if runner.config.DryRun { + runner.performDryRun() + return true + } + + runner.reportSuiteWillBegin() + go runner.registerForInterrupts() + + suitePassed := runner.runBeforeSuite() + + if suitePassed { + suitePassed = runner.runSpecs() + } + + runner.blockForeverIfInterrupted() + + suitePassed = runner.runAfterSuite() && suitePassed + + runner.reportSuiteDidEnd(suitePassed) + + return suitePassed +} + +func (runner *SpecRunner) performDryRun() { + runner.reportSuiteWillBegin() + + if runner.beforeSuiteNode != nil { + summary := runner.beforeSuiteNode.Summary() + summary.State = types.SpecStatePassed + runner.reportBeforeSuite(summary) + } + + for _, spec := range runner.specs.Specs() { + summary := spec.Summary(runner.suiteID) + runner.reportSpecWillRun(summary) + if summary.State == types.SpecStateInvalid { + summary.State = types.SpecStatePassed + } + runner.reportSpecDidComplete(summary, false) + } + + if runner.afterSuiteNode != nil { + summary := runner.afterSuiteNode.Summary() + summary.State = types.SpecStatePassed + runner.reportAfterSuite(summary) + } + + runner.reportSuiteDidEnd(true) +} + +func (runner *SpecRunner) runBeforeSuite() bool { + if runner.beforeSuiteNode == nil || runner.wasInterrupted() { + return true + } + + runner.writer.Truncate() + conf := runner.config + passed := runner.beforeSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) + if !passed { + runner.writer.DumpOut() + } + runner.reportBeforeSuite(runner.beforeSuiteNode.Summary()) + return passed +} + +func (runner *SpecRunner) runAfterSuite() bool { + if runner.afterSuiteNode == nil { + return true + } + + runner.writer.Truncate() + conf := runner.config + passed := runner.afterSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) + if !passed { + runner.writer.DumpOut() + } + runner.reportAfterSuite(runner.afterSuiteNode.Summary()) + return passed +} + +func (runner *SpecRunner) runSpecs() bool { + suiteFailed := false + skipRemainingSpecs := false + for _, spec := range runner.specs.Specs() { + if runner.wasInterrupted() { + return suiteFailed + } + if skipRemainingSpecs { + spec.Skip() + } + runner.reportSpecWillRun(spec.Summary(runner.suiteID)) + + if !spec.Skipped() && !spec.Pending() { + runner.runningSpec = spec + spec.Run(runner.writer) + runner.runningSpec = nil + if spec.Failed() { + suiteFailed = true + } + } else if spec.Pending() && runner.config.FailOnPending { + suiteFailed = true + } + + runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) + + if spec.Failed() && runner.config.FailFast { + skipRemainingSpecs = true + } + } + + return !suiteFailed +} + +func (runner *SpecRunner) CurrentSpecSummary() (*types.SpecSummary, bool) { + if runner.runningSpec == nil { + return nil, false + } + + return runner.runningSpec.Summary(runner.suiteID), true +} + +func (runner *SpecRunner) registerForInterrupts() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + <-c + signal.Stop(c) + runner.markInterrupted() + go runner.registerForHardInterrupts() + runner.writer.DumpOutWithHeader(` +Received interrupt. Emitting contents of GinkgoWriter... +--------------------------------------------------------- +`) + if runner.afterSuiteNode != nil { + fmt.Fprint(os.Stderr, ` +--------------------------------------------------------- +Received interrupt. Running AfterSuite... +^C again to terminate immediately +`) + runner.runAfterSuite() + } + runner.reportSuiteDidEnd(false) + os.Exit(1) +} + +func (runner *SpecRunner) registerForHardInterrupts() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + <-c + fmt.Fprintln(os.Stderr, "\nReceived second interrupt. Shutting down.") + os.Exit(1) +} + +func (runner *SpecRunner) blockForeverIfInterrupted() { + runner.lock.Lock() + interrupted := runner.interrupted + runner.lock.Unlock() + + if interrupted { + select {} + } +} + +func (runner *SpecRunner) markInterrupted() { + runner.lock.Lock() + defer runner.lock.Unlock() + runner.interrupted = true +} + +func (runner *SpecRunner) wasInterrupted() bool { + runner.lock.Lock() + defer runner.lock.Unlock() + return runner.interrupted +} + +func (runner *SpecRunner) reportSuiteWillBegin() { + runner.startTime = time.Now() + summary := runner.summary(true) + for _, reporter := range runner.reporters { + reporter.SpecSuiteWillBegin(runner.config, summary) + } +} + +func (runner *SpecRunner) reportBeforeSuite(summary *types.SetupSummary) { + for _, reporter := range runner.reporters { + reporter.BeforeSuiteDidRun(summary) + } +} + +func (runner *SpecRunner) reportAfterSuite(summary *types.SetupSummary) { + for _, reporter := range runner.reporters { + reporter.AfterSuiteDidRun(summary) + } +} + +func (runner *SpecRunner) reportSpecWillRun(summary *types.SpecSummary) { + runner.writer.Truncate() + + for _, reporter := range runner.reporters { + reporter.SpecWillRun(summary) + } +} + +func (runner *SpecRunner) reportSpecDidComplete(summary *types.SpecSummary, failed bool) { + for i := len(runner.reporters) - 1; i >= 1; i-- { + runner.reporters[i].SpecDidComplete(summary) + } + + if failed { + runner.writer.DumpOut() + } + + runner.reporters[0].SpecDidComplete(summary) +} + +func (runner *SpecRunner) reportSuiteDidEnd(success bool) { + summary := runner.summary(success) + summary.RunTime = time.Since(runner.startTime) + for _, reporter := range runner.reporters { + reporter.SpecSuiteDidEnd(summary) + } +} + +func (runner *SpecRunner) countSpecsSatisfying(filter func(ex *spec.Spec) bool) (count int) { + count = 0 + + for _, spec := range runner.specs.Specs() { + if filter(spec) { + count++ + } + } + + return count +} + +func (runner *SpecRunner) summary(success bool) *types.SuiteSummary { + numberOfSpecsThatWillBeRun := runner.countSpecsSatisfying(func(ex *spec.Spec) bool { + return !ex.Skipped() && !ex.Pending() + }) + + numberOfPendingSpecs := runner.countSpecsSatisfying(func(ex *spec.Spec) bool { + return ex.Pending() + }) + + numberOfSkippedSpecs := runner.countSpecsSatisfying(func(ex *spec.Spec) bool { + return ex.Skipped() + }) + + numberOfPassedSpecs := runner.countSpecsSatisfying(func(ex *spec.Spec) bool { + return ex.Passed() + }) + + numberOfFailedSpecs := runner.countSpecsSatisfying(func(ex *spec.Spec) bool { + return ex.Failed() + }) + + if runner.beforeSuiteNode != nil && !runner.beforeSuiteNode.Passed() && !runner.config.DryRun { + numberOfFailedSpecs = numberOfSpecsThatWillBeRun + } + + return &types.SuiteSummary{ + SuiteDescription: runner.description, + SuiteSucceeded: success, + SuiteID: runner.suiteID, + + NumberOfSpecsBeforeParallelization: runner.specs.NumberOfOriginalSpecs(), + NumberOfTotalSpecs: len(runner.specs.Specs()), + NumberOfSpecsThatWillBeRun: numberOfSpecsThatWillBeRun, + NumberOfPendingSpecs: numberOfPendingSpecs, + NumberOfSkippedSpecs: numberOfSkippedSpecs, + NumberOfPassedSpecs: numberOfPassedSpecs, + NumberOfFailedSpecs: numberOfFailedSpecs, + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_suite_test.go new file mode 100644 index 00000000000..c8388fb6f7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_suite_test.go @@ -0,0 +1,13 @@ +package specrunner_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpecRunner(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Spec Runner Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_test.go new file mode 100644 index 00000000000..99686d3e61f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/specrunner/spec_runner_test.go @@ -0,0 +1,623 @@ +package specrunner_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/specrunner" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/containernode" + Failer "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/internal/spec" + Writer "github.com/onsi/ginkgo/internal/writer" + "github.com/onsi/ginkgo/reporters" +) + +var noneFlag = types.FlagTypeNone +var focusedFlag = types.FlagTypeFocused +var pendingFlag = types.FlagTypePending + +var _ = Describe("Spec Runner", func() { + var ( + reporter1 *reporters.FakeReporter + reporter2 *reporters.FakeReporter + failer *Failer.Failer + writer *Writer.FakeGinkgoWriter + + thingsThatRan []string + + runner *SpecRunner + ) + + newBefSuite := func(text string, fail bool) leafnodes.SuiteNode { + return leafnodes.NewBeforeSuiteNode(func() { + writer.AddEvent(text) + thingsThatRan = append(thingsThatRan, text) + if fail { + failer.Fail(text, codelocation.New(0)) + } + }, codelocation.New(0), 0, failer) + } + + newAftSuite := func(text string, fail bool) leafnodes.SuiteNode { + return leafnodes.NewAfterSuiteNode(func() { + writer.AddEvent(text) + thingsThatRan = append(thingsThatRan, text) + if fail { + failer.Fail(text, codelocation.New(0)) + } + }, codelocation.New(0), 0, failer) + } + + newSpec := func(text string, flag types.FlagType, fail bool) *spec.Spec { + subject := leafnodes.NewItNode(text, func() { + writer.AddEvent(text) + thingsThatRan = append(thingsThatRan, text) + if fail { + failer.Fail(text, codelocation.New(0)) + } + }, flag, codelocation.New(0), 0, failer, 0) + + return spec.New(subject, []*containernode.ContainerNode{}, false) + } + + newSpecWithBody := func(text string, body interface{}) *spec.Spec { + subject := leafnodes.NewItNode(text, body, noneFlag, codelocation.New(0), 0, failer, 0) + + return spec.New(subject, []*containernode.ContainerNode{}, false) + } + + newRunner := func(config config.GinkgoConfigType, beforeSuiteNode leafnodes.SuiteNode, afterSuiteNode leafnodes.SuiteNode, specs ...*spec.Spec) *SpecRunner { + return New("description", beforeSuiteNode, spec.NewSpecs(specs), afterSuiteNode, []reporters.Reporter{reporter1, reporter2}, writer, config) + } + + BeforeEach(func() { + reporter1 = reporters.NewFakeReporter() + reporter2 = reporters.NewFakeReporter() + writer = Writer.NewFake() + failer = Failer.New() + + thingsThatRan = []string{} + }) + + Describe("Running and Reporting", func() { + var specA, pendingSpec, anotherPendingSpec, failedSpec, specB, skippedSpec *spec.Spec + var willRunCalls, didCompleteCalls []string + var conf config.GinkgoConfigType + + JustBeforeEach(func() { + willRunCalls = []string{} + didCompleteCalls = []string{} + specA = newSpec("spec A", noneFlag, false) + pendingSpec = newSpec("pending spec", pendingFlag, false) + anotherPendingSpec = newSpec("another pending spec", pendingFlag, false) + failedSpec = newSpec("failed spec", noneFlag, true) + specB = newSpec("spec B", noneFlag, false) + skippedSpec = newSpec("skipped spec", noneFlag, false) + skippedSpec.Skip() + + reporter1.SpecWillRunStub = func(specSummary *types.SpecSummary) { + willRunCalls = append(willRunCalls, "Reporter1") + } + reporter2.SpecWillRunStub = func(specSummary *types.SpecSummary) { + willRunCalls = append(willRunCalls, "Reporter2") + } + + reporter1.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { + didCompleteCalls = append(didCompleteCalls, "Reporter1") + } + reporter2.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { + didCompleteCalls = append(didCompleteCalls, "Reporter2") + } + + runner = newRunner(conf, newBefSuite("BefSuite", false), newAftSuite("AftSuite", false), specA, pendingSpec, anotherPendingSpec, failedSpec, specB, skippedSpec) + runner.Run() + }) + + BeforeEach(func() { + conf = config.GinkgoConfigType{RandomSeed: 17} + }) + + It("should skip skipped/pending tests", func() { + Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "spec A", "failed spec", "spec B", "AftSuite"})) + }) + + It("should report to any attached reporters", func() { + Ω(reporter1.Config).Should(Equal(reporter2.Config)) + Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) + Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) + Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) + Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) + Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) + Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) + }) + + It("should report that a spec did end in reverse order", func() { + Ω(willRunCalls[0:4]).Should(Equal([]string{"Reporter1", "Reporter2", "Reporter1", "Reporter2"})) + Ω(didCompleteCalls[0:4]).Should(Equal([]string{"Reporter2", "Reporter1", "Reporter2", "Reporter1"})) + }) + + It("should report the passed in config", func() { + Ω(reporter1.Config.RandomSeed).Should(BeNumerically("==", 17)) + }) + + It("should report the beginning of the suite", func() { + Ω(reporter1.BeginSummary.SuiteDescription).Should(Equal("description")) + Ω(reporter1.BeginSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) + Ω(reporter1.BeginSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) + Ω(reporter1.BeginSummary.NumberOfTotalSpecs).Should(Equal(6)) + Ω(reporter1.BeginSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) + Ω(reporter1.BeginSummary.NumberOfPendingSpecs).Should(Equal(2)) + Ω(reporter1.BeginSummary.NumberOfSkippedSpecs).Should(Equal(1)) + }) + + It("should report the end of the suite", func() { + Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) + Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) + Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) + Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) + Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(2)) + Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) + Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(2)) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(1)) + }) + + Context("when told to perform a dry run", func() { + BeforeEach(func() { + conf.DryRun = true + }) + + It("should report to the reporters", func() { + Ω(reporter1.Config).Should(Equal(reporter2.Config)) + Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) + Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) + Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) + Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) + Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) + Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) + }) + + It("should not actually run anything", func() { + Ω(thingsThatRan).Should(BeEmpty()) + }) + + It("report before and after suites as passed", func() { + Ω(reporter1.BeforeSuiteSummary.State).Should(Equal(types.SpecStatePassed)) + Ω(reporter1.AfterSuiteSummary.State).Should(Equal(types.SpecStatePassed)) + }) + + It("should report specs as passed", func() { + summaries := reporter1.SpecSummaries + Ω(summaries).Should(HaveLen(6)) + Ω(summaries[0].ComponentTexts).Should(ContainElement("spec A")) + Ω(summaries[0].State).Should(Equal(types.SpecStatePassed)) + Ω(summaries[1].ComponentTexts).Should(ContainElement("pending spec")) + Ω(summaries[1].State).Should(Equal(types.SpecStatePending)) + Ω(summaries[2].ComponentTexts).Should(ContainElement("another pending spec")) + Ω(summaries[2].State).Should(Equal(types.SpecStatePending)) + Ω(summaries[3].ComponentTexts).Should(ContainElement("failed spec")) + Ω(summaries[3].State).Should(Equal(types.SpecStatePassed)) + Ω(summaries[4].ComponentTexts).Should(ContainElement("spec B")) + Ω(summaries[4].State).Should(Equal(types.SpecStatePassed)) + Ω(summaries[5].ComponentTexts).Should(ContainElement("skipped spec")) + Ω(summaries[5].State).Should(Equal(types.SpecStateSkipped)) + }) + + It("should report the end of the suite", func() { + Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) + Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) + Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) + Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) + Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) + Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(2)) + Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) + Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(0)) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) + }) + }) + }) + + Describe("reporting on specs", func() { + var proceed chan bool + var ready chan bool + var finished chan bool + BeforeEach(func() { + ready = make(chan bool) + proceed = make(chan bool) + finished = make(chan bool) + skippedSpec := newSpec("SKIP", noneFlag, false) + skippedSpec.Skip() + + runner = newRunner( + config.GinkgoConfigType{}, + newBefSuite("BefSuite", false), + newAftSuite("AftSuite", false), + skippedSpec, + newSpec("PENDING", pendingFlag, false), + newSpecWithBody("RUN", func() { + close(ready) + <-proceed + }), + ) + go func() { + runner.Run() + close(finished) + }() + }) + + It("should report about pending/skipped specs", func() { + <-ready + Ω(reporter1.SpecWillRunSummaries).Should(HaveLen(3)) + + Ω(reporter1.SpecWillRunSummaries[0].ComponentTexts[0]).Should(Equal("SKIP")) + Ω(reporter1.SpecWillRunSummaries[1].ComponentTexts[0]).Should(Equal("PENDING")) + Ω(reporter1.SpecWillRunSummaries[2].ComponentTexts[0]).Should(Equal("RUN")) + + Ω(reporter1.SpecSummaries[0].ComponentTexts[0]).Should(Equal("SKIP")) + Ω(reporter1.SpecSummaries[1].ComponentTexts[0]).Should(Equal("PENDING")) + Ω(reporter1.SpecSummaries).Should(HaveLen(2)) + + close(proceed) + <-finished + + Ω(reporter1.SpecSummaries).Should(HaveLen(3)) + Ω(reporter1.SpecSummaries[2].ComponentTexts[0]).Should(Equal("RUN")) + }) + }) + + Describe("Running BeforeSuite & AfterSuite", func() { + var success bool + var befSuite leafnodes.SuiteNode + var aftSuite leafnodes.SuiteNode + Context("with a nil BeforeSuite & AfterSuite", func() { + BeforeEach(func() { + runner = newRunner( + config.GinkgoConfigType{}, + nil, + nil, + newSpec("A", noneFlag, false), + newSpec("B", noneFlag, false), + ) + success = runner.Run() + }) + + It("should not report about the BeforeSuite", func() { + Ω(reporter1.BeforeSuiteSummary).Should(BeNil()) + }) + + It("should not report about the AfterSuite", func() { + Ω(reporter1.AfterSuiteSummary).Should(BeNil()) + }) + + It("should run the specs", func() { + Ω(thingsThatRan).Should(Equal([]string{"A", "B"})) + }) + }) + + Context("when the BeforeSuite & AfterSuite pass", func() { + BeforeEach(func() { + befSuite = newBefSuite("BefSuite", false) + aftSuite = newBefSuite("AftSuite", false) + runner = newRunner( + config.GinkgoConfigType{}, + befSuite, + aftSuite, + newSpec("A", noneFlag, false), + newSpec("B", noneFlag, false), + ) + success = runner.Run() + }) + + It("should run the BeforeSuite, the AfterSuite and the specs", func() { + Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "A", "B", "AftSuite"})) + }) + + It("should report about the BeforeSuite", func() { + Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) + }) + + It("should report about the AfterSuite", func() { + Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) + }) + + It("should report success", func() { + Ω(success).Should(BeTrue()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) + }) + + It("should not dump the writer", func() { + Ω(writer.EventStream).ShouldNot(ContainElement("DUMP")) + }) + }) + + Context("when the BeforeSuite fails", func() { + BeforeEach(func() { + befSuite = newBefSuite("BefSuite", true) + aftSuite = newBefSuite("AftSuite", false) + + skipped := newSpec("Skipped", noneFlag, false) + skipped.Skip() + + runner = newRunner( + config.GinkgoConfigType{}, + befSuite, + aftSuite, + newSpec("A", noneFlag, false), + newSpec("B", noneFlag, false), + newSpec("Pending", pendingFlag, false), + skipped, + ) + success = runner.Run() + }) + + It("should not run the specs, but it should run the AfterSuite", func() { + Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "AftSuite"})) + }) + + It("should report about the BeforeSuite", func() { + Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) + }) + + It("should report about the AfterSuite", func() { + Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) + }) + + It("should report failure", func() { + Ω(success).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(2)) + Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(2)) + }) + + It("should dump the writer", func() { + Ω(writer.EventStream).Should(ContainElement("DUMP")) + }) + }) + + Context("when some other test fails", func() { + BeforeEach(func() { + aftSuite = newBefSuite("AftSuite", false) + + runner = newRunner( + config.GinkgoConfigType{}, + nil, + aftSuite, + newSpec("A", noneFlag, true), + ) + success = runner.Run() + }) + + It("should still run the AfterSuite", func() { + Ω(thingsThatRan).Should(Equal([]string{"A", "AftSuite"})) + }) + + It("should report about the AfterSuite", func() { + Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) + }) + + It("should report failure", func() { + Ω(success).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(1)) + Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(1)) + }) + }) + + Context("when the AfterSuite fails", func() { + BeforeEach(func() { + befSuite = newBefSuite("BefSuite", false) + aftSuite = newBefSuite("AftSuite", true) + runner = newRunner( + config.GinkgoConfigType{}, + befSuite, + aftSuite, + newSpec("A", noneFlag, false), + newSpec("B", noneFlag, false), + ) + success = runner.Run() + }) + + It("should run everything", func() { + Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "A", "B", "AftSuite"})) + }) + + It("should report about the BeforeSuite", func() { + Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) + }) + + It("should report about the AfterSuite", func() { + Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) + }) + + It("should report failure", func() { + Ω(success).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) + }) + + It("should dump the writer", func() { + Ω(writer.EventStream).Should(ContainElement("DUMP")) + }) + }) + }) + + Describe("When instructed to fail fast", func() { + BeforeEach(func() { + conf := config.GinkgoConfigType{ + FailFast: true, + } + runner = newRunner(conf, nil, newAftSuite("after-suite", false), newSpec("passing", noneFlag, false), newSpec("failing", noneFlag, true), newSpec("dont-see", noneFlag, true), newSpec("dont-see", noneFlag, true)) + }) + + It("should return false, report failure, and not run anything past the failing test", func() { + Ω(runner.Run()).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + Ω(thingsThatRan).Should(Equal([]string{"passing", "failing", "after-suite"})) + }) + + It("should announce the subsequent specs as skipped", func() { + runner.Run() + Ω(reporter1.SpecSummaries).Should(HaveLen(4)) + Ω(reporter1.SpecSummaries[2].State).Should(Equal(types.SpecStateSkipped)) + Ω(reporter1.SpecSummaries[3].State).Should(Equal(types.SpecStateSkipped)) + }) + + It("should mark all subsequent specs as skipped", func() { + runner.Run() + Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(2)) + }) + }) + + Describe("Marking failure and success", func() { + Context("when all tests pass", func() { + BeforeEach(func() { + runner = newRunner(config.GinkgoConfigType{}, nil, nil, newSpec("passing", noneFlag, false), newSpec("pending", pendingFlag, false)) + }) + + It("should return true and report success", func() { + Ω(runner.Run()).Should(BeTrue()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) + }) + }) + + Context("when a test fails", func() { + BeforeEach(func() { + runner = newRunner(config.GinkgoConfigType{}, nil, nil, newSpec("failing", noneFlag, true), newSpec("pending", pendingFlag, false)) + }) + + It("should return false and report failure", func() { + Ω(runner.Run()).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + }) + }) + + Context("when there is a pending test, but pendings count as failures", func() { + BeforeEach(func() { + runner = newRunner(config.GinkgoConfigType{FailOnPending: true}, nil, nil, newSpec("passing", noneFlag, false), newSpec("pending", pendingFlag, false)) + }) + + It("should return false and report failure", func() { + Ω(runner.Run()).Should(BeFalse()) + Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) + }) + }) + }) + + Describe("Managing the writer", func() { + BeforeEach(func() { + runner = newRunner( + config.GinkgoConfigType{}, + nil, + nil, + newSpec("A", noneFlag, false), + newSpec("B", noneFlag, true), + newSpec("C", noneFlag, false), + ) + reporter1.SpecWillRunStub = func(specSummary *types.SpecSummary) { + writer.AddEvent("R1.WillRun") + } + reporter2.SpecWillRunStub = func(specSummary *types.SpecSummary) { + writer.AddEvent("R2.WillRun") + } + reporter1.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { + writer.AddEvent("R1.DidComplete") + } + reporter2.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { + writer.AddEvent("R2.DidComplete") + } + runner.Run() + }) + + It("should truncate between tests, but only dump if a test fails", func() { + Ω(writer.EventStream).Should(Equal([]string{ + "TRUNCATE", + "R1.WillRun", + "R2.WillRun", + "A", + "R2.DidComplete", + "R1.DidComplete", + "TRUNCATE", + "R1.WillRun", + "R2.WillRun", + "B", + "R2.DidComplete", + "DUMP", + "R1.DidComplete", + "TRUNCATE", + "R1.WillRun", + "R2.WillRun", + "C", + "R2.DidComplete", + "R1.DidComplete", + })) + }) + }) + + Describe("CurrentSpecSummary", func() { + It("should return the spec summary for the currently running spec", func() { + var summary *types.SpecSummary + runner = newRunner( + config.GinkgoConfigType{}, + nil, + nil, + newSpec("A", noneFlag, false), + newSpecWithBody("B", func() { + var ok bool + summary, ok = runner.CurrentSpecSummary() + Ω(ok).Should(BeTrue()) + }), + newSpec("C", noneFlag, false), + ) + runner.Run() + + Ω(summary.ComponentTexts).Should(Equal([]string{"B"})) + + summary, ok := runner.CurrentSpecSummary() + Ω(summary).Should(BeNil()) + Ω(ok).Should(BeFalse()) + }) + }) + + Context("When running tests in parallel", func() { + It("reports the correct number of specs before parallelization", func() { + specs := spec.NewSpecs([]*spec.Spec{ + newSpec("A", noneFlag, false), + newSpec("B", pendingFlag, false), + newSpec("C", noneFlag, false), + }) + specs.TrimForParallelization(2, 1) + runner = New("description", nil, specs, nil, []reporters.Reporter{reporter1, reporter2}, writer, config.GinkgoConfigType{}) + runner.Run() + + Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(3)) + Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(2)) + Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(1)) + Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(1)) + }) + }) + + Describe("generating a suite id", func() { + It("should generate an id randomly", func() { + runnerA := newRunner(config.GinkgoConfigType{}, nil, nil) + runnerA.Run() + IDA := reporter1.BeginSummary.SuiteID + + runnerB := newRunner(config.GinkgoConfigType{}, nil, nil) + runnerB.Run() + IDB := reporter1.BeginSummary.SuiteID + + IDRegexp := "[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}" + Ω(IDA).Should(MatchRegexp(IDRegexp)) + Ω(IDB).Should(MatchRegexp(IDRegexp)) + + Ω(IDA).ShouldNot(Equal(IDB)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite.go new file mode 100644 index 00000000000..a054602f78b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite.go @@ -0,0 +1,171 @@ +package suite + +import ( + "math/rand" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/containernode" + "github.com/onsi/ginkgo/internal/failer" + "github.com/onsi/ginkgo/internal/leafnodes" + "github.com/onsi/ginkgo/internal/spec" + "github.com/onsi/ginkgo/internal/specrunner" + "github.com/onsi/ginkgo/internal/writer" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" +) + +type ginkgoTestingT interface { + Fail() +} + +type Suite struct { + topLevelContainer *containernode.ContainerNode + currentContainer *containernode.ContainerNode + containerIndex int + beforeSuiteNode leafnodes.SuiteNode + afterSuiteNode leafnodes.SuiteNode + runner *specrunner.SpecRunner + failer *failer.Failer + running bool +} + +func New(failer *failer.Failer) *Suite { + topLevelContainer := containernode.New("[Top Level]", types.FlagTypeNone, types.CodeLocation{}) + + return &Suite{ + topLevelContainer: topLevelContainer, + currentContainer: topLevelContainer, + failer: failer, + containerIndex: 1, + } +} + +func (suite *Suite) Run(t ginkgoTestingT, description string, reporters []reporters.Reporter, writer writer.WriterInterface, config config.GinkgoConfigType) (bool, bool) { + if config.ParallelTotal < 1 { + panic("ginkgo.parallel.total must be >= 1") + } + + if config.ParallelNode > config.ParallelTotal || config.ParallelNode < 1 { + panic("ginkgo.parallel.node is one-indexed and must be <= ginkgo.parallel.total") + } + + r := rand.New(rand.NewSource(config.RandomSeed)) + suite.topLevelContainer.Shuffle(r) + specs := suite.generateSpecs(description, config) + suite.runner = specrunner.New(description, suite.beforeSuiteNode, specs, suite.afterSuiteNode, reporters, writer, config) + + suite.running = true + success := suite.runner.Run() + if !success { + t.Fail() + } + return success, specs.HasProgrammaticFocus() +} + +func (suite *Suite) generateSpecs(description string, config config.GinkgoConfigType) *spec.Specs { + specsSlice := []*spec.Spec{} + suite.topLevelContainer.BackPropagateProgrammaticFocus() + for _, collatedNodes := range suite.topLevelContainer.Collate() { + specsSlice = append(specsSlice, spec.New(collatedNodes.Subject, collatedNodes.Containers, config.EmitSpecProgress)) + } + + specs := spec.NewSpecs(specsSlice) + + if config.RandomizeAllSpecs { + specs.Shuffle(rand.New(rand.NewSource(config.RandomSeed))) + } + + specs.ApplyFocus(description, config.FocusString, config.SkipString) + + if config.SkipMeasurements { + specs.SkipMeasurements() + } + + if config.ParallelTotal > 1 { + specs.TrimForParallelization(config.ParallelTotal, config.ParallelNode) + } + + return specs +} + +func (suite *Suite) CurrentRunningSpecSummary() (*types.SpecSummary, bool) { + return suite.runner.CurrentSpecSummary() +} + +func (suite *Suite) SetBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.beforeSuiteNode != nil { + panic("You may only call BeforeSuite once!") + } + suite.beforeSuiteNode = leafnodes.NewBeforeSuiteNode(body, codeLocation, timeout, suite.failer) +} + +func (suite *Suite) SetAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.afterSuiteNode != nil { + panic("You may only call AfterSuite once!") + } + suite.afterSuiteNode = leafnodes.NewAfterSuiteNode(body, codeLocation, timeout, suite.failer) +} + +func (suite *Suite) SetSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.beforeSuiteNode != nil { + panic("You may only call BeforeSuite once!") + } + suite.beforeSuiteNode = leafnodes.NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) +} + +func (suite *Suite) SetSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.afterSuiteNode != nil { + panic("You may only call AfterSuite once!") + } + suite.afterSuiteNode = leafnodes.NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) +} + +func (suite *Suite) PushContainerNode(text string, body func(), flag types.FlagType, codeLocation types.CodeLocation) { + container := containernode.New(text, flag, codeLocation) + suite.currentContainer.PushContainerNode(container) + + previousContainer := suite.currentContainer + suite.currentContainer = container + suite.containerIndex++ + + body() + + suite.containerIndex-- + suite.currentContainer = previousContainer +} + +func (suite *Suite) PushItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.running { + suite.failer.Fail("You may only call It from within a Describe or Context", codeLocation) + } + suite.currentContainer.PushSubjectNode(leafnodes.NewItNode(text, body, flag, codeLocation, timeout, suite.failer, suite.containerIndex)) +} + +func (suite *Suite) PushMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int) { + if suite.running { + suite.failer.Fail("You may only call Measure from within a Describe or Context", codeLocation) + } + suite.currentContainer.PushSubjectNode(leafnodes.NewMeasureNode(text, body, flag, codeLocation, samples, suite.failer, suite.containerIndex)) +} + +func (suite *Suite) PushBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.running { + suite.failer.Fail("You may only call BeforeEach from within a Describe or Context", codeLocation) + } + suite.currentContainer.PushSetupNode(leafnodes.NewBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) +} + +func (suite *Suite) PushJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.running { + suite.failer.Fail("You may only call JustBeforeEach from within a Describe or Context", codeLocation) + } + suite.currentContainer.PushSetupNode(leafnodes.NewJustBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) +} + +func (suite *Suite) PushAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { + if suite.running { + suite.failer.Fail("You may only call AfterEach from within a Describe or Context", codeLocation) + } + suite.currentContainer.PushSetupNode(leafnodes.NewAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_suite_test.go new file mode 100644 index 00000000000..06fe1d12aba --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_suite_test.go @@ -0,0 +1,35 @@ +package suite_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func Test(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Suite") +} + +var numBeforeSuiteRuns = 0 +var numAfterSuiteRuns = 0 + +var _ = BeforeSuite(func() { + numBeforeSuiteRuns++ +}) + +var _ = AfterSuite(func() { + numAfterSuiteRuns++ + Ω(numBeforeSuiteRuns).Should(Equal(1)) + Ω(numAfterSuiteRuns).Should(Equal(1)) +}) + +//Fakes +type fakeTestingT struct { + didFail bool +} + +func (fakeT *fakeTestingT) Fail() { + fakeT.didFail = true +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_test.go new file mode 100644 index 00000000000..334211a092c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/suite/suite_test.go @@ -0,0 +1,398 @@ +package suite_test + +import ( + "bytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/suite" + . "github.com/onsi/gomega" + + "math/rand" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/codelocation" + Failer "github.com/onsi/ginkgo/internal/failer" + Writer "github.com/onsi/ginkgo/internal/writer" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Suite", func() { + var ( + specSuite *Suite + fakeT *fakeTestingT + fakeR *reporters.FakeReporter + writer *Writer.FakeGinkgoWriter + failer *Failer.Failer + ) + + BeforeEach(func() { + writer = Writer.NewFake() + fakeT = &fakeTestingT{} + fakeR = reporters.NewFakeReporter() + failer = Failer.New() + specSuite = New(failer) + }) + + Describe("running a suite", func() { + var ( + runOrder []string + randomizeAllSpecs bool + randomSeed int64 + focusString string + parallelNode int + parallelTotal int + runResult bool + hasProgrammaticFocus bool + ) + + var f = func(runText string) func() { + return func() { + runOrder = append(runOrder, runText) + } + } + + BeforeEach(func() { + randomizeAllSpecs = false + randomSeed = 11 + parallelNode = 1 + parallelTotal = 1 + focusString = "" + + runOrder = make([]string, 0) + specSuite.SetBeforeSuiteNode(f("BeforeSuite"), codelocation.New(0), 0) + specSuite.PushBeforeEachNode(f("top BE"), codelocation.New(0), 0) + specSuite.PushJustBeforeEachNode(f("top JBE"), codelocation.New(0), 0) + specSuite.PushAfterEachNode(f("top AE"), codelocation.New(0), 0) + + specSuite.PushContainerNode("container", func() { + specSuite.PushBeforeEachNode(f("BE"), codelocation.New(0), 0) + specSuite.PushJustBeforeEachNode(f("JBE"), codelocation.New(0), 0) + specSuite.PushAfterEachNode(f("AE"), codelocation.New(0), 0) + specSuite.PushItNode("it", f("IT"), types.FlagTypeNone, codelocation.New(0), 0) + + specSuite.PushContainerNode("inner container", func() { + specSuite.PushItNode("inner it", f("inner IT"), types.FlagTypeNone, codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0)) + }, types.FlagTypeNone, codelocation.New(0)) + + specSuite.PushContainerNode("container 2", func() { + specSuite.PushBeforeEachNode(f("BE 2"), codelocation.New(0), 0) + specSuite.PushItNode("it 2", f("IT 2"), types.FlagTypeNone, codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0)) + + specSuite.PushItNode("top level it", f("top IT"), types.FlagTypeNone, codelocation.New(0), 0) + + specSuite.SetAfterSuiteNode(f("AfterSuite"), codelocation.New(0), 0) + }) + + JustBeforeEach(func() { + runResult, hasProgrammaticFocus = specSuite.Run(fakeT, "suite description", []reporters.Reporter{fakeR}, writer, config.GinkgoConfigType{ + RandomSeed: randomSeed, + RandomizeAllSpecs: randomizeAllSpecs, + FocusString: focusString, + ParallelNode: parallelNode, + ParallelTotal: parallelTotal, + }) + }) + + It("provides the config and suite description to the reporter", func() { + Ω(fakeR.Config.RandomSeed).Should(Equal(int64(randomSeed))) + Ω(fakeR.Config.RandomizeAllSpecs).Should(Equal(randomizeAllSpecs)) + Ω(fakeR.BeginSummary.SuiteDescription).Should(Equal("suite description")) + }) + + It("reports that the BeforeSuite node ran", func() { + Ω(fakeR.BeforeSuiteSummary).ShouldNot(BeNil()) + }) + + It("reports that the AfterSuite node ran", func() { + Ω(fakeR.AfterSuiteSummary).ShouldNot(BeNil()) + }) + + It("provides information about the current test", func() { + description := CurrentGinkgoTestDescription() + Ω(description.ComponentTexts).Should(Equal([]string{"Suite", "running a suite", "provides information about the current test"})) + Ω(description.FullTestText).Should(Equal("Suite running a suite provides information about the current test")) + Ω(description.TestText).Should(Equal("provides information about the current test")) + Ω(description.IsMeasurement).Should(BeFalse()) + Ω(description.FileName).Should(ContainSubstring("suite_test.go")) + Ω(description.LineNumber).Should(BeNumerically(">", 50)) + Ω(description.LineNumber).Should(BeNumerically("<", 150)) + }) + + Measure("should run measurements", func(b Benchmarker) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + runtime := b.Time("sleeping", func() { + sleepTime := time.Duration(r.Float64() * 0.01 * float64(time.Second)) + time.Sleep(sleepTime) + }) + Ω(runtime.Seconds()).Should(BeNumerically("<=", 0.015)) + Ω(runtime.Seconds()).Should(BeNumerically(">=", 0)) + + randomValue := r.Float64() * 10.0 + b.RecordValue("random value", randomValue) + Ω(randomValue).Should(BeNumerically("<=", 10.0)) + Ω(randomValue).Should(BeNumerically(">=", 0.0)) + }, 10) + + It("creates a node hierarchy, converts it to a spec collection, and runs it", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE", + "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", + "top BE", "BE 2", "top JBE", "IT 2", "top AE", + "top BE", "top JBE", "top IT", "top AE", + "AfterSuite", + })) + }) + + Context("when told to randomize all specs", func() { + BeforeEach(func() { + randomizeAllSpecs = true + }) + + It("does", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "top JBE", "top IT", "top AE", + "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", + "top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE", + "top BE", "BE 2", "top JBE", "IT 2", "top AE", + "AfterSuite", + })) + }) + }) + + Describe("with ginkgo.parallel.total > 1", func() { + BeforeEach(func() { + parallelTotal = 2 + randomizeAllSpecs = true + }) + + Context("for one worker", func() { + BeforeEach(func() { + parallelNode = 1 + }) + + It("should run a subset of tests", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "top JBE", "top IT", "top AE", + "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", + "AfterSuite", + })) + }) + }) + + Context("for another worker", func() { + BeforeEach(func() { + parallelNode = 2 + }) + + It("should run a (different) subset of tests", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE", + "top BE", "BE 2", "top JBE", "IT 2", "top AE", + "AfterSuite", + })) + }) + }) + }) + + Context("when provided with a filter", func() { + BeforeEach(func() { + focusString = `inner|\d` + }) + + It("converts the filter to a regular expression and uses it to filter the running specs", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", + "top BE", "BE 2", "top JBE", "IT 2", "top AE", + "AfterSuite", + })) + }) + + It("should not report a programmatic focus", func() { + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + + Context("with a programatically focused spec", func() { + BeforeEach(func() { + specSuite.PushItNode("focused it", f("focused it"), types.FlagTypeFocused, codelocation.New(0), 0) + + specSuite.PushContainerNode("focused container", func() { + specSuite.PushItNode("inner focused it", f("inner focused it"), types.FlagTypeFocused, codelocation.New(0), 0) + specSuite.PushItNode("inner unfocused it", f("inner unfocused it"), types.FlagTypeNone, codelocation.New(0), 0) + }, types.FlagTypeFocused, codelocation.New(0)) + + }) + + It("should only run the focused test, applying backpropagation to favor most deeply focused leaf nodes", func() { + Ω(runOrder).Should(Equal([]string{ + "BeforeSuite", + "top BE", "top JBE", "focused it", "top AE", + "top BE", "top JBE", "inner focused it", "top AE", + "AfterSuite", + })) + }) + + It("should report a programmatic focus", func() { + Ω(hasProgrammaticFocus).Should(BeTrue()) + }) + }) + + Context("when the specs pass", func() { + It("doesn't report a failure", func() { + Ω(fakeT.didFail).Should(BeFalse()) + }) + + It("should return true", func() { + Ω(runResult).Should(BeTrue()) + }) + }) + + Context("when a spec fails", func() { + var location types.CodeLocation + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + location = codelocation.New(0) + failer.Fail("oops!", location) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should return false", func() { + Ω(runResult).Should(BeFalse()) + }) + + It("reports a failure", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + + It("generates the correct failure data", func() { + Ω(fakeR.SpecSummaries[0].Failure.Message).Should(Equal("oops!")) + Ω(fakeR.SpecSummaries[0].Failure.Location).Should(Equal(location)) + }) + }) + + Context("when runnable nodes are nested within other runnable nodes", func() { + Context("when an It is nested", func() { + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + specSuite.PushItNode("nested it", f("oops"), types.FlagTypeNone, codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should fail", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + }) + + Context("when a Measure is nested", func() { + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + specSuite.PushMeasureNode("nested measure", func(Benchmarker) {}, types.FlagTypeNone, codelocation.New(0), 10) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should fail", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + }) + + Context("when a BeforeEach is nested", func() { + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + specSuite.PushBeforeEachNode(f("nested bef"), codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should fail", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + }) + + Context("when a JustBeforeEach is nested", func() { + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + specSuite.PushJustBeforeEachNode(f("nested jbef"), codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should fail", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + }) + + Context("when a AfterEach is nested", func() { + BeforeEach(func() { + specSuite.PushItNode("top level it", func() { + specSuite.PushAfterEachNode(f("nested aft"), codelocation.New(0), 0) + }, types.FlagTypeNone, codelocation.New(0), 0) + }) + + It("should fail", func() { + Ω(fakeT.didFail).Should(BeTrue()) + }) + }) + }) + }) + + Describe("BeforeSuite", func() { + Context("when setting BeforeSuite more than once", func() { + It("should panic", func() { + specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0) + + Ω(func() { + specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0) + }).Should(Panic()) + + }) + }) + }) + + Describe("AfterSuite", func() { + Context("when setting AfterSuite more than once", func() { + It("should panic", func() { + specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0) + + Ω(func() { + specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0) + }).Should(Panic()) + }) + }) + }) + + Describe("By", func() { + It("writes to the GinkgoWriter", func() { + originalGinkgoWriter := GinkgoWriter + buffer := &bytes.Buffer{} + + GinkgoWriter = buffer + By("Saying Hello GinkgoWriter") + GinkgoWriter = originalGinkgoWriter + + Ω(buffer.String()).Should(ContainSubstring("STEP")) + Ω(buffer.String()).Should(ContainSubstring(": Saying Hello GinkgoWriter\n")) + }) + + It("calls the passed-in callback if present", func() { + a := 0 + By("calling the callback", func() { + a = 1 + }) + Ω(a).Should(Equal(1)) + }) + + It("panics if there is more than one callback", func() { + Ω(func() { + By("registering more than one callback", func() {}, func() {}) + }).Should(Panic()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/testingtproxy/testing_t_proxy.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/testingtproxy/testing_t_proxy.go new file mode 100644 index 00000000000..a2b9af80629 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/testingtproxy/testing_t_proxy.go @@ -0,0 +1,76 @@ +package testingtproxy + +import ( + "fmt" + "io" +) + +type failFunc func(message string, callerSkip ...int) + +func New(writer io.Writer, fail failFunc, offset int) *ginkgoTestingTProxy { + return &ginkgoTestingTProxy{ + fail: fail, + offset: offset, + writer: writer, + } +} + +type ginkgoTestingTProxy struct { + fail failFunc + offset int + writer io.Writer +} + +func (t *ginkgoTestingTProxy) Error(args ...interface{}) { + t.fail(fmt.Sprintln(args...), t.offset) +} + +func (t *ginkgoTestingTProxy) Errorf(format string, args ...interface{}) { + t.fail(fmt.Sprintf(format, args...), t.offset) +} + +func (t *ginkgoTestingTProxy) Fail() { + t.fail("failed", t.offset) +} + +func (t *ginkgoTestingTProxy) FailNow() { + t.fail("failed", t.offset) +} + +func (t *ginkgoTestingTProxy) Fatal(args ...interface{}) { + t.fail(fmt.Sprintln(args...), t.offset) +} + +func (t *ginkgoTestingTProxy) Fatalf(format string, args ...interface{}) { + t.fail(fmt.Sprintf(format, args...), t.offset) +} + +func (t *ginkgoTestingTProxy) Log(args ...interface{}) { + fmt.Fprintln(t.writer, args...) +} + +func (t *ginkgoTestingTProxy) Logf(format string, args ...interface{}) { + fmt.Fprintf(t.writer, format, args...) +} + +func (t *ginkgoTestingTProxy) Failed() bool { + return false +} + +func (t *ginkgoTestingTProxy) Parallel() { +} + +func (t *ginkgoTestingTProxy) Skip(args ...interface{}) { + fmt.Println(args...) +} + +func (t *ginkgoTestingTProxy) Skipf(format string, args ...interface{}) { + fmt.Printf(format, args...) +} + +func (t *ginkgoTestingTProxy) SkipNow() { +} + +func (t *ginkgoTestingTProxy) Skipped() bool { + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/fake_writer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/fake_writer.go new file mode 100644 index 00000000000..ac6540f0c1d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/fake_writer.go @@ -0,0 +1,31 @@ +package writer + +type FakeGinkgoWriter struct { + EventStream []string +} + +func NewFake() *FakeGinkgoWriter { + return &FakeGinkgoWriter{ + EventStream: []string{}, + } +} + +func (writer *FakeGinkgoWriter) AddEvent(event string) { + writer.EventStream = append(writer.EventStream, event) +} + +func (writer *FakeGinkgoWriter) Truncate() { + writer.EventStream = append(writer.EventStream, "TRUNCATE") +} + +func (writer *FakeGinkgoWriter) DumpOut() { + writer.EventStream = append(writer.EventStream, "DUMP") +} + +func (writer *FakeGinkgoWriter) DumpOutWithHeader(header string) { + writer.EventStream = append(writer.EventStream, "DUMP_WITH_HEADER: "+header) +} + +func (writer *FakeGinkgoWriter) Write(data []byte) (n int, err error) { + return 0, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer.go new file mode 100644 index 00000000000..7678fc1d9cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer.go @@ -0,0 +1,71 @@ +package writer + +import ( + "bytes" + "io" + "sync" +) + +type WriterInterface interface { + io.Writer + + Truncate() + DumpOut() + DumpOutWithHeader(header string) +} + +type Writer struct { + buffer *bytes.Buffer + outWriter io.Writer + lock *sync.Mutex + stream bool +} + +func New(outWriter io.Writer) *Writer { + return &Writer{ + buffer: &bytes.Buffer{}, + lock: &sync.Mutex{}, + outWriter: outWriter, + stream: true, + } +} + +func (w *Writer) SetStream(stream bool) { + w.lock.Lock() + defer w.lock.Unlock() + w.stream = stream +} + +func (w *Writer) Write(b []byte) (n int, err error) { + w.lock.Lock() + defer w.lock.Unlock() + + if w.stream { + return w.outWriter.Write(b) + } else { + return w.buffer.Write(b) + } +} + +func (w *Writer) Truncate() { + w.lock.Lock() + defer w.lock.Unlock() + w.buffer.Reset() +} + +func (w *Writer) DumpOut() { + w.lock.Lock() + defer w.lock.Unlock() + if !w.stream { + w.buffer.WriteTo(w.outWriter) + } +} + +func (w *Writer) DumpOutWithHeader(header string) { + w.lock.Lock() + defer w.lock.Unlock() + if !w.stream && w.buffer.Len() > 0 { + w.outWriter.Write([]byte(header)) + w.buffer.WriteTo(w.outWriter) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_suite_test.go new file mode 100644 index 00000000000..e206577919a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_suite_test.go @@ -0,0 +1,13 @@ +package writer_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestWriter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Writer Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_test.go new file mode 100644 index 00000000000..3e1d17c6d50 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/internal/writer/writer_test.go @@ -0,0 +1,75 @@ +package writer_test + +import ( + "github.com/onsi/gomega/gbytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/writer" + . "github.com/onsi/gomega" +) + +var _ = Describe("Writer", func() { + var writer *Writer + var out *gbytes.Buffer + + BeforeEach(func() { + out = gbytes.NewBuffer() + writer = New(out) + }) + + It("should stream directly to the outbuffer by default", func() { + writer.Write([]byte("foo")) + Ω(out).Should(gbytes.Say("foo")) + }) + + It("should not emit the header when asked to DumpOutWitHeader", func() { + writer.Write([]byte("foo")) + writer.DumpOutWithHeader("my header") + Ω(out).ShouldNot(gbytes.Say("my header")) + Ω(out).Should(gbytes.Say("foo")) + }) + + Context("when told not to stream", func() { + BeforeEach(func() { + writer.SetStream(false) + }) + + It("should only write to the buffer when told to DumpOut", func() { + writer.Write([]byte("foo")) + Ω(out).ShouldNot(gbytes.Say("foo")) + writer.DumpOut() + Ω(out).Should(gbytes.Say("foo")) + }) + + It("should truncate the internal buffer when told to truncate", func() { + writer.Write([]byte("foo")) + writer.Truncate() + writer.DumpOut() + Ω(out).ShouldNot(gbytes.Say("foo")) + + writer.Write([]byte("bar")) + writer.DumpOut() + Ω(out).Should(gbytes.Say("bar")) + }) + + Describe("emitting a header", func() { + Context("when the buffer has content", func() { + It("should emit the header followed by the content", func() { + writer.Write([]byte("foo")) + writer.DumpOutWithHeader("my header") + + Ω(out).Should(gbytes.Say("my header")) + Ω(out).Should(gbytes.Say("foo")) + }) + }) + + Context("when the buffer has no content", func() { + It("should not emit the header", func() { + writer.DumpOutWithHeader("my header") + + Ω(out).ShouldNot(gbytes.Say("my header")) + }) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter.go new file mode 100644 index 00000000000..45a44deed92 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter.go @@ -0,0 +1,83 @@ +/* +Ginkgo's Default Reporter + +A number of command line flags are available to tweak Ginkgo's default output. + +These are documented [here](http://onsi.github.io/ginkgo/#running_tests) +*/ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" +) + +type DefaultReporter struct { + config config.DefaultReporterConfigType + stenographer stenographer.Stenographer + specSummaries []*types.SpecSummary +} + +func NewDefaultReporter(config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *DefaultReporter { + return &DefaultReporter{ + config: config, + stenographer: stenographer, + } +} + +func (reporter *DefaultReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + reporter.stenographer.AnnounceSuite(summary.SuiteDescription, config.RandomSeed, config.RandomizeAllSpecs, reporter.config.Succinct) + if config.ParallelTotal > 1 { + reporter.stenographer.AnnounceParallelRun(config.ParallelNode, config.ParallelTotal, summary.NumberOfTotalSpecs, summary.NumberOfSpecsBeforeParallelization, reporter.config.Succinct) + } + reporter.stenographer.AnnounceNumberOfSpecs(summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, reporter.config.Succinct) +} + +func (reporter *DefaultReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + if setupSummary.State != types.SpecStatePassed { + reporter.stenographer.AnnounceBeforeSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) + } +} + +func (reporter *DefaultReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + if setupSummary.State != types.SpecStatePassed { + reporter.stenographer.AnnounceAfterSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) + } +} + +func (reporter *DefaultReporter) SpecWillRun(specSummary *types.SpecSummary) { + if reporter.config.Verbose && !reporter.config.Succinct && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { + reporter.stenographer.AnnounceSpecWillRun(specSummary) + } +} + +func (reporter *DefaultReporter) SpecDidComplete(specSummary *types.SpecSummary) { + switch specSummary.State { + case types.SpecStatePassed: + if specSummary.IsMeasurement { + reporter.stenographer.AnnounceSuccesfulMeasurement(specSummary, reporter.config.Succinct) + } else if specSummary.RunTime.Seconds() >= reporter.config.SlowSpecThreshold { + reporter.stenographer.AnnounceSuccesfulSlowSpec(specSummary, reporter.config.Succinct) + } else { + reporter.stenographer.AnnounceSuccesfulSpec(specSummary) + } + case types.SpecStatePending: + reporter.stenographer.AnnouncePendingSpec(specSummary, reporter.config.NoisyPendings && !reporter.config.Succinct) + case types.SpecStateSkipped: + reporter.stenographer.AnnounceSkippedSpec(specSummary) + case types.SpecStateTimedOut: + reporter.stenographer.AnnounceSpecTimedOut(specSummary, reporter.config.Succinct, reporter.config.FullTrace) + case types.SpecStatePanicked: + reporter.stenographer.AnnounceSpecPanicked(specSummary, reporter.config.Succinct, reporter.config.FullTrace) + case types.SpecStateFailed: + reporter.stenographer.AnnounceSpecFailed(specSummary, reporter.config.Succinct, reporter.config.FullTrace) + } + + reporter.specSummaries = append(reporter.specSummaries, specSummary) +} + +func (reporter *DefaultReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + reporter.stenographer.SummarizeFailures(reporter.specSummaries) + reporter.stenographer.AnnounceSpecRunCompletion(summary, reporter.config.Succinct) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter_test.go new file mode 100644 index 00000000000..3b719a1e055 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/default_reporter_test.go @@ -0,0 +1,397 @@ +package reporters_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + st "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("DefaultReporter", func() { + var ( + reporter *reporters.DefaultReporter + reporterConfig config.DefaultReporterConfigType + stenographer *st.FakeStenographer + + ginkgoConfig config.GinkgoConfigType + suite *types.SuiteSummary + spec *types.SpecSummary + ) + + BeforeEach(func() { + stenographer = st.NewFakeStenographer() + reporterConfig = config.DefaultReporterConfigType{ + NoColor: false, + SlowSpecThreshold: 0.1, + NoisyPendings: true, + Verbose: true, + FullTrace: true, + } + + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + }) + + call := func(method string, args ...interface{}) st.FakeStenographerCall { + return st.NewFakeStenographerCall(method, args...) + } + + Describe("SpecSuiteWillBegin", func() { + BeforeEach(func() { + suite = &types.SuiteSummary{ + SuiteDescription: "A Sweet Suite", + NumberOfTotalSpecs: 10, + NumberOfSpecsThatWillBeRun: 8, + } + + ginkgoConfig = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + } + }) + + Context("when a serial (non-parallel) suite begins", func() { + BeforeEach(func() { + ginkgoConfig.ParallelTotal = 1 + + reporter.SpecSuiteWillBegin(ginkgoConfig, suite) + }) + + It("should announce the suite, then announce the number of specs", func() { + Ω(stenographer.Calls()).Should(HaveLen(2)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true, false))) + Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceNumberOfSpecs", 8, 10, false))) + }) + }) + + Context("when a parallel suite begins", func() { + BeforeEach(func() { + ginkgoConfig.ParallelTotal = 2 + ginkgoConfig.ParallelNode = 1 + suite.NumberOfSpecsBeforeParallelization = 20 + + reporter.SpecSuiteWillBegin(ginkgoConfig, suite) + }) + + It("should announce the suite, announce that it's a parallel run, then announce the number of specs", func() { + Ω(stenographer.Calls()).Should(HaveLen(3)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true, false))) + Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceParallelRun", 1, 2, 10, 20, false))) + Ω(stenographer.Calls()[2]).Should(Equal(call("AnnounceNumberOfSpecs", 8, 10, false))) + }) + }) + }) + + Describe("BeforeSuiteDidRun", func() { + Context("when the BeforeSuite passes", func() { + It("should announce nothing", func() { + reporter.BeforeSuiteDidRun(&types.SetupSummary{ + State: types.SpecStatePassed, + }) + + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + + Context("when the BeforeSuite fails", func() { + It("should announce the failure", func() { + summary := &types.SetupSummary{ + State: types.SpecStateFailed, + } + reporter.BeforeSuiteDidRun(summary) + + Ω(stenographer.Calls()).Should(HaveLen(1)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceBeforeSuiteFailure", summary, false, true))) + }) + }) + }) + + Describe("AfterSuiteDidRun", func() { + Context("when the AfterSuite passes", func() { + It("should announce nothing", func() { + reporter.AfterSuiteDidRun(&types.SetupSummary{ + State: types.SpecStatePassed, + }) + + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + + Context("when the AfterSuite fails", func() { + It("should announce the failure", func() { + summary := &types.SetupSummary{ + State: types.SpecStateFailed, + } + reporter.AfterSuiteDidRun(summary) + + Ω(stenographer.Calls()).Should(HaveLen(1)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceAfterSuiteFailure", summary, false, true))) + }) + }) + }) + + Describe("SpecWillRun", func() { + Context("When running in verbose mode", func() { + Context("and the spec will run", func() { + BeforeEach(func() { + spec = &types.SpecSummary{} + reporter.SpecWillRun(spec) + }) + + It("should announce that the spec will run", func() { + Ω(stenographer.Calls()).Should(HaveLen(1)) + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecWillRun", spec))) + }) + }) + + Context("and the spec will not run", func() { + Context("because it is pending", func() { + BeforeEach(func() { + spec = &types.SpecSummary{ + State: types.SpecStatePending, + } + reporter.SpecWillRun(spec) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + + Context("because it is skipped", func() { + BeforeEach(func() { + spec = &types.SpecSummary{ + State: types.SpecStateSkipped, + } + reporter.SpecWillRun(spec) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + }) + }) + + Context("When running in verbose & succinct mode", func() { + BeforeEach(func() { + reporterConfig.Succinct = true + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + spec = &types.SpecSummary{} + reporter.SpecWillRun(spec) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + + Context("When not running in verbose mode", func() { + BeforeEach(func() { + reporterConfig.Verbose = false + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + spec = &types.SpecSummary{} + reporter.SpecWillRun(spec) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls()).Should(BeEmpty()) + }) + }) + }) + + Describe("SpecDidComplete", func() { + JustBeforeEach(func() { + reporter.SpecDidComplete(spec) + }) + + BeforeEach(func() { + spec = &types.SpecSummary{} + }) + + Context("When the spec passed", func() { + BeforeEach(func() { + spec.State = types.SpecStatePassed + }) + + Context("When the spec was a measurement", func() { + BeforeEach(func() { + spec.IsMeasurement = true + }) + + It("should announce the measurement", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulMeasurement", spec, false))) + }) + }) + + Context("When the spec is slow", func() { + BeforeEach(func() { + spec.RunTime = time.Second + }) + + It("should announce that it was slow", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulSlowSpec", spec, false))) + }) + }) + + Context("Otherwise", func() { + It("should announce the succesful spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulSpec", spec))) + }) + }) + }) + + Context("When the spec is pending", func() { + BeforeEach(func() { + spec.State = types.SpecStatePending + }) + + It("should announce the pending spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnouncePendingSpec", spec, true))) + }) + }) + + Context("When the spec is skipped", func() { + BeforeEach(func() { + spec.State = types.SpecStateSkipped + }) + + It("should announce the skipped spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSkippedSpec", spec))) + }) + }) + + Context("When the spec timed out", func() { + BeforeEach(func() { + spec.State = types.SpecStateTimedOut + }) + + It("should announce the timedout spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecTimedOut", spec, false, true))) + }) + }) + + Context("When the spec panicked", func() { + BeforeEach(func() { + spec.State = types.SpecStatePanicked + }) + + It("should announce the panicked spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecPanicked", spec, false, true))) + }) + }) + + Context("When the spec failed", func() { + BeforeEach(func() { + spec.State = types.SpecStateFailed + }) + + It("should announce the failed spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecFailed", spec, false, true))) + }) + }) + + Context("in succinct mode", func() { + BeforeEach(func() { + reporterConfig.Succinct = true + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + }) + + Context("When the spec passed", func() { + BeforeEach(func() { + spec.State = types.SpecStatePassed + }) + + Context("When the spec was a measurement", func() { + BeforeEach(func() { + spec.IsMeasurement = true + }) + + It("should announce the measurement", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulMeasurement", spec, true))) + }) + }) + + Context("When the spec is slow", func() { + BeforeEach(func() { + spec.RunTime = time.Second + }) + + It("should announce that it was slow", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulSlowSpec", spec, true))) + }) + }) + + Context("Otherwise", func() { + It("should announce the succesful spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccesfulSpec", spec))) + }) + }) + }) + + Context("When the spec is pending", func() { + BeforeEach(func() { + spec.State = types.SpecStatePending + }) + + It("should announce the pending spec, but never noisily", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnouncePendingSpec", spec, false))) + }) + }) + + Context("When the spec is skipped", func() { + BeforeEach(func() { + spec.State = types.SpecStateSkipped + }) + + It("should announce the skipped spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSkippedSpec", spec))) + }) + }) + + Context("When the spec timed out", func() { + BeforeEach(func() { + spec.State = types.SpecStateTimedOut + }) + + It("should announce the timedout spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecTimedOut", spec, true, true))) + }) + }) + + Context("When the spec panicked", func() { + BeforeEach(func() { + spec.State = types.SpecStatePanicked + }) + + It("should announce the panicked spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecPanicked", spec, true, true))) + }) + }) + + Context("When the spec failed", func() { + BeforeEach(func() { + spec.State = types.SpecStateFailed + }) + + It("should announce the failed spec", func() { + Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecFailed", spec, true, true))) + }) + }) + }) + }) + + Describe("SpecSuiteDidEnd", func() { + BeforeEach(func() { + suite = &types.SuiteSummary{} + reporter.SpecSuiteDidEnd(suite) + }) + + It("should announce the spec run's completion", func() { + Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceSpecRunCompletion", suite, false))) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/fake_reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/fake_reporter.go new file mode 100644 index 00000000000..27db4794908 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/fake_reporter.go @@ -0,0 +1,59 @@ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +//FakeReporter is useful for testing purposes +type FakeReporter struct { + Config config.GinkgoConfigType + + BeginSummary *types.SuiteSummary + BeforeSuiteSummary *types.SetupSummary + SpecWillRunSummaries []*types.SpecSummary + SpecSummaries []*types.SpecSummary + AfterSuiteSummary *types.SetupSummary + EndSummary *types.SuiteSummary + + SpecWillRunStub func(specSummary *types.SpecSummary) + SpecDidCompleteStub func(specSummary *types.SpecSummary) +} + +func NewFakeReporter() *FakeReporter { + return &FakeReporter{ + SpecWillRunSummaries: make([]*types.SpecSummary, 0), + SpecSummaries: make([]*types.SpecSummary, 0), + } +} + +func (fakeR *FakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + fakeR.Config = config + fakeR.BeginSummary = summary +} + +func (fakeR *FakeReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + fakeR.BeforeSuiteSummary = setupSummary +} + +func (fakeR *FakeReporter) SpecWillRun(specSummary *types.SpecSummary) { + if fakeR.SpecWillRunStub != nil { + fakeR.SpecWillRunStub(specSummary) + } + fakeR.SpecWillRunSummaries = append(fakeR.SpecWillRunSummaries, specSummary) +} + +func (fakeR *FakeReporter) SpecDidComplete(specSummary *types.SpecSummary) { + if fakeR.SpecDidCompleteStub != nil { + fakeR.SpecDidCompleteStub(specSummary) + } + fakeR.SpecSummaries = append(fakeR.SpecSummaries, specSummary) +} + +func (fakeR *FakeReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + fakeR.AfterSuiteSummary = setupSummary +} + +func (fakeR *FakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + fakeR.EndSummary = summary +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter.go new file mode 100644 index 00000000000..278a88ed735 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter.go @@ -0,0 +1,139 @@ +/* + +JUnit XML Reporter for Ginkgo + +For usage instructions: http://onsi.github.io/ginkgo/#generating_junit_xml_output + +*/ + +package reporters + +import ( + "encoding/xml" + "fmt" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" + "os" + "strings" +) + +type JUnitTestSuite struct { + XMLName xml.Name `xml:"testsuite"` + TestCases []JUnitTestCase `xml:"testcase"` + Tests int `xml:"tests,attr"` + Failures int `xml:"failures,attr"` + Time float64 `xml:"time,attr"` +} + +type JUnitTestCase struct { + Name string `xml:"name,attr"` + ClassName string `xml:"classname,attr"` + FailureMessage *JUnitFailureMessage `xml:"failure,omitempty"` + Skipped *JUnitSkipped `xml:"skipped,omitempty"` + Time float64 `xml:"time,attr"` +} + +type JUnitFailureMessage struct { + Type string `xml:"type,attr"` + Message string `xml:",chardata"` +} + +type JUnitSkipped struct { + XMLName xml.Name `xml:"skipped"` +} + +type JUnitReporter struct { + suite JUnitTestSuite + filename string + testSuiteName string +} + +//NewJUnitReporter creates a new JUnit XML reporter. The XML will be stored in the passed in filename. +func NewJUnitReporter(filename string) *JUnitReporter { + return &JUnitReporter{ + filename: filename, + } +} + +func (reporter *JUnitReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + reporter.suite = JUnitTestSuite{ + Tests: summary.NumberOfSpecsThatWillBeRun, + TestCases: []JUnitTestCase{}, + } + reporter.testSuiteName = summary.SuiteDescription +} + +func (reporter *JUnitReporter) SpecWillRun(specSummary *types.SpecSummary) { +} + +func (reporter *JUnitReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + reporter.handleSetupSummary("BeforeSuite", setupSummary) +} + +func (reporter *JUnitReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + reporter.handleSetupSummary("AfterSuite", setupSummary) +} + +func (reporter *JUnitReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) { + if setupSummary.State != types.SpecStatePassed { + testCase := JUnitTestCase{ + Name: name, + ClassName: reporter.testSuiteName, + } + + testCase.FailureMessage = &JUnitFailureMessage{ + Type: reporter.failureTypeForState(setupSummary.State), + Message: fmt.Sprintf("%s\n%s", setupSummary.Failure.ComponentCodeLocation.String(), setupSummary.Failure.Message), + } + testCase.Time = setupSummary.RunTime.Seconds() + reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) + } +} + +func (reporter *JUnitReporter) SpecDidComplete(specSummary *types.SpecSummary) { + testCase := JUnitTestCase{ + Name: strings.Join(specSummary.ComponentTexts[1:], " "), + ClassName: reporter.testSuiteName, + } + if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked { + testCase.FailureMessage = &JUnitFailureMessage{ + Type: reporter.failureTypeForState(specSummary.State), + Message: fmt.Sprintf("%s\n%s", specSummary.Failure.ComponentCodeLocation.String(), specSummary.Failure.Message), + } + } + if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending { + testCase.Skipped = &JUnitSkipped{} + } + testCase.Time = specSummary.RunTime.Seconds() + reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) +} + +func (reporter *JUnitReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + reporter.suite.Time = summary.RunTime.Seconds() + reporter.suite.Failures = summary.NumberOfFailedSpecs + file, err := os.Create(reporter.filename) + if err != nil { + fmt.Printf("Failed to create JUnit report file: %s\n\t%s", reporter.filename, err.Error()) + } + defer file.Close() + file.WriteString(xml.Header) + encoder := xml.NewEncoder(file) + encoder.Indent(" ", " ") + err = encoder.Encode(reporter.suite) + if err != nil { + fmt.Printf("Failed to generate JUnit report\n\t%s", err.Error()) + } +} + +func (reporter *JUnitReporter) failureTypeForState(state types.SpecState) string { + switch state { + case types.SpecStateFailed: + return "Failure" + case types.SpecStateTimedOut: + return "Timeout" + case types.SpecStatePanicked: + return "Panic" + default: + return "" + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter_test.go new file mode 100644 index 00000000000..744f383a05f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/junit_reporter_test.go @@ -0,0 +1,241 @@ +package reporters_test + +import ( + "encoding/xml" + "io/ioutil" + "os" + "time" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("JUnit Reporter", func() { + var ( + outputFile string + reporter Reporter + ) + + readOutputFile := func() reporters.JUnitTestSuite { + bytes, err := ioutil.ReadFile(outputFile) + Ω(err).ShouldNot(HaveOccurred()) + var suite reporters.JUnitTestSuite + err = xml.Unmarshal(bytes, &suite) + Ω(err).ShouldNot(HaveOccurred()) + return suite + } + + BeforeEach(func() { + f, err := ioutil.TempFile("", "output") + Ω(err).ShouldNot(HaveOccurred()) + f.Close() + outputFile = f.Name() + + reporter = reporters.NewJUnitReporter(outputFile) + + reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ + SuiteDescription: "My test suite", + NumberOfSpecsThatWillBeRun: 1, + }) + }) + + AfterEach(func() { + os.RemoveAll(outputFile) + }) + + Describe("a passing test", func() { + BeforeEach(func() { + beforeSuite := &types.SetupSummary{ + State: types.SpecStatePassed, + } + reporter.BeforeSuiteDidRun(beforeSuite) + + afterSuite := &types.SetupSummary{ + State: types.SpecStatePassed, + } + reporter.AfterSuiteDidRun(afterSuite) + + spec := &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: types.SpecStatePassed, + RunTime: 5 * time.Second, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 0, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as passing", func() { + output := readOutputFile() + Ω(output.Tests).Should(Equal(1)) + Ω(output.Failures).Should(Equal(0)) + Ω(output.Time).Should(Equal(10.0)) + Ω(output.TestCases).Should(HaveLen(1)) + Ω(output.TestCases[0].Name).Should(Equal("A B C")) + Ω(output.TestCases[0].ClassName).Should(Equal("My test suite")) + Ω(output.TestCases[0].FailureMessage).Should(BeNil()) + Ω(output.TestCases[0].Skipped).Should(BeNil()) + Ω(output.TestCases[0].Time).Should(Equal(5.0)) + }) + }) + + Describe("when the BeforeSuite fails", func() { + var beforeSuite *types.SetupSummary + + BeforeEach(func() { + beforeSuite = &types.SetupSummary{ + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.SpecFailure{ + Message: "failed to setup", + ComponentCodeLocation: codelocation.New(0), + }, + } + reporter.BeforeSuiteDidRun(beforeSuite) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as having failed", func() { + output := readOutputFile() + Ω(output.Tests).Should(Equal(1)) + Ω(output.Failures).Should(Equal(1)) + Ω(output.Time).Should(Equal(10.0)) + Ω(output.TestCases[0].Name).Should(Equal("BeforeSuite")) + Ω(output.TestCases[0].Time).Should(Equal(3.0)) + Ω(output.TestCases[0].ClassName).Should(Equal("My test suite")) + Ω(output.TestCases[0].FailureMessage.Type).Should(Equal("Failure")) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring("failed to setup")) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring(beforeSuite.Failure.ComponentCodeLocation.String())) + Ω(output.TestCases[0].Skipped).Should(BeNil()) + }) + }) + + Describe("when the AfterSuite fails", func() { + var afterSuite *types.SetupSummary + + BeforeEach(func() { + afterSuite = &types.SetupSummary{ + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.SpecFailure{ + Message: "failed to setup", + ComponentCodeLocation: codelocation.New(0), + }, + } + reporter.AfterSuiteDidRun(afterSuite) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as having failed", func() { + output := readOutputFile() + Ω(output.Tests).Should(Equal(1)) + Ω(output.Failures).Should(Equal(1)) + Ω(output.Time).Should(Equal(10.0)) + Ω(output.TestCases[0].Name).Should(Equal("AfterSuite")) + Ω(output.TestCases[0].Time).Should(Equal(3.0)) + Ω(output.TestCases[0].ClassName).Should(Equal("My test suite")) + Ω(output.TestCases[0].FailureMessage.Type).Should(Equal("Failure")) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring("failed to setup")) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring(afterSuite.Failure.ComponentCodeLocation.String())) + Ω(output.TestCases[0].Skipped).Should(BeNil()) + }) + }) + + specStateCases := []struct { + state types.SpecState + message string + }{ + {types.SpecStateFailed, "Failure"}, + {types.SpecStateTimedOut, "Timeout"}, + {types.SpecStatePanicked, "Panic"}, + } + + for _, specStateCase := range specStateCases { + specStateCase := specStateCase + Describe("a failing test", func() { + var spec *types.SpecSummary + BeforeEach(func() { + spec = &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: specStateCase.state, + RunTime: 5 * time.Second, + Failure: types.SpecFailure{ + ComponentCodeLocation: codelocation.New(0), + Message: "I failed", + }, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record test as failing", func() { + output := readOutputFile() + Ω(output.Tests).Should(Equal(1)) + Ω(output.Failures).Should(Equal(1)) + Ω(output.Time).Should(Equal(10.0)) + Ω(output.TestCases[0].Name).Should(Equal("A B C")) + Ω(output.TestCases[0].ClassName).Should(Equal("My test suite")) + Ω(output.TestCases[0].FailureMessage.Type).Should(Equal(specStateCase.message)) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring("I failed")) + Ω(output.TestCases[0].FailureMessage.Message).Should(ContainSubstring(spec.Failure.ComponentCodeLocation.String())) + Ω(output.TestCases[0].Skipped).Should(BeNil()) + }) + }) + } + + for _, specStateCase := range []types.SpecState{types.SpecStatePending, types.SpecStateSkipped} { + specStateCase := specStateCase + Describe("a skipped test", func() { + var spec *types.SpecSummary + BeforeEach(func() { + spec = &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: specStateCase, + RunTime: 5 * time.Second, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 0, + RunTime: 10 * time.Second, + }) + }) + + It("should record test as failing", func() { + output := readOutputFile() + Ω(output.Tests).Should(Equal(1)) + Ω(output.Failures).Should(Equal(0)) + Ω(output.Time).Should(Equal(10.0)) + Ω(output.TestCases[0].Name).Should(Equal("A B C")) + Ω(output.TestCases[0].Skipped).ShouldNot(BeNil()) + }) + }) + } +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporter.go new file mode 100644 index 00000000000..348b9dfce1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporter.go @@ -0,0 +1,15 @@ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +type Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) + BeforeSuiteDidRun(setupSummary *types.SetupSummary) + SpecWillRun(specSummary *types.SpecSummary) + SpecDidComplete(specSummary *types.SpecSummary) + AfterSuiteDidRun(setupSummary *types.SetupSummary) + SpecSuiteDidEnd(summary *types.SuiteSummary) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporters_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporters_suite_test.go new file mode 100644 index 00000000000..cec5a4dbfb0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/reporters_suite_test.go @@ -0,0 +1,13 @@ +package reporters_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestReporters(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Reporters Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/console_logging.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/console_logging.go new file mode 100644 index 00000000000..ce5433af6a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/console_logging.go @@ -0,0 +1,64 @@ +package stenographer + +import ( + "fmt" + "strings" +) + +func (s *consoleStenographer) colorize(colorCode string, format string, args ...interface{}) string { + var out string + + if len(args) > 0 { + out = fmt.Sprintf(format, args...) + } else { + out = format + } + + if s.color { + return fmt.Sprintf("%s%s%s", colorCode, out, defaultStyle) + } else { + return out + } +} + +func (s *consoleStenographer) printBanner(text string, bannerCharacter string) { + fmt.Println(text) + fmt.Println(strings.Repeat(bannerCharacter, len(text))) +} + +func (s *consoleStenographer) printNewLine() { + fmt.Println("") +} + +func (s *consoleStenographer) printDelimiter() { + fmt.Println(s.colorize(grayColor, "%s", strings.Repeat("-", 30))) +} + +func (s *consoleStenographer) print(indentation int, format string, args ...interface{}) { + fmt.Print(s.indent(indentation, format, args...)) +} + +func (s *consoleStenographer) println(indentation int, format string, args ...interface{}) { + fmt.Println(s.indent(indentation, format, args...)) +} + +func (s *consoleStenographer) indent(indentation int, format string, args ...interface{}) string { + var text string + + if len(args) > 0 { + text = fmt.Sprintf(format, args...) + } else { + text = format + } + + stringArray := strings.Split(text, "\n") + padding := "" + if indentation >= 0 { + padding = strings.Repeat(" ", indentation) + } + for i, s := range stringArray { + stringArray[i] = fmt.Sprintf("%s%s", padding, s) + } + + return strings.Join(stringArray, "\n") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/fake_stenographer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/fake_stenographer.go new file mode 100644 index 00000000000..3a1e0c2d71c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/fake_stenographer.go @@ -0,0 +1,138 @@ +package stenographer + +import ( + "sync" + + "github.com/onsi/ginkgo/types" +) + +func NewFakeStenographerCall(method string, args ...interface{}) FakeStenographerCall { + return FakeStenographerCall{ + Method: method, + Args: args, + } +} + +type FakeStenographer struct { + calls []FakeStenographerCall + lock *sync.Mutex +} + +type FakeStenographerCall struct { + Method string + Args []interface{} +} + +func NewFakeStenographer() *FakeStenographer { + stenographer := &FakeStenographer{ + lock: &sync.Mutex{}, + } + stenographer.Reset() + return stenographer +} + +func (stenographer *FakeStenographer) Calls() []FakeStenographerCall { + stenographer.lock.Lock() + defer stenographer.lock.Unlock() + + return stenographer.calls +} + +func (stenographer *FakeStenographer) Reset() { + stenographer.lock.Lock() + defer stenographer.lock.Unlock() + + stenographer.calls = make([]FakeStenographerCall, 0) +} + +func (stenographer *FakeStenographer) CallsTo(method string) []FakeStenographerCall { + stenographer.lock.Lock() + defer stenographer.lock.Unlock() + + results := make([]FakeStenographerCall, 0) + for _, call := range stenographer.calls { + if call.Method == method { + results = append(results, call) + } + } + + return results +} + +func (stenographer *FakeStenographer) registerCall(method string, args ...interface{}) { + stenographer.lock.Lock() + defer stenographer.lock.Unlock() + + stenographer.calls = append(stenographer.calls, NewFakeStenographerCall(method, args...)) +} + +func (stenographer *FakeStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { + stenographer.registerCall("AnnounceSuite", description, randomSeed, randomizingAll, succinct) +} + +func (stenographer *FakeStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { + stenographer.registerCall("AnnounceAggregatedParallelRun", nodes, succinct) +} + +func (stenographer *FakeStenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int, succinct bool) { + stenographer.registerCall("AnnounceParallelRun", node, nodes, specsToRun, totalSpecs, succinct) +} + +func (stenographer *FakeStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { + stenographer.registerCall("AnnounceNumberOfSpecs", specsToRun, total, succinct) +} + +func (stenographer *FakeStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { + stenographer.registerCall("AnnounceSpecRunCompletion", summary, succinct) +} + +func (stenographer *FakeStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { + stenographer.registerCall("AnnounceSpecWillRun", spec) +} + +func (stenographer *FakeStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { + stenographer.registerCall("AnnounceBeforeSuiteFailure", summary, succinct, fullTrace) +} + +func (stenographer *FakeStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { + stenographer.registerCall("AnnounceAfterSuiteFailure", summary, succinct, fullTrace) +} +func (stenographer *FakeStenographer) AnnounceCapturedOutput(output string) { + stenographer.registerCall("AnnounceCapturedOutput", output) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulSpec(spec *types.SpecSummary) { + stenographer.registerCall("AnnounceSuccesfulSpec", spec) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulSlowSpec(spec *types.SpecSummary, succinct bool) { + stenographer.registerCall("AnnounceSuccesfulSlowSpec", spec, succinct) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulMeasurement(spec *types.SpecSummary, succinct bool) { + stenographer.registerCall("AnnounceSuccesfulMeasurement", spec, succinct) +} + +func (stenographer *FakeStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { + stenographer.registerCall("AnnouncePendingSpec", spec, noisy) +} + +func (stenographer *FakeStenographer) AnnounceSkippedSpec(spec *types.SpecSummary) { + stenographer.registerCall("AnnounceSkippedSpec", spec) +} + +func (stenographer *FakeStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { + stenographer.registerCall("AnnounceSpecTimedOut", spec, succinct, fullTrace) +} + +func (stenographer *FakeStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { + stenographer.registerCall("AnnounceSpecPanicked", spec, succinct, fullTrace) +} + +func (stenographer *FakeStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { + stenographer.registerCall("AnnounceSpecFailed", spec, succinct, fullTrace) +} + +func (stenographer *FakeStenographer) SummarizeFailures(summaries []*types.SpecSummary) { + stenographer.registerCall("SummarizeFailures", summaries) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/stenographer.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/stenographer.go new file mode 100644 index 00000000000..d56f193864d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/stenographer/stenographer.go @@ -0,0 +1,520 @@ +/* +The stenographer is used by Ginkgo's reporters to generate output. + +Move along, nothing to see here. +*/ + +package stenographer + +import ( + "fmt" + "strings" + + "github.com/onsi/ginkgo/types" +) + +const defaultStyle = "\x1b[0m" +const boldStyle = "\x1b[1m" +const redColor = "\x1b[91m" +const greenColor = "\x1b[32m" +const yellowColor = "\x1b[33m" +const cyanColor = "\x1b[36m" +const grayColor = "\x1b[90m" +const lightGrayColor = "\x1b[37m" + +type cursorStateType int + +const ( + cursorStateTop cursorStateType = iota + cursorStateStreaming + cursorStateMidBlock + cursorStateEndBlock +) + +type Stenographer interface { + AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) + AnnounceAggregatedParallelRun(nodes int, succinct bool) + AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int, succinct bool) + AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) + AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) + + AnnounceSpecWillRun(spec *types.SpecSummary) + AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) + AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) + + AnnounceCapturedOutput(output string) + + AnnounceSuccesfulSpec(spec *types.SpecSummary) + AnnounceSuccesfulSlowSpec(spec *types.SpecSummary, succinct bool) + AnnounceSuccesfulMeasurement(spec *types.SpecSummary, succinct bool) + + AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) + AnnounceSkippedSpec(spec *types.SpecSummary) + + AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) + AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) + AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) + + SummarizeFailures(summaries []*types.SpecSummary) +} + +func New(color bool) Stenographer { + return &consoleStenographer{ + color: color, + cursorState: cursorStateTop, + } +} + +type consoleStenographer struct { + color bool + cursorState cursorStateType +} + +var alternatingColors = []string{defaultStyle, grayColor} + +func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { + if succinct { + s.print(0, "[%d] %s ", randomSeed, s.colorize(boldStyle, description)) + return + } + s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=") + s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed)) + if randomizingAll { + s.print(0, " - Will randomize all specs") + } + s.printNewLine() +} + +func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int, succinct bool) { + if succinct { + s.print(0, "- node #%d ", node) + return + } + s.println(0, + "Parallel test node %s/%s. Assigned %s of %s specs.", + s.colorize(boldStyle, "%d", node), + s.colorize(boldStyle, "%d", nodes), + s.colorize(boldStyle, "%d", specsToRun), + s.colorize(boldStyle, "%d", totalSpecs), + ) + s.printNewLine() +} + +func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { + if succinct { + s.print(0, "- %d nodes ", nodes) + return + } + s.println(0, + "Running in parallel across %s nodes", + s.colorize(boldStyle, "%d", nodes), + ) + s.printNewLine() +} + +func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { + if succinct { + s.print(0, "- %d/%d specs ", specsToRun, total) + s.stream() + return + } + s.println(0, + "Will run %s of %s specs", + s.colorize(boldStyle, "%d", specsToRun), + s.colorize(boldStyle, "%d", total), + ) + + s.printNewLine() +} + +func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { + if succinct && summary.SuiteSucceeded { + s.print(0, " %s %s ", s.colorize(greenColor, "SUCCESS!"), summary.RunTime) + return + } + s.printNewLine() + color := greenColor + if !summary.SuiteSucceeded { + color = redColor + } + s.println(0, s.colorize(boldStyle+color, "Ran %d of %d Specs in %.3f seconds", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, summary.RunTime.Seconds())) + + status := "" + if summary.SuiteSucceeded { + status = s.colorize(boldStyle+greenColor, "SUCCESS!") + } else { + status = s.colorize(boldStyle+redColor, "FAIL!") + } + + s.print(0, + "%s -- %s | %s | %s | %s ", + status, + s.colorize(greenColor+boldStyle, "%d Passed", summary.NumberOfPassedSpecs), + s.colorize(redColor+boldStyle, "%d Failed", summary.NumberOfFailedSpecs), + s.colorize(yellowColor+boldStyle, "%d Pending", summary.NumberOfPendingSpecs), + s.colorize(cyanColor+boldStyle, "%d Skipped", summary.NumberOfSkippedSpecs), + ) +} + +func (s *consoleStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { + s.startBlock() + for i, text := range spec.ComponentTexts[1 : len(spec.ComponentTexts)-1] { + s.print(0, s.colorize(alternatingColors[i%2], text)+" ") + } + + indentation := 0 + if len(spec.ComponentTexts) > 2 { + indentation = 1 + s.printNewLine() + } + index := len(spec.ComponentTexts) - 1 + s.print(indentation, s.colorize(boldStyle, spec.ComponentTexts[index])) + s.printNewLine() + s.print(indentation, s.colorize(lightGrayColor, spec.ComponentCodeLocations[index].String())) + s.printNewLine() + s.midBlock() +} + +func (s *consoleStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { + s.announceSetupFailure("BeforeSuite", summary, succinct, fullTrace) +} + +func (s *consoleStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { + s.announceSetupFailure("AfterSuite", summary, succinct, fullTrace) +} + +func (s *consoleStenographer) announceSetupFailure(name string, summary *types.SetupSummary, succinct bool, fullTrace bool) { + s.startBlock() + var message string + switch summary.State { + case types.SpecStateFailed: + message = "Failure" + case types.SpecStatePanicked: + message = "Panic" + case types.SpecStateTimedOut: + message = "Timeout" + } + + s.println(0, s.colorize(redColor+boldStyle, "%s [%.3f seconds]", message, summary.RunTime.Seconds())) + + indentation := s.printCodeLocationBlock([]string{name}, []types.CodeLocation{summary.CodeLocation}, summary.ComponentType, 0, true, true) + + s.printNewLine() + s.printFailure(indentation, summary.State, summary.Failure, fullTrace) + + s.endBlock() +} + +func (s *consoleStenographer) AnnounceCapturedOutput(output string) { + if output == "" { + return + } + + s.startBlock() + s.println(0, output) + s.midBlock() +} + +func (s *consoleStenographer) AnnounceSuccesfulSpec(spec *types.SpecSummary) { + s.print(0, s.colorize(greenColor, "•")) + s.stream() +} + +func (s *consoleStenographer) AnnounceSuccesfulSlowSpec(spec *types.SpecSummary, succinct bool) { + s.printBlockWithMessage( + s.colorize(greenColor, "• [SLOW TEST:%.3f seconds]", spec.RunTime.Seconds()), + "", + spec, + succinct, + ) +} + +func (s *consoleStenographer) AnnounceSuccesfulMeasurement(spec *types.SpecSummary, succinct bool) { + s.printBlockWithMessage( + s.colorize(greenColor, "• [MEASUREMENT]"), + s.measurementReport(spec, succinct), + spec, + succinct, + ) +} + +func (s *consoleStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { + if noisy { + s.printBlockWithMessage( + s.colorize(yellowColor, "P [PENDING]"), + "", + spec, + false, + ) + } else { + s.print(0, s.colorize(yellowColor, "P")) + s.stream() + } +} + +func (s *consoleStenographer) AnnounceSkippedSpec(spec *types.SpecSummary) { + s.print(0, s.colorize(cyanColor, "S")) + s.stream() +} + +func (s *consoleStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { + s.printSpecFailure("•... Timeout", spec, succinct, fullTrace) +} + +func (s *consoleStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { + s.printSpecFailure("•! Panic", spec, succinct, fullTrace) +} + +func (s *consoleStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { + s.printSpecFailure("• Failure", spec, succinct, fullTrace) +} + +func (s *consoleStenographer) SummarizeFailures(summaries []*types.SpecSummary) { + failingSpecs := []*types.SpecSummary{} + + for _, summary := range summaries { + if summary.HasFailureState() { + failingSpecs = append(failingSpecs, summary) + } + } + + if len(failingSpecs) == 0 { + return + } + + s.printNewLine() + s.printNewLine() + plural := "s" + if len(failingSpecs) == 1 { + plural = "" + } + s.println(0, s.colorize(redColor+boldStyle, "Summarizing %d Failure%s:", len(failingSpecs), plural)) + for _, summary := range failingSpecs { + s.printNewLine() + if summary.HasFailureState() { + if summary.TimedOut() { + s.print(0, s.colorize(redColor+boldStyle, "[Timeout...] ")) + } else if summary.Panicked() { + s.print(0, s.colorize(redColor+boldStyle, "[Panic!] ")) + } else if summary.Failed() { + s.print(0, s.colorize(redColor+boldStyle, "[Fail] ")) + } + s.printSpecContext(summary.ComponentTexts, summary.ComponentCodeLocations, summary.Failure.ComponentType, summary.Failure.ComponentIndex, true, true) + s.printNewLine() + s.println(0, s.colorize(lightGrayColor, summary.Failure.Location.String())) + } + } +} + +func (s *consoleStenographer) startBlock() { + if s.cursorState == cursorStateStreaming { + s.printNewLine() + s.printDelimiter() + } else if s.cursorState == cursorStateMidBlock { + s.printNewLine() + } +} + +func (s *consoleStenographer) midBlock() { + s.cursorState = cursorStateMidBlock +} + +func (s *consoleStenographer) endBlock() { + s.printDelimiter() + s.cursorState = cursorStateEndBlock +} + +func (s *consoleStenographer) stream() { + s.cursorState = cursorStateStreaming +} + +func (s *consoleStenographer) printBlockWithMessage(header string, message string, spec *types.SpecSummary, succinct bool) { + s.startBlock() + s.println(0, header) + + indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, types.SpecComponentTypeInvalid, 0, false, succinct) + + if message != "" { + s.printNewLine() + s.println(indentation, message) + } + + s.endBlock() +} + +func (s *consoleStenographer) printSpecFailure(message string, spec *types.SpecSummary, succinct bool, fullTrace bool) { + s.startBlock() + s.println(0, s.colorize(redColor+boldStyle, "%s%s [%.3f seconds]", message, s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) + + indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, true, succinct) + + s.printNewLine() + s.printFailure(indentation, spec.State, spec.Failure, fullTrace) + s.endBlock() +} + +func (s *consoleStenographer) failureContext(failedComponentType types.SpecComponentType) string { + switch failedComponentType { + case types.SpecComponentTypeBeforeSuite: + return " in Suite Setup (BeforeSuite)" + case types.SpecComponentTypeAfterSuite: + return " in Suite Teardown (AfterSuite)" + case types.SpecComponentTypeBeforeEach: + return " in Spec Setup (BeforeEach)" + case types.SpecComponentTypeJustBeforeEach: + return " in Spec Setup (JustBeforeEach)" + case types.SpecComponentTypeAfterEach: + return " in Spec Teardown (AfterEach)" + } + + return "" +} + +func (s *consoleStenographer) printFailure(indentation int, state types.SpecState, failure types.SpecFailure, fullTrace bool) { + if state == types.SpecStatePanicked { + s.println(indentation, s.colorize(redColor+boldStyle, failure.Message)) + s.println(indentation, s.colorize(redColor, "%v", failure.ForwardedPanic)) + s.println(indentation, failure.Location.String()) + s.printNewLine() + s.println(indentation, s.colorize(redColor, "Full Stack Trace")) + s.println(indentation, failure.Location.FullStackTrace) + } else { + s.println(indentation, s.colorize(redColor, failure.Message)) + s.printNewLine() + s.println(indentation, failure.Location.String()) + if fullTrace { + s.printNewLine() + s.println(indentation, s.colorize(redColor, "Full Stack Trace")) + s.println(indentation, failure.Location.FullStackTrace) + } + } +} + +func (s *consoleStenographer) printSpecContext(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, failure bool, succinct bool) int { + startIndex := 1 + indentation := 0 + + if len(componentTexts) == 1 { + startIndex = 0 + } + + for i := startIndex; i < len(componentTexts); i++ { + if failure && i == failedComponentIndex { + blockType := "" + switch failedComponentType { + case types.SpecComponentTypeBeforeSuite: + blockType = "BeforeSuite" + case types.SpecComponentTypeAfterSuite: + blockType = "AfterSuite" + case types.SpecComponentTypeBeforeEach: + blockType = "BeforeEach" + case types.SpecComponentTypeJustBeforeEach: + blockType = "JustBeforeEach" + case types.SpecComponentTypeAfterEach: + blockType = "AfterEach" + case types.SpecComponentTypeIt: + blockType = "It" + case types.SpecComponentTypeMeasure: + blockType = "Measurement" + } + if succinct { + s.print(0, s.colorize(redColor+boldStyle, "[%s] %s ", blockType, componentTexts[i])) + } else { + s.println(indentation, s.colorize(redColor+boldStyle, "%s [%s]", componentTexts[i], blockType)) + s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) + } + } else { + if succinct { + s.print(0, s.colorize(alternatingColors[i%2], "%s ", componentTexts[i])) + } else { + s.println(indentation, componentTexts[i]) + s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) + } + } + indentation++ + } + + return indentation +} + +func (s *consoleStenographer) printCodeLocationBlock(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, failure bool, succinct bool) int { + indentation := s.printSpecContext(componentTexts, componentCodeLocations, failedComponentType, failedComponentIndex, failure, succinct) + + if succinct { + if len(componentTexts) > 0 { + s.printNewLine() + s.print(0, s.colorize(lightGrayColor, "%s", componentCodeLocations[len(componentCodeLocations)-1])) + } + s.printNewLine() + indentation = 1 + } else { + indentation-- + } + + return indentation +} + +func (s *consoleStenographer) orderedMeasurementKeys(measurements map[string]*types.SpecMeasurement) []string { + orderedKeys := make([]string, len(measurements)) + for key, measurement := range measurements { + orderedKeys[measurement.Order] = key + } + return orderedKeys +} + +func (s *consoleStenographer) measurementReport(spec *types.SpecSummary, succinct bool) string { + if len(spec.Measurements) == 0 { + return "Found no measurements" + } + + message := []string{} + orderedKeys := s.orderedMeasurementKeys(spec.Measurements) + + if succinct { + message = append(message, fmt.Sprintf("%s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) + for _, key := range orderedKeys { + measurement := spec.Measurements[key] + message = append(message, fmt.Sprintf(" %s - %s: %s%s, %s: %s%s ± %s%s, %s: %s%s", + s.colorize(boldStyle, "%s", measurement.Name), + measurement.SmallestLabel, + s.colorize(greenColor, "%.3f", measurement.Smallest), + measurement.Units, + measurement.AverageLabel, + s.colorize(cyanColor, "%.3f", measurement.Average), + measurement.Units, + s.colorize(cyanColor, "%.3f", measurement.StdDeviation), + measurement.Units, + measurement.LargestLabel, + s.colorize(redColor, "%.3f", measurement.Largest), + measurement.Units, + )) + } + } else { + message = append(message, fmt.Sprintf("Ran %s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) + for _, key := range orderedKeys { + measurement := spec.Measurements[key] + info := "" + if measurement.Info != nil { + message = append(message, fmt.Sprintf("%v", measurement.Info)) + } + + message = append(message, fmt.Sprintf("%s:\n%s %s: %s%s\n %s: %s%s\n %s: %s%s ± %s%s", + s.colorize(boldStyle, "%s", measurement.Name), + info, + measurement.SmallestLabel, + s.colorize(greenColor, "%.3f", measurement.Smallest), + measurement.Units, + measurement.LargestLabel, + s.colorize(redColor, "%.3f", measurement.Largest), + measurement.Units, + measurement.AverageLabel, + s.colorize(cyanColor, "%.3f", measurement.Average), + measurement.Units, + s.colorize(cyanColor, "%.3f", measurement.StdDeviation), + measurement.Units, + )) + } + } + + return strings.Join(message, "\n") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter.go new file mode 100644 index 00000000000..657dfe726e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter.go @@ -0,0 +1,92 @@ +/* + +TeamCity Reporter for Ginkgo + +Makes use of TeamCity's support for Service Messages +http://confluence.jetbrains.com/display/TCD7/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingTests +*/ + +package reporters + +import ( + "fmt" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" + "io" + "strings" +) + +const ( + messageId = "##teamcity" +) + +type TeamCityReporter struct { + writer io.Writer + testSuiteName string +} + +func NewTeamCityReporter(writer io.Writer) *TeamCityReporter { + return &TeamCityReporter{ + writer: writer, + } +} + +func (reporter *TeamCityReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + reporter.testSuiteName = escape(summary.SuiteDescription) + fmt.Fprintf(reporter.writer, "%s[testSuiteStarted name='%s']", messageId, reporter.testSuiteName) +} + +func (reporter *TeamCityReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { + reporter.handleSetupSummary("BeforeSuite", setupSummary) +} + +func (reporter *TeamCityReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { + reporter.handleSetupSummary("AfterSuite", setupSummary) +} + +func (reporter *TeamCityReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) { + if setupSummary.State != types.SpecStatePassed { + testName := escape(name) + fmt.Fprintf(reporter.writer, "%s[testStarted name='%s']", messageId, testName) + message := escape(setupSummary.Failure.ComponentCodeLocation.String()) + details := escape(setupSummary.Failure.Message) + fmt.Fprintf(reporter.writer, "%s[testFailed name='%s' message='%s' details='%s']", messageId, testName, message, details) + durationInMilliseconds := setupSummary.RunTime.Seconds() * 1000 + fmt.Fprintf(reporter.writer, "%s[testFinished name='%s' duration='%v']", messageId, testName, durationInMilliseconds) + } +} + +func (reporter *TeamCityReporter) SpecWillRun(specSummary *types.SpecSummary) { + testName := escape(strings.Join(specSummary.ComponentTexts[1:], " ")) + fmt.Fprintf(reporter.writer, "%s[testStarted name='%s']", messageId, testName) +} + +func (reporter *TeamCityReporter) SpecDidComplete(specSummary *types.SpecSummary) { + testName := escape(strings.Join(specSummary.ComponentTexts[1:], " ")) + + if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked { + message := escape(specSummary.Failure.ComponentCodeLocation.String()) + details := escape(specSummary.Failure.Message) + fmt.Fprintf(reporter.writer, "%s[testFailed name='%s' message='%s' details='%s']", messageId, testName, message, details) + } + if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending { + fmt.Fprintf(reporter.writer, "%s[testIgnored name='%s']", messageId, testName) + } + + durationInMilliseconds := specSummary.RunTime.Seconds() * 1000 + fmt.Fprintf(reporter.writer, "%s[testFinished name='%s' duration='%v']", messageId, testName, durationInMilliseconds) +} + +func (reporter *TeamCityReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + fmt.Fprintf(reporter.writer, "%s[testSuiteFinished name='%s']", messageId, reporter.testSuiteName) +} + +func escape(output string) string { + output = strings.Replace(output, "|", "||", -1) + output = strings.Replace(output, "'", "|'", -1) + output = strings.Replace(output, "\n", "|n", -1) + output = strings.Replace(output, "\r", "|r", -1) + output = strings.Replace(output, "[", "|[", -1) + output = strings.Replace(output, "]", "|]", -1) + return output +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter_test.go new file mode 100644 index 00000000000..de87732113c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/reporters/teamcity_reporter_test.go @@ -0,0 +1,213 @@ +package reporters_test + +import ( + "bytes" + "fmt" + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "time" +) + +var _ = Describe("TeamCity Reporter", func() { + var ( + buffer bytes.Buffer + reporter Reporter + ) + + BeforeEach(func() { + buffer.Truncate(0) + reporter = reporters.NewTeamCityReporter(&buffer) + reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ + SuiteDescription: "Foo's test suite", + NumberOfSpecsThatWillBeRun: 1, + }) + }) + + Describe("a passing test", func() { + BeforeEach(func() { + beforeSuite := &types.SetupSummary{ + State: types.SpecStatePassed, + } + reporter.BeforeSuiteDidRun(beforeSuite) + + afterSuite := &types.SetupSummary{ + State: types.SpecStatePassed, + } + reporter.AfterSuiteDidRun(afterSuite) + + spec := &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: types.SpecStatePassed, + RunTime: 5 * time.Second, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 0, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as passing", func() { + actual := buffer.String() + expected := + "##teamcity[testSuiteStarted name='Foo|'s test suite']" + + "##teamcity[testStarted name='A B C']" + + "##teamcity[testFinished name='A B C' duration='5000']" + + "##teamcity[testSuiteFinished name='Foo|'s test suite']" + Ω(actual).Should(Equal(expected)) + }) + }) + + Describe("when the BeforeSuite fails", func() { + var beforeSuite *types.SetupSummary + + BeforeEach(func() { + beforeSuite = &types.SetupSummary{ + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.SpecFailure{ + Message: "failed to setup\n", + ComponentCodeLocation: codelocation.New(0), + }, + } + reporter.BeforeSuiteDidRun(beforeSuite) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as having failed", func() { + actual := buffer.String() + expected := fmt.Sprintf( + "##teamcity[testSuiteStarted name='Foo|'s test suite']"+ + "##teamcity[testStarted name='BeforeSuite']"+ + "##teamcity[testFailed name='BeforeSuite' message='%s' details='failed to setup|n']"+ + "##teamcity[testFinished name='BeforeSuite' duration='3000']"+ + "##teamcity[testSuiteFinished name='Foo|'s test suite']", beforeSuite.Failure.ComponentCodeLocation.String(), + ) + Ω(actual).Should(Equal(expected)) + }) + }) + + Describe("when the AfterSuite fails", func() { + var afterSuite *types.SetupSummary + + BeforeEach(func() { + afterSuite = &types.SetupSummary{ + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.SpecFailure{ + Message: "failed to setup\n", + ComponentCodeLocation: codelocation.New(0), + }, + } + reporter.AfterSuiteDidRun(afterSuite) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record the test as having failed", func() { + actual := buffer.String() + expected := fmt.Sprintf( + "##teamcity[testSuiteStarted name='Foo|'s test suite']"+ + "##teamcity[testStarted name='AfterSuite']"+ + "##teamcity[testFailed name='AfterSuite' message='%s' details='failed to setup|n']"+ + "##teamcity[testFinished name='AfterSuite' duration='3000']"+ + "##teamcity[testSuiteFinished name='Foo|'s test suite']", afterSuite.Failure.ComponentCodeLocation.String(), + ) + Ω(actual).Should(Equal(expected)) + }) + }) + specStateCases := []struct { + state types.SpecState + message string + }{ + {types.SpecStateFailed, "Failure"}, + {types.SpecStateTimedOut, "Timeout"}, + {types.SpecStatePanicked, "Panic"}, + } + + for _, specStateCase := range specStateCases { + specStateCase := specStateCase + Describe("a failing test", func() { + var spec *types.SpecSummary + BeforeEach(func() { + spec = &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: specStateCase.state, + RunTime: 5 * time.Second, + Failure: types.SpecFailure{ + ComponentCodeLocation: codelocation.New(0), + Message: "I failed", + }, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 1, + RunTime: 10 * time.Second, + }) + }) + + It("should record test as failing", func() { + actual := buffer.String() + expected := + fmt.Sprintf("##teamcity[testSuiteStarted name='Foo|'s test suite']"+ + "##teamcity[testStarted name='A B C']"+ + "##teamcity[testFailed name='A B C' message='%s' details='I failed']"+ + "##teamcity[testFinished name='A B C' duration='5000']"+ + "##teamcity[testSuiteFinished name='Foo|'s test suite']", spec.Failure.ComponentCodeLocation.String()) + Ω(actual).Should(Equal(expected)) + }) + }) + } + + for _, specStateCase := range []types.SpecState{types.SpecStatePending, types.SpecStateSkipped} { + specStateCase := specStateCase + Describe("a skipped test", func() { + var spec *types.SpecSummary + BeforeEach(func() { + spec = &types.SpecSummary{ + ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, + State: specStateCase, + RunTime: 5 * time.Second, + } + reporter.SpecWillRun(spec) + reporter.SpecDidComplete(spec) + + reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + NumberOfSpecsThatWillBeRun: 1, + NumberOfFailedSpecs: 0, + RunTime: 10 * time.Second, + }) + }) + + It("should record test as ignored", func() { + actual := buffer.String() + expected := + "##teamcity[testSuiteStarted name='Foo|'s test suite']" + + "##teamcity[testStarted name='A B C']" + + "##teamcity[testIgnored name='A B C']" + + "##teamcity[testFinished name='A B C' duration='5000']" + + "##teamcity[testSuiteFinished name='Foo|'s test suite']" + Ω(actual).Should(Equal(expected)) + }) + }) + } +}) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/types/code_location.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/code_location.go new file mode 100644 index 00000000000..935a89e136a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/code_location.go @@ -0,0 +1,15 @@ +package types + +import ( + "fmt" +) + +type CodeLocation struct { + FileName string + LineNumber int + FullStackTrace string +} + +func (codeLocation CodeLocation) String() string { + return fmt.Sprintf("%s:%d", codeLocation.FileName, codeLocation.LineNumber) +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/types/synchronization.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/synchronization.go new file mode 100644 index 00000000000..fdd6ed5bdf8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/synchronization.go @@ -0,0 +1,30 @@ +package types + +import ( + "encoding/json" +) + +type RemoteBeforeSuiteState int + +const ( + RemoteBeforeSuiteStateInvalid RemoteBeforeSuiteState = iota + + RemoteBeforeSuiteStatePending + RemoteBeforeSuiteStatePassed + RemoteBeforeSuiteStateFailed + RemoteBeforeSuiteStateDisappeared +) + +type RemoteBeforeSuiteData struct { + Data []byte + State RemoteBeforeSuiteState +} + +func (r RemoteBeforeSuiteData) ToJSON() []byte { + data, _ := json.Marshal(r) + return data +} + +type RemoteAfterSuiteData struct { + CanRun bool +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types.go new file mode 100644 index 00000000000..4a3b2138e6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types.go @@ -0,0 +1,141 @@ +package types + +import ( + "time" +) + +const GINKGO_FOCUS_EXIT_CODE = 197 + +type SuiteSummary struct { + SuiteDescription string + SuiteSucceeded bool + SuiteID string + + NumberOfSpecsBeforeParallelization int + NumberOfTotalSpecs int + NumberOfSpecsThatWillBeRun int + NumberOfPendingSpecs int + NumberOfSkippedSpecs int + NumberOfPassedSpecs int + NumberOfFailedSpecs int + RunTime time.Duration +} + +type SpecSummary struct { + ComponentTexts []string + ComponentCodeLocations []CodeLocation + + State SpecState + RunTime time.Duration + Failure SpecFailure + IsMeasurement bool + NumberOfSamples int + Measurements map[string]*SpecMeasurement + + CapturedOutput string + SuiteID string +} + +func (s SpecSummary) HasFailureState() bool { + return s.State == SpecStateTimedOut || s.State == SpecStatePanicked || s.State == SpecStateFailed +} + +func (s SpecSummary) TimedOut() bool { + return s.State == SpecStateTimedOut +} + +func (s SpecSummary) Panicked() bool { + return s.State == SpecStatePanicked +} + +func (s SpecSummary) Failed() bool { + return s.State == SpecStateFailed +} + +func (s SpecSummary) Passed() bool { + return s.State == SpecStatePassed +} + +func (s SpecSummary) Skipped() bool { + return s.State == SpecStateSkipped +} + +func (s SpecSummary) Pending() bool { + return s.State == SpecStatePending +} + +type SetupSummary struct { + ComponentType SpecComponentType + CodeLocation CodeLocation + + State SpecState + RunTime time.Duration + Failure SpecFailure + + CapturedOutput string + SuiteID string +} + +type SpecFailure struct { + Message string + Location CodeLocation + ForwardedPanic interface{} + + ComponentIndex int + ComponentType SpecComponentType + ComponentCodeLocation CodeLocation +} + +type SpecMeasurement struct { + Name string + Info interface{} + Order int + + Results []float64 + + Smallest float64 + Largest float64 + Average float64 + StdDeviation float64 + + SmallestLabel string + LargestLabel string + AverageLabel string + Units string +} + +type SpecState uint + +const ( + SpecStateInvalid SpecState = iota + + SpecStatePending + SpecStateSkipped + SpecStatePassed + SpecStateFailed + SpecStatePanicked + SpecStateTimedOut +) + +type SpecComponentType uint + +const ( + SpecComponentTypeInvalid SpecComponentType = iota + + SpecComponentTypeContainer + SpecComponentTypeBeforeSuite + SpecComponentTypeAfterSuite + SpecComponentTypeBeforeEach + SpecComponentTypeJustBeforeEach + SpecComponentTypeAfterEach + SpecComponentTypeIt + SpecComponentTypeMeasure +) + +type FlagType uint + +const ( + FlagTypeNone FlagType = iota + FlagTypeFocused + FlagTypePending +) diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_suite_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_suite_test.go new file mode 100644 index 00000000000..b026169c12b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_suite_test.go @@ -0,0 +1,13 @@ +package types_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Types Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_test.go b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_test.go new file mode 100644 index 00000000000..124b216ec60 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/ginkgo/types/types_test.go @@ -0,0 +1,81 @@ +package types_test + +import ( + . "github.com/onsi/ginkgo/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var specStates = []SpecState{ + SpecStatePassed, + SpecStateTimedOut, + SpecStatePanicked, + SpecStateFailed, + SpecStatePending, + SpecStateSkipped, +} + +func verifySpecSummary(caller func(SpecSummary) bool, trueStates ...SpecState) { + summary := SpecSummary{} + trueStateLookup := map[SpecState]bool{} + for _, state := range trueStates { + trueStateLookup[state] = true + summary.State = state + Ω(caller(summary)).Should(BeTrue()) + } + + for _, state := range specStates { + if trueStateLookup[state] { + continue + } + summary.State = state + Ω(caller(summary)).Should(BeFalse()) + } +} + +var _ = Describe("Types", func() { + Describe("SpecSummary", func() { + It("knows when it is in a failure-like state", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.HasFailureState() + }, SpecStateTimedOut, SpecStatePanicked, SpecStateFailed) + }) + + It("knows when it passed", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.Passed() + }, SpecStatePassed) + }) + + It("knows when it has failed", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.Failed() + }, SpecStateFailed) + }) + + It("knows when it has panicked", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.Panicked() + }, SpecStatePanicked) + }) + + It("knows when it has timed out", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.TimedOut() + }, SpecStateTimedOut) + }) + + It("knows when it is pending", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.Pending() + }, SpecStatePending) + }) + + It("knows when it is skipped", func() { + verifySpecSummary(func(summary SpecSummary) bool { + return summary.Skipped() + }, SpecStateSkipped) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/.gitignore b/Godeps/_workspace/src/github.com/onsi/gomega/.gitignore new file mode 100644 index 00000000000..55145320362 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.test +. diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/.travis.yml b/Godeps/_workspace/src/github.com/onsi/gomega/.travis.yml new file mode 100644 index 00000000000..2ecdf95a534 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/.travis.yml @@ -0,0 +1,10 @@ +language: go +go: + - 1.3 + +install: + - go get -v ./... + - go get github.com/onsi/ginkgo + - go install github.com/onsi/ginkgo/ginkgo + +script: $HOME/gopath/bin/ginkgo -r --randomizeAllSpecs --failOnPending --randomizeSuites --race diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/CHANGELOG.md b/Godeps/_workspace/src/github.com/onsi/gomega/CHANGELOG.md new file mode 100644 index 00000000000..cfbde1f88d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/CHANGELOG.md @@ -0,0 +1,58 @@ +## HEAD + +Improvements: + +- Added `BeSent` which attempts to send a value down a channel and fails if the attempt blocks. Can be paired with `Eventually` to safely send a value down a channel with a timeout. +- `Ω`, `Expect`, `Eventually`, and `Consistently` now immediately `panic` if there is no registered fail handler. This is always a mistake that can hide failing tests. +- `Receive()` no longer errors when passed a closed channel, it's perfectly fine to attempt to read from a closed channel so Ω(c).Should(Receive()) always fails and Ω(c).ShoudlNot(Receive()) always passes with a closed channel. +- Added `HavePrefix` and `HaveSuffix` matchers. + +## 1.0 (8/2/2014) + +No changes. Dropping "beta" from the version number. + +## 1.0.0-beta (7/8/2014) +Breaking Changes: + +- Changed OmegaMatcher interface. Instead of having `Match` return failure messages, two new methods `FailureMessage` and `NegatedFailureMessage` are called instead. +- Moved and renamed OmegaFailHandler to types.GomegaFailHandler and OmegaMatcher to types.GomegaMatcher. Any references to OmegaMatcher in any custom matchers will need to be changed to point to types.GomegaMatcher + +New Test-Support Features: + +- `ghttp`: supports testing http clients + - Provides a flexible fake http server + - Provides a collection of chainable http handlers that perform assertions. +- `gbytes`: supports making ordered assertions against streams of data + - Provides a `gbytes.Buffer` + - Provides a `Say` matcher to perform ordered assertions against output data +- `gexec`: supports testing external processes + - Provides support for building Go binaries + - Wraps and starts `exec.Cmd` commands + - Makes it easy to assert against stdout and stderr + - Makes it easy to send signals and wait for processes to exit + - Provides an `Exit` matcher to assert against exit code. + +DSL Changes: + +- `Eventually` and `Consistently` can accept `time.Duration` interval and polling inputs. +- The default timeouts for `Eventually` and `Consistently` are now configurable. + +New Matchers: + +- `ConsistOf`: order-independent assertion against the elements of an array/slice or keys of a map. +- `BeTemporally`: like `BeNumerically` but for `time.Time` +- `HaveKeyWithValue`: asserts a map has a given key with the given value. + +Updated Matchers: + +- `Receive` matcher can take a matcher as an argument and passes only if the channel under test receives an objet that satisfies the passed-in matcher. +- Matchers that implement `MatchMayChangeInTheFuture(actual interface{}) bool` can inform `Eventually` and/or `Consistently` when a match has no chance of changing status in the future. For example, `Receive` returns `false` when a channel is closed. + +Misc: + +- Start using semantic versioning +- Start maintaining changelog + +Major refactor: + +- Pull out Gomega's internal to `internal` diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/LICENSE b/Godeps/_workspace/src/github.com/onsi/gomega/LICENSE new file mode 100644 index 00000000000..9415ee72c17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/README.md b/Godeps/_workspace/src/github.com/onsi/gomega/README.md new file mode 100644 index 00000000000..c8255919225 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/README.md @@ -0,0 +1,17 @@ +![Gomega: Ginkgo's Preferred Matcher Library](http://onsi.github.io/gomega/images/gomega.png) + +[![Build Status](https://travis-ci.org/onsi/gomega.png)](https://travis-ci.org/onsi/gomega) + +Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers). + +To discuss Gomega and get updates, join the [google group](https://groups.google.com/d/forum/ginkgo-and-gomega). + +## [Ginkgo](http://github.com/onsi/ginkgo): a BDD Testing Framework for Golang + +Learn more about Ginkgo [here](http://onsi.github.io/ginkgo/) + +## License + +Gomega is MIT-Licensed + +The `ConsistOf` matcher uses [goraph](https://github.com/amitkgupta/goraph) which is embedded in the source to simplify distribution. goraph has an MIT license. diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/format/format.go b/Godeps/_workspace/src/github.com/onsi/gomega/format/format.go new file mode 100644 index 00000000000..ec9c91a42f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/format/format.go @@ -0,0 +1,276 @@ +/* +Gomega's format package pretty-prints objects. It explores input objects recursively and generates formatted, indented output with type information. +*/ +package format + +import ( + "fmt" + "reflect" + "strings" +) + +// Use MaxDepth to set the maximum recursion depth when printing deeply nested objects +var MaxDepth = uint(10) + +/* +By default, all objects (even those that implement fmt.Stringer and fmt.GoStringer) are recursively inspected to generate output. + +Set UseStringerRepresentation = true to use GoString (for fmt.GoStringers) or String (for fmt.Stringer) instead. + +Note that GoString and String don't always have all the information you need to understand why a test failed! +*/ +var UseStringerRepresentation = false + +//The default indentation string emitted by the format package +var Indent = " " + +var longFormThreshold = 20 + +/* +Generates a formatted matcher success/failure message of the form: + + Expected + + + + +If expected is omited, then the message looks like: + + Expected + + +*/ +func Message(actual interface{}, message string, expected ...interface{}) string { + if len(expected) == 0 { + return fmt.Sprintf("Expected\n%s\n%s", Object(actual, 1), message) + } else { + return fmt.Sprintf("Expected\n%s\n%s\n%s", Object(actual, 1), message, Object(expected[0], 1)) + } +} + +/* +Pretty prints the passed in object at the passed in indentation level. + +Object recurses into deeply nested objects emitting pretty-printed representations of their components. + +Modify format.MaxDepth to control how deep the recursion is allowed to go +Set format.UseStringerRepresentation to true to return object.GoString() or object.String() when available instead of +recursing into the object. +*/ +func Object(object interface{}, indentation uint) string { + indent := strings.Repeat(Indent, int(indentation)) + value := reflect.ValueOf(object) + return fmt.Sprintf("%s<%s>: %s", indent, formatType(object), formatValue(value, indentation)) +} + +/* +IndentString takes a string and indents each line by the specified amount. +*/ +func IndentString(s string, indentation uint) string { + components := strings.Split(s, "\n") + result := "" + indent := strings.Repeat(Indent, int(indentation)) + for i, component := range components { + result += indent + component + if i < len(components)-1 { + result += "\n" + } + } + + return result +} + +func formatType(object interface{}) string { + t := reflect.TypeOf(object) + if t == nil { + return "nil" + } + switch t.Kind() { + case reflect.Chan: + v := reflect.ValueOf(object) + return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap()) + case reflect.Ptr: + return fmt.Sprintf("%T | %p", object, object) + case reflect.Slice: + v := reflect.ValueOf(object) + return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap()) + case reflect.Map: + v := reflect.ValueOf(object) + return fmt.Sprintf("%T | len:%d", object, v.Len()) + default: + return fmt.Sprintf("%T", object) + } +} + +func formatValue(value reflect.Value, indentation uint) string { + if indentation > MaxDepth { + return "..." + } + + if isNilValue(value) { + return "nil" + } + + if UseStringerRepresentation { + if value.CanInterface() { + obj := value.Interface() + switch x := obj.(type) { + case fmt.GoStringer: + return x.GoString() + case fmt.Stringer: + return x.String() + } + } + } + + switch value.Kind() { + case reflect.Bool: + return fmt.Sprintf("%v", value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return fmt.Sprintf("%v", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return fmt.Sprintf("%v", value.Uint()) + case reflect.Uintptr: + return fmt.Sprintf("0x%x", value.Uint()) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%v", value.Float()) + case reflect.Complex64, reflect.Complex128: + return fmt.Sprintf("%v", value.Complex()) + case reflect.Chan: + return fmt.Sprintf("0x%x", value.Pointer()) + case reflect.Func: + return fmt.Sprintf("0x%x", value.Pointer()) + case reflect.Ptr: + return formatValue(value.Elem(), indentation) + case reflect.Slice: + if value.Type().Elem().Kind() == reflect.Uint8 { + return formatString(value.Bytes(), indentation) + } + return formatSlice(value, indentation) + case reflect.String: + return formatString(value.String(), indentation) + case reflect.Array: + return formatSlice(value, indentation) + case reflect.Map: + return formatMap(value, indentation) + case reflect.Struct: + return formatStruct(value, indentation) + case reflect.Interface: + return formatValue(value.Elem(), indentation) + default: + if value.CanInterface() { + return fmt.Sprintf("%#v", value.Interface()) + } else { + return fmt.Sprintf("%#v", value) + } + } +} + +func formatString(object interface{}, indentation uint) string { + if indentation == 1 { + s := fmt.Sprintf("%s", object) + components := strings.Split(s, "\n") + result := "" + for i, component := range components { + if i == 0 { + result += component + } else { + result += Indent + component + } + if i < len(components)-1 { + result += "\n" + } + } + + return fmt.Sprintf("%s", result) + } else { + return fmt.Sprintf("%q", object) + } +} + +func formatSlice(v reflect.Value, indentation uint) string { + l := v.Len() + result := make([]string, l) + longest := 0 + for i := 0; i < l; i++ { + result[i] = formatValue(v.Index(i), indentation+1) + if len(result[i]) > longest { + longest = len(result[i]) + } + } + + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("[\n%s%s,\n%s]", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } else { + return fmt.Sprintf("[%s]", strings.Join(result, ", ")) + } +} + +func formatMap(v reflect.Value, indentation uint) string { + l := v.Len() + result := make([]string, l) + + longest := 0 + for i, key := range v.MapKeys() { + value := v.MapIndex(key) + result[i] = fmt.Sprintf("%s: %s", formatValue(key, 0), formatValue(value, indentation+1)) + if len(result[i]) > longest { + longest = len(result[i]) + } + } + + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("{\n%s%s,\n%s}", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } else { + return fmt.Sprintf("{%s}", strings.Join(result, ", ")) + } +} + +func formatStruct(v reflect.Value, indentation uint) string { + t := v.Type() + + l := v.NumField() + result := []string{} + longest := 0 + for i := 0; i < l; i++ { + structField := t.Field(i) + fieldEntry := v.Field(i) + representation := fmt.Sprintf("%s: %s", structField.Name, formatValue(fieldEntry, indentation+1)) + result = append(result, representation) + if len(representation) > longest { + longest = len(representation) + } + } + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("{\n%s%s,\n%s}", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } else { + return fmt.Sprintf("{%s}", strings.Join(result, ", ")) + } +} + +func isNilValue(a reflect.Value) bool { + switch a.Kind() { + case reflect.Invalid: + return true + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return a.IsNil() + } + + return false +} + +func isNil(a interface{}) bool { + if a == nil { + return true + } + + switch reflect.TypeOf(a).Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return reflect.ValueOf(a).IsNil() + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/format/format_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/format/format_suite_test.go new file mode 100644 index 00000000000..8e65a95292d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/format/format_suite_test.go @@ -0,0 +1,13 @@ +package format_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFormat(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Format Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/format/format_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/format/format_test.go new file mode 100644 index 00000000000..fd926f58ea8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/format/format_test.go @@ -0,0 +1,449 @@ +package format_test + +import ( + "fmt" + "strings" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +//recursive struct + +type StringAlias string +type ByteAlias []byte +type IntAlias int + +type AStruct struct { + Exported string +} + +type SimpleStruct struct { + Name string + Enumeration int + Veritas bool + Data []byte + secret uint32 +} + +type ComplexStruct struct { + Strings []string + SimpleThings []*SimpleStruct + DataMaps map[int]ByteAlias +} + +type SecretiveStruct struct { + boolValue bool + intValue int + uintValue uint + uintptrValue uintptr + floatValue float32 + complexValue complex64 + chanValue chan bool + funcValue func() + pointerValue *int + sliceValue []string + byteSliceValue []byte + stringValue string + arrValue [3]int + byteArrValue [3]byte + mapValue map[string]int + structValue AStruct + interfaceValue interface{} +} + +type GoStringer struct { +} + +func (g GoStringer) GoString() string { + return "go-string" +} + +func (g GoStringer) String() string { + return "string" +} + +type Stringer struct { +} + +func (g Stringer) String() string { + return "string" +} + +var _ = Describe("Format", func() { + match := func(typeRepresentation string, valueRepresentation string, args ...interface{}) types.GomegaMatcher { + if len(args) > 0 { + valueRepresentation = fmt.Sprintf(valueRepresentation, args...) + } + return Equal(fmt.Sprintf("%s<%s>: %s", Indent, typeRepresentation, valueRepresentation)) + } + + matchRegexp := func(typeRepresentation string, valueRepresentation string, args ...interface{}) types.GomegaMatcher { + if len(args) > 0 { + valueRepresentation = fmt.Sprintf(valueRepresentation, args...) + } + return MatchRegexp(fmt.Sprintf("%s<%s>: %s", Indent, typeRepresentation, valueRepresentation)) + } + + hashMatchingRegexp := func(entries ...string) string { + entriesSwitch := "(" + strings.Join(entries, "|") + ")" + arr := make([]string, len(entries)) + for i := range arr { + arr[i] = entriesSwitch + } + return "{" + strings.Join(arr, ", ") + "}" + } + + Describe("Message", func() { + Context("with only an actual value", func() { + It("should print out an indented formatted representation of the value and the message", func() { + Ω(Message(3, "to be three.")).Should(Equal("Expected\n : 3\nto be three.")) + }) + }) + + Context("with an actual and an expected value", func() { + It("should print out an indented formatted representatino of both values, and the message", func() { + Ω(Message(3, "to equal", 4)).Should(Equal("Expected\n : 3\nto equal\n : 4")) + }) + }) + }) + + Describe("IndentString", func() { + It("should indent the string", func() { + Ω(IndentString("foo\n bar\nbaz", 2)).Should(Equal(" foo\n bar\n baz")) + }) + }) + + Describe("Object", func() { + Describe("formatting boolean values", func() { + It("should give the type and format values correctly", func() { + Ω(Object(true, 1)).Should(match("bool", "true")) + Ω(Object(false, 1)).Should(match("bool", "false")) + }) + }) + + Describe("formatting numbers", func() { + It("should give the type and format values correctly", func() { + Ω(Object(int(3), 1)).Should(match("int", "3")) + Ω(Object(int8(3), 1)).Should(match("int8", "3")) + Ω(Object(int16(3), 1)).Should(match("int16", "3")) + Ω(Object(int32(3), 1)).Should(match("int32", "3")) + Ω(Object(int64(3), 1)).Should(match("int64", "3")) + + Ω(Object(uint(3), 1)).Should(match("uint", "3")) + Ω(Object(uint8(3), 1)).Should(match("uint8", "3")) + Ω(Object(uint16(3), 1)).Should(match("uint16", "3")) + Ω(Object(uint32(3), 1)).Should(match("uint32", "3")) + Ω(Object(uint64(3), 1)).Should(match("uint64", "3")) + }) + + It("should handle uintptr differently", func() { + Ω(Object(uintptr(3), 1)).Should(match("uintptr", "0x3")) + }) + }) + + Describe("formatting channels", func() { + It("should give the type and format values correctly", func() { + c := make(chan<- bool, 3) + c <- true + c <- false + Ω(Object(c, 1)).Should(match("chan<- bool | len:2, cap:3", "%v", c)) + }) + }) + + Describe("formatting strings", func() { + It("should give the type and format values correctly", func() { + s := "a\nb\nc" + Ω(Object(s, 1)).Should(match("string", `a + b + c`)) + }) + }) + + Describe("formatting []byte slices", func() { + It("should present them as strings", func() { + b := []byte("a\nb\nc") + Ω(Object(b, 1)).Should(matchRegexp(`\[\]uint8 \| len:5, cap:\d+`, `a + b + c`)) + }) + }) + + Describe("formatting functions", func() { + It("should give the type and format values correctly", func() { + f := func(a string, b []int) ([]byte, error) { + return []byte("abc"), nil + } + Ω(Object(f, 1)).Should(match("func(string, []int) ([]uint8, error)", "%v", f)) + }) + }) + + Describe("formatting pointers", func() { + It("should give the type and dereference the value to format it correctly", func() { + a := 3 + Ω(Object(&a, 1)).Should(match(fmt.Sprintf("*int | %p", &a), "3")) + }) + + Context("when there are pointers to pointers...", func() { + It("should recursively deference the pointer until it gets to a value", func() { + a := 3 + var b *int + var c **int + var d ***int + b = &a + c = &b + d = &c + + Ω(Object(d, 1)).Should(match(fmt.Sprintf("***int | %p", d), "3")) + }) + }) + + Context("when the pointer points to nil", func() { + It("should say nil and not explode", func() { + var a *AStruct + Ω(Object(a, 1)).Should(match("*format_test.AStruct | 0x0", "nil")) + }) + }) + }) + + Describe("formatting arrays", func() { + It("should give the type and format values correctly", func() { + w := [3]string{"Jed Bartlet", "Toby Ziegler", "CJ Cregg"} + Ω(Object(w, 1)).Should(match("[3]string", `["Jed Bartlet", "Toby Ziegler", "CJ Cregg"]`)) + }) + + Context("with byte arrays", func() { + It("should give the type and format values correctly", func() { + w := [3]byte{17, 28, 19} + Ω(Object(w, 1)).Should(match("[3]uint8", `[17, 28, 19]`)) + }) + }) + }) + + Describe("formatting slices", func() { + It("should include the length and capacity in the type information", func() { + s := make([]bool, 3, 4) + Ω(Object(s, 1)).Should(match("[]bool | len:3, cap:4", "[false, false, false]")) + }) + + Context("when the slice contains long entries", func() { + It("should format the entries with newlines", func() { + w := []string{"Josiah Edward Bartlet", "Toby Ziegler", "CJ Cregg"} + expected := `[ + "Josiah Edward Bartlet", + "Toby Ziegler", + "CJ Cregg", + ]` + Ω(Object(w, 1)).Should(match("[]string | len:3, cap:3", expected)) + }) + }) + }) + + Describe("formatting maps", func() { + It("should include the length in the type information", func() { + m := make(map[int]bool, 5) + m[3] = true + m[4] = false + Ω(Object(m, 1)).Should(matchRegexp(`map\[int\]bool \| len:2`, hashMatchingRegexp("3: true", "4: false"))) + }) + + Context("when the slice contains long entries", func() { + It("should format the entries with newlines", func() { + m := map[string][]byte{} + m["Josiah Edward Bartlet"] = []byte("Martin Sheen") + m["Toby Ziegler"] = []byte("Richard Schiff") + m["CJ Cregg"] = []byte("Allison Janney") + expected := `{ + ("Josiah Edward Bartlet": "Martin Sheen"|"Toby Ziegler": "Richard Schiff"|"CJ Cregg": "Allison Janney"), + ("Josiah Edward Bartlet": "Martin Sheen"|"Toby Ziegler": "Richard Schiff"|"CJ Cregg": "Allison Janney"), + ("Josiah Edward Bartlet": "Martin Sheen"|"Toby Ziegler": "Richard Schiff"|"CJ Cregg": "Allison Janney"), + }` + Ω(Object(m, 1)).Should(matchRegexp(`map\[string\]\[\]uint8 \| len:3`, expected)) + }) + }) + }) + + Describe("formatting structs", func() { + It("should include the struct name and the field names", func() { + s := SimpleStruct{ + Name: "Oswald", + Enumeration: 17, + Veritas: true, + Data: []byte("datum"), + secret: 1983, + } + + Ω(Object(s, 1)).Should(match("format_test.SimpleStruct", `{Name: "Oswald", Enumeration: 17, Veritas: true, Data: "datum", secret: 1983}`)) + }) + + Context("when the struct contains long entries", func() { + It("should format the entries with new lines", func() { + s := &SimpleStruct{ + Name: "Mithrandir Gandalf Greyhame", + Enumeration: 2021, + Veritas: true, + Data: []byte("wizard"), + secret: 3, + } + + Ω(Object(s, 1)).Should(match(fmt.Sprintf("*format_test.SimpleStruct | %p", s), `{ + Name: "Mithrandir Gandalf Greyhame", + Enumeration: 2021, + Veritas: true, + Data: "wizard", + secret: 3, + }`)) + }) + }) + }) + + Describe("formatting nil values", func() { + It("should print out nil", func() { + Ω(Object(nil, 1)).Should(match("nil", "nil")) + var typedNil *AStruct + Ω(Object(typedNil, 1)).Should(match("*format_test.AStruct | 0x0", "nil")) + var c chan<- bool + Ω(Object(c, 1)).Should(match("chan<- bool | len:0, cap:0", "nil")) + var s []string + Ω(Object(s, 1)).Should(match("[]string | len:0, cap:0", "nil")) + var m map[string]bool + Ω(Object(m, 1)).Should(match("map[string]bool | len:0", "nil")) + }) + }) + + Describe("formatting aliased types", func() { + It("should print out the correct alias type", func() { + Ω(Object(StringAlias("alias"), 1)).Should(match("format_test.StringAlias", `alias`)) + Ω(Object(ByteAlias("alias"), 1)).Should(matchRegexp(`format_test\.ByteAlias \| len:5, cap:\d+`, `alias`)) + Ω(Object(IntAlias(3), 1)).Should(match("format_test.IntAlias", "3")) + }) + }) + + Describe("handling nested things", func() { + It("should produce a correctly nested representation", func() { + s := ComplexStruct{ + Strings: []string{"lots", "of", "short", "strings"}, + SimpleThings: []*SimpleStruct{ + {"short", 7, true, []byte("succinct"), 17}, + {"something longer", 427, true, []byte("designed to wrap around nicely"), 30}, + }, + DataMaps: map[int]ByteAlias{ + 17: ByteAlias("some substantially longer chunks of data"), + 1138: ByteAlias("that should make things wrap"), + }, + } + expected := `{ + Strings: \["lots", "of", "short", "strings"\], + SimpleThings: \[ + {Name: "short", Enumeration: 7, Veritas: true, Data: "succinct", secret: 17}, + { + Name: "something longer", + Enumeration: 427, + Veritas: true, + Data: "designed to wrap around nicely", + secret: 30, + }, + \], + DataMaps: { + (17: "some substantially longer chunks of data"|1138: "that should make things wrap"), + (17: "some substantially longer chunks of data"|1138: "that should make things wrap"), + }, + }` + Ω(Object(s, 1)).Should(matchRegexp(`format_test\.ComplexStruct`, expected)) + }) + }) + }) + + Describe("Handling unexported fields in structs", func() { + It("should handle all the various types correctly", func() { + a := int(5) + s := SecretiveStruct{ + boolValue: true, + intValue: 3, + uintValue: 4, + uintptrValue: 5, + floatValue: 6.0, + complexValue: complex(5.0, 3.0), + chanValue: make(chan bool, 2), + funcValue: func() {}, + pointerValue: &a, + sliceValue: []string{"string", "slice"}, + byteSliceValue: []byte("bytes"), + stringValue: "a string", + arrValue: [3]int{11, 12, 13}, + byteArrValue: [3]byte{17, 20, 32}, + mapValue: map[string]int{"a key": 20, "b key": 30}, + structValue: AStruct{"exported"}, + interfaceValue: map[string]int{"a key": 17}, + } + + expected := fmt.Sprintf(`{ + boolValue: true, + intValue: 3, + uintValue: 4, + uintptrValue: 0x5, + floatValue: 6, + complexValue: \(5\+3i\), + chanValue: %p, + funcValue: %p, + pointerValue: 5, + sliceValue: \["string", "slice"\], + byteSliceValue: "bytes", + stringValue: "a string", + arrValue: \[11, 12, 13\], + byteArrValue: \[17, 20, 32\], + mapValue: %s, + structValue: {Exported: "exported"}, + interfaceValue: {"a key": 17}, + }`, s.chanValue, s.funcValue, hashMatchingRegexp(`"a key": 20`, `"b key": 30`)) + + Ω(Object(s, 1)).Should(matchRegexp(`format_test\.SecretiveStruct`, expected)) + }) + }) + + Describe("Handling interfaces", func() { + It("should unpack the interface", func() { + outerHash := map[string]interface{}{} + innerHash := map[string]int{} + + innerHash["inner"] = 3 + outerHash["integer"] = 2 + outerHash["map"] = innerHash + + expected := hashMatchingRegexp(`"integer": 2`, `"map": {"inner": 3}`) + Ω(Object(outerHash, 1)).Should(matchRegexp(`map\[string\]interface {} \| len:2`, expected)) + }) + }) + + Describe("Handling recursive things", func() { + It("should not go crazy...", func() { + m := map[string]interface{}{} + m["integer"] = 2 + m["map"] = m + Ω(Object(m, 1)).Should(ContainSubstring("...")) + }) + }) + + Describe("When instructed to use the Stringer representation", func() { + BeforeEach(func() { + UseStringerRepresentation = true + }) + + AfterEach(func() { + UseStringerRepresentation = false + }) + + Context("when passed a GoStringer", func() { + It("should use what GoString() returns", func() { + Ω(Object(GoStringer{}, 1)).Should(ContainSubstring(": go-string")) + }) + }) + + Context("when passed a stringer", func() { + It("should use what String() returns", func() { + Ω(Object(Stringer{}, 1)).Should(ContainSubstring(": string")) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer.go b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer.go new file mode 100644 index 00000000000..7e42334af35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer.go @@ -0,0 +1,204 @@ +/* +Package gbytes provides a buffer that supports incrementally detecting input. + +You use gbytes.Buffer with the gbytes.Say matcher. When Say finds a match, it fastforwards the buffer's read cursor to the end of that match. + +Subsequent matches against the buffer will only operate against data that appears *after* the read cursor. + +The read cursor is an opaque implementation detail that you cannot access. You should use the Say matcher to sift through the buffer. You can always +access the entire buffer's contents with Contents(). + +*/ +package gbytes + +import ( + "errors" + "fmt" + "regexp" + "sync" + "time" +) + +/* +gbytes.Buffer implements an io.Writer and can be used with the gbytes.Say matcher. + +You should only use a gbytes.Buffer in test code. It stores all writes in an in-memory buffer - behavior that is inappropriate for production code! +*/ +type Buffer struct { + contents []byte + readCursor uint64 + lock *sync.Mutex + detectCloser chan interface{} + closed bool +} + +/* +NewBuffer returns a new gbytes.Buffer +*/ +func NewBuffer() *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + } +} + +/* +BufferWithBytes returns a new gbytes.Buffer seeded with the passed in bytes +*/ +func BufferWithBytes(bytes []byte) *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + contents: bytes, + } +} + +/* +Write implements the io.Writer interface +*/ +func (b *Buffer) Write(p []byte) (n int, err error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return 0, errors.New("attempt to write to closed buffer") + } + + b.contents = append(b.contents, p...) + return len(p), nil +} + +/* +Close signifies that the buffer will no longer be written to +*/ +func (b *Buffer) Close() error { + b.lock.Lock() + defer b.lock.Unlock() + + b.closed = true + + return nil +} + +/* +Closed returns true if the buffer has been closed +*/ +func (b *Buffer) Closed() bool { + b.lock.Lock() + defer b.lock.Unlock() + + return b.closed +} + +/* +Contents returns all data ever written to the buffer. +*/ +func (b *Buffer) Contents() []byte { + b.lock.Lock() + defer b.lock.Unlock() + + contents := make([]byte, len(b.contents)) + copy(contents, b.contents) + return contents +} + +/* +Detect takes a regular expression and returns a channel. + +The channel will receive true the first time data matching the regular expression is written to the buffer. +The channel is subsequently closed and the buffer's read-cursor is fast-forwarded to just after the matching region. + +You typically don't need to use Detect and should use the ghttp.Say matcher instead. Detect is useful, however, in cases where your code must +be branch and handle different outputs written to the buffer. + +For example, consider a buffer hooked up to the stdout of a client library. You may (or may not, depending on state outside of your control) need to authenticate the client library. + +You could do something like: + +select { +case <-buffer.Detect("You are not logged in"): + //log in +case <-buffer.Detect("Success"): + //carry on +case <-time.After(time.Second): + //welp +} +buffer.CancelDetects() + +You should always call CancelDetects after using Detect. This will close any channels that have not detected and clean up the goroutines that were spawned to support them. + +Finally, you can pass detect a format string followed by variadic arguments. This will construct the regexp using fmt.Sprintf. +*/ +func (b *Buffer) Detect(desired string, args ...interface{}) chan bool { + formattedRegexp := desired + if len(args) > 0 { + formattedRegexp = fmt.Sprintf(desired, args...) + } + re := regexp.MustCompile(formattedRegexp) + + b.lock.Lock() + defer b.lock.Unlock() + + if b.detectCloser == nil { + b.detectCloser = make(chan interface{}) + } + + closer := b.detectCloser + response := make(chan bool) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + defer close(response) + for { + select { + case <-ticker.C: + b.lock.Lock() + data, cursor := b.contents[b.readCursor:], b.readCursor + loc := re.FindIndex(data) + b.lock.Unlock() + + if loc != nil { + response <- true + b.lock.Lock() + newCursorPosition := cursor + uint64(loc[1]) + if newCursorPosition >= b.readCursor { + b.readCursor = newCursorPosition + } + b.lock.Unlock() + return + } + case <-closer: + return + } + } + }() + + return response +} + +/* +CancelDetects cancels any pending detects and cleans up their goroutines. You should always call this when you're done with a set of Detect channels. +*/ +func (b *Buffer) CancelDetects() { + b.lock.Lock() + defer b.lock.Unlock() + + close(b.detectCloser) + b.detectCloser = nil +} + +func (b *Buffer) didSay(re *regexp.Regexp) (bool, []byte) { + b.lock.Lock() + defer b.lock.Unlock() + + unreadBytes := b.contents[b.readCursor:] + copyOfUnreadBytes := make([]byte, len(unreadBytes)) + copy(copyOfUnreadBytes, unreadBytes) + + loc := re.FindIndex(unreadBytes) + + if loc != nil { + b.readCursor += uint64(loc[1]) + return true, copyOfUnreadBytes + } else { + return false, copyOfUnreadBytes + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer_test.go new file mode 100644 index 00000000000..9aa2937dd89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/buffer_test.go @@ -0,0 +1,121 @@ +package gbytes_test + +import ( + "time" + . "github.com/onsi/gomega/gbytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Buffer", func() { + var buffer *Buffer + + BeforeEach(func() { + buffer = NewBuffer() + }) + + Describe("dumping the entire contents of the buffer", func() { + It("should return everything that's been written", func() { + buffer.Write([]byte("abc")) + buffer.Write([]byte("def")) + Ω(buffer.Contents()).Should(Equal([]byte("abcdef"))) + + Ω(buffer).Should(Say("bcd")) + Ω(buffer.Contents()).Should(Equal([]byte("abcdef"))) + }) + }) + + Describe("creating a buffer with bytes", func() { + It("should create the buffer with the cursor set to the beginning", func() { + buffer := BufferWithBytes([]byte("abcdef")) + Ω(buffer.Contents()).Should(Equal([]byte("abcdef"))) + Ω(buffer).Should(Say("abc")) + Ω(buffer).ShouldNot(Say("abc")) + Ω(buffer).Should(Say("def")) + }) + }) + + Describe("detecting regular expressions", func() { + It("should fire the appropriate channel when the passed in pattern matches, then close it", func(done Done) { + go func() { + time.Sleep(10 * time.Millisecond) + buffer.Write([]byte("abcde")) + }() + + A := buffer.Detect("%s", "a.c") + B := buffer.Detect("def") + + var gotIt bool + select { + case gotIt = <-A: + case <-B: + Fail("should not have gotten here") + } + + Ω(gotIt).Should(BeTrue()) + Eventually(A).Should(BeClosed()) + + buffer.Write([]byte("f")) + Eventually(B).Should(Receive()) + Eventually(B).Should(BeClosed()) + + close(done) + }) + + It("should fast-forward the buffer upon detection", func(done Done) { + buffer.Write([]byte("abcde")) + <-buffer.Detect("abc") + Ω(buffer).ShouldNot(Say("abc")) + Ω(buffer).Should(Say("de")) + close(done) + }) + + It("should only fast-forward the buffer when the channel is read, and only if doing so would not rewind it", func(done Done) { + buffer.Write([]byte("abcde")) + A := buffer.Detect("abc") + time.Sleep(20 * time.Millisecond) //give the goroutine a chance to detect and write to the channel + Ω(buffer).Should(Say("abcd")) + <-A + Ω(buffer).ShouldNot(Say("d")) + Ω(buffer).Should(Say("e")) + Eventually(A).Should(BeClosed()) + close(done) + }) + + It("should be possible to cancel a detection", func(done Done) { + A := buffer.Detect("abc") + B := buffer.Detect("def") + buffer.CancelDetects() + buffer.Write([]byte("abcdef")) + Eventually(A).Should(BeClosed()) + Eventually(B).Should(BeClosed()) + + Ω(buffer).Should(Say("bcde")) + <-buffer.Detect("f") + close(done) + }) + }) + + Describe("closing the buffer", func() { + It("should error when further write attempts are made", func() { + _, err := buffer.Write([]byte("abc")) + Ω(err).ShouldNot(HaveOccurred()) + + buffer.Close() + + _, err = buffer.Write([]byte("def")) + Ω(err).Should(HaveOccurred()) + + Ω(buffer.Contents()).Should(Equal([]byte("abc"))) + }) + + It("should be closed", func() { + Ω(buffer.Closed()).Should(BeFalse()) + + buffer.Close() + + Ω(buffer.Closed()).Should(BeTrue()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/gbuffer_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/gbuffer_suite_test.go new file mode 100644 index 00000000000..3a7dc06123e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/gbuffer_suite_test.go @@ -0,0 +1,13 @@ +package gbytes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGbytes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gbytes Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher.go new file mode 100644 index 00000000000..ce5ebcbfa59 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher.go @@ -0,0 +1,105 @@ +package gbytes + +import ( + "fmt" + "regexp" + + "github.com/onsi/gomega/format" +) + +//Objects satisfying the BufferProvider can be used with the Say matcher. +type BufferProvider interface { + Buffer() *Buffer +} + +/* +Say is a Gomega matcher that operates on gbytes.Buffers: + + Ω(buffer).Should(Say("something")) + +will succeed if the unread portion of the buffer matches the regular expression "something". + +When Say succeeds, it fast forwards the gbytes.Buffer's read cursor to just after the succesful match. +Thus, subsequent calls to Say will only match against the unread portion of the buffer + +Say pairs very well with Eventually. To asser that a buffer eventually receives data matching "[123]-star" within 3 seconds you can: + + Eventually(buffer, 3).Should(Say("[123]-star")) + +Ditto with consistently. To assert that a buffer does not receive data matching "never-see-this" for 1 second you can: + + Consistently(buffer, 1).ShouldNot(Say("never-see-this")) + +In addition to bytes.Buffers, Say can operate on objects that implement the gbytes.BufferProvider interface. +In such cases, Say simply operates on the *gbytes.Buffer returned by Buffer() + +If the buffer is closed, the Say matcher will tell Eventually to abort. +*/ +func Say(expected string, args ...interface{}) *sayMatcher { + formattedRegexp := expected + if len(args) > 0 { + formattedRegexp = fmt.Sprintf(expected, args...) + } + return &sayMatcher{ + re: regexp.MustCompile(formattedRegexp), + } +} + +type sayMatcher struct { + re *regexp.Regexp + receivedSayings []byte +} + +func (m *sayMatcher) buffer(actual interface{}) (*Buffer, bool) { + var buffer *Buffer + + switch x := actual.(type) { + case *Buffer: + buffer = x + case BufferProvider: + buffer = x.Buffer() + default: + return nil, false + } + + return buffer, true +} + +func (m *sayMatcher) Match(actual interface{}) (success bool, err error) { + buffer, ok := m.buffer(actual) + if !ok { + return false, fmt.Errorf("Say must be passed a *gbytes.Buffer or BufferProvider. Got:\n%s", format.Object(actual, 1)) + } + + didSay, sayings := buffer.didSay(m.re) + m.receivedSayings = sayings + + return didSay, nil +} + +func (m *sayMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Got stuck at:\n%s\nWaiting for:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Saw:\n%s\nWhich matches the unexpected:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + switch x := actual.(type) { + case *Buffer: + return !x.Closed() + case BufferProvider: + return !x.Buffer().Closed() + default: + return true + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher_test.go new file mode 100644 index 00000000000..d0ddf1f74ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gbytes/say_matcher_test.go @@ -0,0 +1,163 @@ +package gbytes_test + +import ( + "time" + . "github.com/onsi/gomega/gbytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type speaker struct { + buffer *Buffer +} + +func (s *speaker) Buffer() *Buffer { + return s.buffer +} + +var _ = Describe("SayMatcher", func() { + var buffer *Buffer + + BeforeEach(func() { + buffer = NewBuffer() + buffer.Write([]byte("abc")) + }) + + Context("when actual is not a gexec Buffer, or a BufferProvider", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Ω("foo").Should(Say("foo")) + }) + Ω(failures[0]).Should(ContainSubstring("*gbytes.Buffer")) + }) + }) + + Context("when a match is found", func() { + It("should succeed", func() { + Ω(buffer).Should(Say("abc")) + }) + + It("should support printf-like formatting", func() { + Ω(buffer).Should(Say("a%sc", "b")) + }) + + It("should use a regular expression", func() { + Ω(buffer).Should(Say("a.c")) + }) + + It("should fastforward the buffer", func() { + buffer.Write([]byte("def")) + Ω(buffer).Should(Say("abcd")) + Ω(buffer).Should(Say("ef")) + Ω(buffer).ShouldNot(Say("[a-z]")) + }) + }) + + Context("when no match is found", func() { + It("should not error", func() { + Ω(buffer).ShouldNot(Say("def")) + }) + + Context("when the buffer is closed", func() { + BeforeEach(func() { + buffer.Close() + }) + + It("should abort an eventually", func() { + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(buffer).Should(Say("def")) + }) + Eventually(buffer).ShouldNot(Say("def")) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + Ω(failures).Should(HaveLen(1)) + + t = time.Now() + Eventually(buffer).Should(Say("abc")) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + }) + + It("should abort a consistently", func() { + t := time.Now() + Consistently(buffer, 2.0).ShouldNot(Say("def")) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + }) + + It("should not error with a synchronous matcher", func() { + Ω(buffer).ShouldNot(Say("def")) + Ω(buffer).Should(Say("abc")) + }) + }) + }) + + Context("when a positive match fails", func() { + It("should report where it got stuck", func() { + Ω(buffer).Should(Say("abc")) + buffer.Write([]byte("def")) + failures := InterceptGomegaFailures(func() { + Ω(buffer).Should(Say("abc")) + }) + Ω(failures[0]).Should(ContainSubstring("Got stuck at:")) + Ω(failures[0]).Should(ContainSubstring("def")) + }) + }) + + Context("when a negative match fails", func() { + It("should report where it got stuck", func() { + failures := InterceptGomegaFailures(func() { + Ω(buffer).ShouldNot(Say("abc")) + }) + Ω(failures[0]).Should(ContainSubstring("Saw:")) + Ω(failures[0]).Should(ContainSubstring("Which matches the unexpected:")) + Ω(failures[0]).Should(ContainSubstring("abc")) + }) + }) + + Context("when a match is not found", func() { + It("should not fastforward the buffer", func() { + Ω(buffer).ShouldNot(Say("def")) + Ω(buffer).Should(Say("abc")) + }) + }) + + Context("a nice real-life example", func() { + It("should behave well", func() { + Ω(buffer).Should(Say("abc")) + go func() { + time.Sleep(10 * time.Millisecond) + buffer.Write([]byte("def")) + }() + Ω(buffer).ShouldNot(Say("def")) + Eventually(buffer).Should(Say("def")) + }) + }) + + Context("when actual is a BufferProvider", func() { + It("should use actual's buffer", func() { + s := &speaker{ + buffer: NewBuffer(), + } + + Ω(s).ShouldNot(Say("abc")) + + s.Buffer().Write([]byte("abc")) + Ω(s).Should(Say("abc")) + }) + + It("should abort an eventually", func() { + s := &speaker{ + buffer: NewBuffer(), + } + + s.buffer.Close() + + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(s).Should(Say("def")) + }) + Ω(failures).Should(HaveLen(1)) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/build.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/build.go new file mode 100644 index 00000000000..3e9bf9f9478 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/build.go @@ -0,0 +1,78 @@ +package gexec + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" +) + +var tmpDir string + +/* +Build uses go build to compile the package at packagePath. The resulting binary is saved off in a temporary directory. +A path pointing to this binary is returned. + +Build uses the $GOPATH set in your environment. It passes the variadic args on to `go build`. +*/ +func Build(packagePath string, args ...string) (compiledPath string, err error) { + return BuildIn(os.Getenv("GOPATH"), packagePath, args...) +} + +/* +BuildIn is identical to Build but allows you to specify a custom $GOPATH (the first argument). +*/ +func BuildIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) { + tmpDir, err := temporaryDirectory() + if err != nil { + return "", err + } + + if len(gopath) == 0 { + return "", errors.New("$GOPATH not provided when building " + packagePath) + } + + executable := filepath.Join(tmpDir, path.Base(packagePath)) + if runtime.GOOS == "windows" { + executable = executable + ".exe" + } + + cmdArgs := append([]string{"build"}, args...) + cmdArgs = append(cmdArgs, "-o", executable, packagePath) + + build := exec.Command("go", cmdArgs...) + build.Env = append([]string{"GOPATH=" + gopath}, os.Environ()...) + + output, err := build.CombinedOutput() + if err != nil { + return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output)) + } + + return executable, nil +} + +/* +You should call CleanupBuildArtifacts before your test ends to clean up any temporary artifacts generated by +gexec. In Ginkgo this is typically done in an AfterSuite callback. +*/ +func CleanupBuildArtifacts() { + if tmpDir != "" { + os.RemoveAll(tmpDir) + } +} + +func temporaryDirectory() (string, error) { + var err error + if tmpDir == "" { + tmpDir, err = ioutil.TempDir("", "gexec_artifacts") + if err != nil { + return "", err + } + } + + return ioutil.TempDir(tmpDir, "g") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher.go new file mode 100644 index 00000000000..e6f43294270 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher.go @@ -0,0 +1,88 @@ +package gexec + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +/* +The Exit matcher operates on a session: + + Ω(session).Should(Exit()) + +Exit passes if the session has already exited. + +If no status code is provided, then Exit will succeed if the session has exited regardless of exit code. +Otherwise, Exit will only succeed if the process has exited with the provided status code. + +Note that the process must have already exited. To wait for a process to exit, use Eventually: + + Eventually(session, 3).Should(Exit(0)) +*/ +func Exit(optionalExitCode ...int) *exitMatcher { + exitCode := -1 + if len(optionalExitCode) > 0 { + exitCode = optionalExitCode[0] + } + + return &exitMatcher{ + exitCode: exitCode, + } +} + +type exitMatcher struct { + exitCode int + didExit bool + actualExitCode int +} + +type Exiter interface { + ExitCode() int +} + +func (m *exitMatcher) Match(actual interface{}) (success bool, err error) { + exiter, ok := actual.(Exiter) + if !ok { + return false, fmt.Errorf("Exit must be passed a gexec.Exiter (Missing method ExitCode() int) Got:\n%s", format.Object(actual, 1)) + } + + m.actualExitCode = exiter.ExitCode() + + if m.actualExitCode == -1 { + return false, nil + } + + if m.exitCode == -1 { + return true, nil + } + return m.exitCode == m.actualExitCode, nil +} + +func (m *exitMatcher) FailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "Expected process to exit. It did not." + } else { + return format.Message(m.actualExitCode, "to match exit code:", m.exitCode) + } +} + +func (m *exitMatcher) NegatedFailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "you really shouldn't be able to see this!" + } else { + if m.exitCode == -1 { + return "Expected process not to exit. It did." + } else { + return format.Message(m.actualExitCode, "not to match exit code:", m.exitCode) + } + } +} + +func (m *exitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + session, ok := actual.(*Session) + if ok { + return session.ExitCode() == -1 + } + return true +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher_test.go new file mode 100644 index 00000000000..9f18e2d6e0a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/exit_matcher_test.go @@ -0,0 +1,113 @@ +package gexec_test + +import ( + "os/exec" + "time" + . "github.com/onsi/gomega/gexec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type NeverExits struct{} + +func (e NeverExits) ExitCode() int { + return -1 +} + +var _ = Describe("ExitMatcher", func() { + var command *exec.Cmd + var session *Session + + BeforeEach(func() { + var err error + command = exec.Command(fireflyPath, "0") + session, err = Start(command, nil, nil) + Ω(err).ShouldNot(HaveOccurred()) + }) + + Describe("when passed something that is an Exiter", func() { + It("should act normally", func() { + failures := InterceptGomegaFailures(func() { + Ω(NeverExits{}).Should(Exit()) + }) + + Ω(failures[0]).Should(ContainSubstring("Expected process to exit. It did not.")) + }) + }) + + Describe("when passed something that is not an Exiter", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Ω("aardvark").Should(Exit()) + }) + + Ω(failures[0]).Should(ContainSubstring("Exit must be passed a gexec.Exiter")) + }) + }) + + Context("with no exit code", func() { + It("should say the right things when it fails", func() { + Ω(session).ShouldNot(Exit()) + + failures := InterceptGomegaFailures(func() { + Ω(session).Should(Exit()) + }) + + Ω(failures[0]).Should(ContainSubstring("Expected process to exit. It did not.")) + + Eventually(session).Should(Exit()) + + Ω(session).Should(Exit()) + + failures = InterceptGomegaFailures(func() { + Ω(session).ShouldNot(Exit()) + }) + + Ω(failures[0]).Should(ContainSubstring("Expected process not to exit. It did.")) + }) + }) + + Context("with an exit code", func() { + It("should say the right things when it fails", func() { + Ω(session).ShouldNot(Exit(0)) + Ω(session).ShouldNot(Exit(1)) + + failures := InterceptGomegaFailures(func() { + Ω(session).Should(Exit(0)) + }) + + Ω(failures[0]).Should(ContainSubstring("Expected process to exit. It did not.")) + + Eventually(session).Should(Exit(0)) + + Ω(session).Should(Exit(0)) + + failures = InterceptGomegaFailures(func() { + Ω(session).Should(Exit(1)) + }) + + Ω(failures[0]).Should(ContainSubstring("to match exit code:")) + + Ω(session).ShouldNot(Exit(1)) + + failures = InterceptGomegaFailures(func() { + Ω(session).ShouldNot(Exit(0)) + }) + + Ω(failures[0]).Should(ContainSubstring("not to match exit code:")) + }) + }) + + Describe("bailing out early", func() { + It("should bail out early once the process exits", func() { + t := time.Now() + + failures := InterceptGomegaFailures(func() { + Eventually(session).Should(Exit(1)) + }) + Ω(time.Since(t)).Should(BeNumerically("<=", 500*time.Millisecond)) + Ω(failures).Should(HaveLen(1)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/gexec_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/gexec_suite_test.go new file mode 100644 index 00000000000..87672aafa39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/gexec_suite_test.go @@ -0,0 +1,26 @@ +package gexec_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + + "testing" +) + +var fireflyPath string + +func TestGexec(t *testing.T) { + BeforeSuite(func() { + var err error + fireflyPath, err = gexec.Build("./_fixture/firefly") + Ω(err).ShouldNot(HaveOccurred()) + }) + + AfterSuite(func() { + gexec.CleanupBuildArtifacts() + }) + + RegisterFailHandler(Fail) + RunSpecs(t, "Gexec Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer.go new file mode 100644 index 00000000000..556182bdec0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer.go @@ -0,0 +1,80 @@ +package gexec + +import ( + "bytes" + "io" + "sync" +) + +/* +PrefixedWriter wraps an io.Writer, emiting the passed in prefix at the beginning of each new line. +This can be useful when running multiple gexec.Sessions concurrently - you can prefix the log output of each +session by passing in a PrefixedWriter: + +gexec.Start(cmd, NewPrefixedWriter("[my-cmd] ", GinkgoWriter), NewPrefixedWriter("[my-cmd] ", GinkgoWriter)) +*/ +type PrefixedWriter struct { + prefix []byte + writer io.Writer + lock *sync.Mutex + isNewLine bool + isFirstWrite bool +} + +func NewPrefixedWriter(prefix string, writer io.Writer) *PrefixedWriter { + return &PrefixedWriter{ + prefix: []byte(prefix), + writer: writer, + lock: &sync.Mutex{}, + isFirstWrite: true, + } +} + +func (w *PrefixedWriter) Write(b []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + + newLine := []byte("\n") + segments := bytes.Split(b, newLine) + + if len(segments) != 0 { + toWrite := []byte{} + if w.isFirstWrite { + toWrite = append(toWrite, w.prefix...) + toWrite = append(toWrite, segments[0]...) + w.isFirstWrite = false + } else if w.isNewLine { + toWrite = append(toWrite, newLine...) + toWrite = append(toWrite, w.prefix...) + toWrite = append(toWrite, segments[0]...) + } else { + toWrite = append(toWrite, segments[0]...) + } + + for i := 1; i < len(segments)-1; i++ { + toWrite = append(toWrite, newLine...) + toWrite = append(toWrite, w.prefix...) + toWrite = append(toWrite, segments[i]...) + } + + if len(segments) > 1 { + lastSegment := segments[len(segments)-1] + + if len(lastSegment) == 0 { + w.isNewLine = true + } else { + toWrite = append(toWrite, newLine...) + toWrite = append(toWrite, w.prefix...) + toWrite = append(toWrite, lastSegment...) + w.isNewLine = false + } + } + + _, err := w.writer.Write(toWrite) + if err != nil { + return 0, err + } + } + + return len(b), nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer_test.go new file mode 100644 index 00000000000..27f7487496e --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/prefixed_writer_test.go @@ -0,0 +1,41 @@ +package gexec_test + +import ( + "bytes" + . "github.com/onsi/gomega/gexec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("PrefixedWriter", func() { + var buffer *bytes.Buffer + var writer *PrefixedWriter + BeforeEach(func() { + buffer = &bytes.Buffer{} + writer = NewPrefixedWriter("[p]", buffer) + }) + + It("should emit the prefix on newlines", func() { + writer.Write([]byte("abc")) + writer.Write([]byte("def\n")) + writer.Write([]byte("hij\n")) + writer.Write([]byte("\n\n")) + writer.Write([]byte("klm\n\nnop")) + writer.Write([]byte("")) + writer.Write([]byte("qrs")) + writer.Write([]byte("\ntuv\nwx")) + writer.Write([]byte("yz\n\n")) + + Ω(buffer.String()).Should(Equal(`[p]abcdef +[p]hij +[p] +[p] +[p]klm +[p] +[p]nopqrs +[p]tuv +[p]wxyz +[p]`)) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session.go new file mode 100644 index 00000000000..460cfe58846 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session.go @@ -0,0 +1,214 @@ +/* +Package gexec provides support for testing external processes. +*/ +package gexec + +import ( + "io" + "os" + "os/exec" + "reflect" + "sync" + "syscall" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +const INVALID_EXIT_CODE = 254 + +type Session struct { + //The wrapped command + Command *exec.Cmd + + //A *gbytes.Buffer connected to the command's stdout + Out *gbytes.Buffer + + //A *gbytes.Buffer connected to the command's stderr + Err *gbytes.Buffer + + //A channel that will close when the command exits + Exited <-chan struct{} + + lock *sync.Mutex + exitCode int +} + +/* +Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session. + +The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err. +These buffers can be used with the gbytes.Say matcher to match against unread output: + + Ω(session.Out).Should(gbytes.Say("foo-out")) + Ω(session.Err).Should(gbytes.Say("foo-err")) + +In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with: + + Ω(session).Should(gbytes.Say("foo-out")) + +When outWriter and/or errWriter are non-nil, the session will pipe stdout and/or stderr output both into the session *gybtes.Buffers and to the passed-in outWriter/errWriter. +This is useful for capturing the process's output or logging it to screen. In particular, when using Ginkgo it can be convenient to direct output to the GinkgoWriter: + + session, err := Start(command, GinkgoWriter, GinkgoWriter) + +This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails. + +The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself. +Instead, to assert that the command has exited you can use the gexec.Exit matcher: + + Ω(session).Should(gexec.Exit()) + +When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any +Eventuallys waiting fo the buffers to Say something. +*/ +func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) { + exited := make(chan struct{}) + + session := &Session{ + Command: command, + Out: gbytes.NewBuffer(), + Err: gbytes.NewBuffer(), + Exited: exited, + lock: &sync.Mutex{}, + exitCode: -1, + } + + var commandOut, commandErr io.Writer + + commandOut, commandErr = session.Out, session.Err + + if outWriter != nil && !reflect.ValueOf(outWriter).IsNil() { + commandOut = io.MultiWriter(commandOut, outWriter) + } + + if errWriter != nil && !reflect.ValueOf(errWriter).IsNil() { + commandErr = io.MultiWriter(commandErr, errWriter) + } + + command.Stdout = commandOut + command.Stderr = commandErr + + err := command.Start() + if err == nil { + go session.monitorForExit(exited) + } + + return session, err +} + +/* +Buffer implements the gbytes.BufferProvider interface and returns s.Out +This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out: + + Eventually(session).Should(gbytes.Say("foo")) +*/ +func (s *Session) Buffer() *gbytes.Buffer { + return s.Out +} + +/* +ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1. + +To assert that the command has exited it is more convenient to use the Exit matcher: + + Eventually(s).Should(gexec.Exit()) + +When the process exits because it has received a particular signal, the exit code will be 128+signal-value +(See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html) + +*/ +func (s *Session) ExitCode() int { + s.lock.Lock() + defer s.lock.Unlock() + return s.exitCode +} + +/* +Wait waits until the wrapped command exits. It can be passed an optional timeout. +If the command does not exit within the timeout, Wait will trigger a test failure. + +Wait returns the session, making it possible to chain: + + session.Wait().Out.Contents() + +will wait for the command to exit then return the entirety of Out's contents. + +Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does. +*/ +func (s *Session) Wait(timeout ...interface{}) *Session { + Eventually(s, timeout...).Should(Exit()) + return s +} + +/* +Kill sends the running command a SIGKILL signal. It does not wait for the process to exit. + +If the command has already exited, Kill returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Kill() *Session { + if s.ExitCode() != -1 { + return s + } + s.Command.Process.Kill() + return s +} + +/* +Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit. + +If the command has already exited, Interrupt returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Interrupt() *Session { + return s.Signal(syscall.SIGINT) +} + +/* +Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit. + +If the command has already exited, Terminate returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Terminate() *Session { + return s.Signal(syscall.SIGTERM) +} + +/* +Terminate sends the running command the passed in signal. It does not wait for the process to exit. + +If the command has already exited, Signal returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Signal(signal os.Signal) *Session { + if s.ExitCode() != -1 { + return s + } + s.Command.Process.Signal(signal) + return s +} + +func (s *Session) monitorForExit(exited chan<- struct{}) { + err := s.Command.Wait() + s.lock.Lock() + s.Out.Close() + s.Err.Close() + status := s.Command.ProcessState.Sys().(syscall.WaitStatus) + if status.Signaled() { + s.exitCode = 128 + int(status.Signal()) + } else { + exitStatus := status.ExitStatus() + if exitStatus == -1 && err != nil { + s.exitCode = INVALID_EXIT_CODE + } + s.exitCode = exitStatus + } + s.lock.Unlock() + + close(exited) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session_test.go new file mode 100644 index 00000000000..cd48e6f4dbe --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gexec/session_test.go @@ -0,0 +1,177 @@ +package gexec_test + +import ( + "os/exec" + "syscall" + "time" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Session", func() { + var command *exec.Cmd + var session *Session + + var outWriter, errWriter *Buffer + + BeforeEach(func() { + outWriter = nil + errWriter = nil + }) + + JustBeforeEach(func() { + command = exec.Command(fireflyPath) + var err error + session, err = Start(command, outWriter, errWriter) + Ω(err).ShouldNot(HaveOccurred()) + }) + + Context("running a command", func() { + It("should start the process", func() { + Ω(command.Process).ShouldNot(BeNil()) + }) + + It("should wrap the process's stdout and stderr with gbytes buffers", func(done Done) { + Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty")) + Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!")) + defer session.Out.CancelDetects() + + select { + case <-session.Out.Detect("Can we maybe vote on the whole murdering people issue"): + Eventually(session).Should(Exit(0)) + case <-session.Out.Detect("I swear by my pretty floral bonnet, I will end you."): + Eventually(session).Should(Exit(1)) + case <-session.Out.Detect("My work's illegal, but at least it's honest."): + Eventually(session).Should(Exit(2)) + } + + close(done) + }) + + It("should satisfy the gbytes.BufferProvider interface, passing Stdout", func() { + Eventually(session).Should(Say("We've done the impossible, and that makes us mighty")) + Eventually(session).Should(Exit()) + }) + }) + + Describe("providing the exit code", func() { + It("should provide the app's exit code", func() { + Ω(session.ExitCode()).Should(Equal(-1)) + + Eventually(session).Should(Exit()) + Ω(session.ExitCode()).Should(BeNumerically(">=", 0)) + Ω(session.ExitCode()).Should(BeNumerically("<", 3)) + }) + }) + + Describe("wait", func() { + It("should wait till the command exits", func() { + Ω(session.ExitCode()).Should(Equal(-1)) + Ω(session.Wait().ExitCode()).Should(BeNumerically(">=", 0)) + Ω(session.Wait().ExitCode()).Should(BeNumerically("<", 3)) + }) + }) + + Describe("exited", func() { + It("should close when the command exits", func() { + Eventually(session.Exited).Should(BeClosed()) + Ω(session.ExitCode()).ShouldNot(Equal(-1)) + }) + }) + + Describe("kill", func() { + It("should kill the command and wait for it to exit", func() { + session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + session.Kill() + Ω(session).ShouldNot(Exit(), "Should not exit immediately...") + Eventually(session).Should(Exit(128 + 9)) + }) + }) + + Describe("interrupt", func() { + It("should interrupt the command", func() { + session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + session.Interrupt() + Ω(session).ShouldNot(Exit(), "Should not exit immediately...") + Eventually(session).Should(Exit(128 + 2)) + }) + }) + + Describe("terminate", func() { + It("should terminate the command", func() { + session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + session.Terminate() + Ω(session).ShouldNot(Exit(), "Should not exit immediately...") + Eventually(session).Should(Exit(128 + 15)) + }) + }) + + Describe("signal", func() { + It("should send the signal to the command", func() { + session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + session.Signal(syscall.SIGABRT) + Ω(session).ShouldNot(Exit(), "Should not exit immediately...") + Eventually(session).Should(Exit(128 + 6)) + }) + }) + + Context("when the command exits", func() { + It("should close the buffers", func() { + Eventually(session).Should(Exit()) + + Ω(session.Out.Closed()).Should(BeTrue()) + Ω(session.Err.Closed()).Should(BeTrue()) + + Ω(session.Out).Should(Say("We've done the impossible, and that makes us mighty")) + }) + + var So = It + + So("this means that eventually should short circuit", func() { + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(session).Should(Say("blah blah blah blah blah")) + }) + Ω(time.Since(t)).Should(BeNumerically("<=", 500*time.Millisecond)) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Context("when wrapping out and err", func() { + BeforeEach(func() { + outWriter = NewBuffer() + errWriter = NewBuffer() + }) + + It("should route to both the provided writers and the gbytes buffers", func() { + Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty")) + Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!")) + + Ω(outWriter.Contents()).Should(ContainSubstring("We've done the impossible, and that makes us mighty")) + Ω(errWriter.Contents()).Should(ContainSubstring("Ah, curse your sudden but inevitable betrayal!")) + + Eventually(session).Should(Exit()) + + Ω(outWriter.Contents()).Should(Equal(session.Out.Contents())) + Ω(errWriter.Contents()).Should(Equal(session.Err.Contents())) + }) + }) + + Describe("when the command fails to start", func() { + It("should return an error", func() { + _, err := Start(exec.Command("agklsjdfas"), nil, nil) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/handlers.go b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/handlers.go new file mode 100644 index 00000000000..d27ad80ce92 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/handlers.go @@ -0,0 +1,202 @@ +package ghttp + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" +) + +//CombineHandler takes variadic list of handlers and produces one handler +//that calls each handler in order. +func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + for _, handler := range handlers { + handler(w, req) + } + } +} + +//VerifyRequest returns a handler that verifies that a request uses the specified method to connect to the specified path +//You may also pass in an optional rawQuery string which is tested against the request's `req.URL.RawQuery` +// +//For path, you may pass in a string, in which case strict equality will be applied +//Alternatively you can pass in a matcher (ContainSubstring("/foo") and MatchRegexp("/foo/[a-f0-9]+") for example) +func VerifyRequest(method string, path interface{}, rawQuery ...string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + Ω(req.Method).Should(Equal(method), "Method mismatch") + switch p := path.(type) { + case types.GomegaMatcher: + Ω(req.URL.Path).Should(p, "Path mismatch") + default: + Ω(req.URL.Path).Should(Equal(path), "Path mismatch") + } + if len(rawQuery) > 0 { + Ω(req.URL.RawQuery).Should(Equal(rawQuery[0]), "RawQuery mismatch") + } + } +} + +//VerifyContentType returns a handler that verifies that a request has a Content-Type header set to the +//specified value +func VerifyContentType(contentType string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + Ω(req.Header.Get("Content-Type")).Should(Equal(contentType)) + } +} + +//VerifyBasicAuth returns a handler that verifies the request contains a BasicAuth Authorization header +//matching the passed in username and password +func VerifyBasicAuth(username string, password string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + auth := req.Header.Get("Authorization") + decoded, err := base64.StdEncoding.DecodeString(auth[6:]) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(string(decoded)).Should(Equal(fmt.Sprintf("%s:%s", username, password)), "Authorization mismatch") + } +} + +//VerifyHeader returns a handler that verifies the request contains the passed in headers. +//The passed in header keys are first canonicalized via http.CanonicalHeaderKey. +// +//The request must contain *all* the passed in headers, but it is allowed to have additional headers +//beyond the passed in set. +func VerifyHeader(header http.Header) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + for key, values := range header { + key = http.CanonicalHeaderKey(key) + Ω(req.Header[key]).Should(Equal(values), "Header mismatch for key: %s", key) + } + } +} + +//VerifyHeaderKV returns a handler that verifies the request contains a header matching the passed in key and values +//(recall that a `http.Header` is a mapping from string (key) to []string (values)) +//It is a convenience wrapper around `VerifyHeader` that allows you to avoid having to create an `http.Header` object. +func VerifyHeaderKV(key string, values ...string) http.HandlerFunc { + return VerifyHeader(http.Header{key: values}) +} + +//VerifyJSON returns a handler that verifies that the body of the request is a valid JSON representation +//matching the passed in JSON string. It does this using Gomega's MatchJSON method +// +//VerifyJSON also verifies that the request's content type is application/json +func VerifyJSON(expectedJSON string) http.HandlerFunc { + return CombineHandlers( + VerifyContentType("application/json"), + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(MatchJSON(expectedJSON), "JSON Mismatch") + }, + ) +} + +//VerifyJSONRepresenting is similar to VerifyJSON. Instead of taking a JSON string, however, it +//takes an arbitrary JSON-encodable object and verifies that the requests's body is a JSON representation +//that matches the object +func VerifyJSONRepresenting(object interface{}) http.HandlerFunc { + data, err := json.Marshal(object) + Ω(err).ShouldNot(HaveOccurred()) + return CombineHandlers( + VerifyContentType("application/json"), + VerifyJSON(string(data)), + ) +} + +func copyHeader(src http.Header, dst http.Header) { + for key, value := range src { + dst[key] = value + } +} + +/* +RespondWith returns a handler that responds to a request with the specified status code and body + +Body may be a string or []byte + +Also, RespondWith can be given an optional http.Header. The headers defined therein will be added to the response headers. +*/ +func RespondWith(statusCode int, body interface{}, optionalHeader ...http.Header) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if len(optionalHeader) == 1 { + copyHeader(optionalHeader[0], w.Header()) + } + w.WriteHeader(statusCode) + switch x := body.(type) { + case string: + w.Write([]byte(x)) + case []byte: + w.Write(x) + default: + Ω(body).Should(BeNil(), "Invalid type for body. Should be string or []byte.") + } + } +} + +/* +RespondWithPtr returns a handler that responds to a request with the specified status code and body + +Unlike RespondWith, you pass RepondWithPtr a pointer to the status code and body allowing different tests +to share the same setup but specify different status codes and bodies. + +Also, RespondWithPtr can be given an optional http.Header. The headers defined therein will be added to the response headers. +Since the http.Header can be mutated after the fact you don't need to pass in a pointer. +*/ +func RespondWithPtr(statusCode *int, body interface{}, optionalHeader ...http.Header) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if len(optionalHeader) == 1 { + copyHeader(optionalHeader[0], w.Header()) + } + w.WriteHeader(*statusCode) + if body != nil { + switch x := (body).(type) { + case *string: + w.Write([]byte(*x)) + case *[]byte: + w.Write(*x) + default: + Ω(body).Should(BeNil(), "Invalid type for body. Should be string or []byte.") + } + } + } +} + +/* +RespondWithJSONEncoded returns a handler that responds to a request with the specified status code and a body +containing the JSON-encoding of the passed in object + +Also, RespondWithJSONEncoded can be given an optional http.Header. The headers defined therein will be added to the response headers. +*/ +func RespondWithJSONEncoded(statusCode int, object interface{}, optionalHeader ...http.Header) http.HandlerFunc { + data, err := json.Marshal(object) + Ω(err).ShouldNot(HaveOccurred()) + return RespondWith(statusCode, string(data), optionalHeader...) +} + +/* +RespondWithJSONEncodedPtr behaves like RespondWithJSONEncoded but takes a pointer +to a status code and object. + +This allows different tests to share the same setup but specify different status codes and JSON-encoded +objects. + +Also, RespondWithJSONEncodedPtr can be given an optional http.Header. The headers defined therein will be added to the response headers. +Since the http.Header can be mutated after the fact you don't need to pass in a pointer. +*/ +func RespondWithJSONEncodedPtr(statusCode *int, object *interface{}, optionalHeader ...http.Header) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + data, err := json.Marshal(*object) + Ω(err).ShouldNot(HaveOccurred()) + if len(optionalHeader) == 1 { + copyHeader(optionalHeader[0], w.Header()) + } + w.WriteHeader(*statusCode) + w.Write(data) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server.go b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server.go new file mode 100644 index 00000000000..07a2bb31377 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server.go @@ -0,0 +1,303 @@ +/* +Package ghttp supports testing HTTP clients by providing a test server (simply a thin wrapper around httptest's server) that supports +registering multiple handlers. Incoming requests are not routed between the different handlers +- rather it is merely the order of the handlers that matters. The first request is handled by the first +registered handler, the second request by the second handler, etc. + +The intent here is to have each handler *verify* that the incoming request is valid. To accomplish, ghttp +also provides a collection of bite-size handlers that each perform one aspect of request verification. These can +be composed together and registered with a ghttp server. The result is an expressive language for describing +the requests generated by the client under test. + +Here's a simple example, note that the server handler is only defined in one BeforeEach and then modified, as required, by the nested BeforeEaches. +A more comprehensive example is available at https://onsi.github.io/gomega/#_testing_http_clients + + var _ = Describe("A Sprockets Client", func() { + var server *ghttp.Server + var client *SprocketClient + BeforeEach(func() { + server = ghttp.NewServer() + client = NewSprocketClient(server.URL(), "skywalker", "tk427") + }) + + AfterEach(func() { + server.Close() + }) + + Describe("fetching sprockets", func() { + var statusCode int + var sprockets []Sprocket + BeforeEach(func() { + statusCode = http.StatusOK + sprockets = []Sprocket{} + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/sprockets"), + ghttp.VerifyBasicAuth("skywalker", "tk427"), + ghttp.RespondWithJSONEncodedPtr(&statusCode, &sprockets), + )) + }) + + Context("when requesting all sprockets", func() { + Context("when the response is succesful", func() { + BeforeEach(func() { + sprockets = []Sprocket{ + NewSprocket("Alfalfa"), + NewSprocket("Banana"), + } + }) + + It("should return the returned sprockets", func() { + Ω(client.Sprockets()).Should(Equal(sprockets)) + }) + }) + + Context("when the response is missing", func() { + BeforeEach(func() { + statusCode = http.StatusNotFound + }) + + It("should return an empty list of sprockets", func() { + Ω(client.Sprockets()).Should(BeEmpty()) + }) + }) + + Context("when the response fails to authenticate", func() { + BeforeEach(func() { + statusCode = http.StatusUnauthorized + }) + + It("should return an AuthenticationError error", func() { + sprockets, err := client.Sprockets() + Ω(sprockets).Should(BeEmpty()) + Ω(err).Should(MatchError(AuthenticationError)) + }) + }) + + Context("when the response is a server failure", func() { + BeforeEach(func() { + statusCode = http.StatusInternalServerError + }) + + It("should return an InternalError error", func() { + sprockets, err := client.Sprockets() + Ω(sprockets).Should(BeEmpty()) + Ω(err).Should(MatchError(InternalError)) + }) + }) + }) + + Context("when requesting some sprockets", func() { + BeforeEach(func() { + sprockets = []Sprocket{ + NewSprocket("Alfalfa"), + NewSprocket("Banana"), + } + + server.WrapHandler(0, ghttp.VerifyRequest("GET", "/sprockets", "filter=FOOD")) + }) + + It("should make the request with a filter", func() { + Ω(client.Sprockets("food")).Should(Equal(sprockets)) + }) + }) + }) + }) +*/ +package ghttp + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "regexp" + "sync" + . "github.com/onsi/gomega" +) + +func new() *Server { + return &Server{ + AllowUnhandledRequests: false, + UnhandledRequestStatusCode: http.StatusInternalServerError, + writeLock: &sync.Mutex{}, + } +} + +type routedHandler struct { + method string + pathRegexp *regexp.Regexp + path string + handler http.HandlerFunc +} + +// NewServer returns a new `*ghttp.Server` that wraps an `httptest` server. The server is started automatically. +func NewServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewServer(s) + return s +} + +// NewTLSServer returns a new `*ghttp.Server` that wraps an `httptest` TLS server. The server is started automatically. +func NewTLSServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewTLSServer(s) + return s +} + +type Server struct { + //The underlying httptest server + HTTPTestServer *httptest.Server + + //Defaults to false. If set to true, the Server will allow more requests than there are registered handlers. + AllowUnhandledRequests bool + + //The status code returned when receiving an unhandled request. + //Defaults to http.StatusInternalServerError. + //Only applies if AllowUnhandledRequests is true + UnhandledRequestStatusCode int + + receivedRequests []*http.Request + requestHandlers []http.HandlerFunc + routedHandlers []routedHandler + + writeLock *sync.Mutex + calls int +} + +//URL() returns a url that will hit the server +func (s *Server) URL() string { + return s.HTTPTestServer.URL +} + +//Close() should be called at the end of each test. It spins down and cleans up the test server. +func (s *Server) Close() { + server := s.HTTPTestServer + s.HTTPTestServer = nil + server.Close() +} + +//ServeHTTP() makes Server an http.Handler +//When the server receives a request it handles the request in the following order: +// +//1. If the request matches a handler registered with RouteToHandler, that handler is called. +//2. Otherwise, if there are handlers registered via AppendHandlers, those handlers are called in order. +//3. If all registered handlers have been called then: +// a) If AllowUnhandledRequests is true, the request will be handled with response code of UnhandledRequestStatusCode +// b) If AllowUnhandledRequests is false, the request will not be handled and the current test will be marked as failed. +func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + defer func() { + recover() + }() + + if routedHandler, ok := s.handlerForRoute(req.Method, req.URL.Path); ok { + routedHandler(w, req) + } else if s.calls < len(s.requestHandlers) { + s.requestHandlers[s.calls](w, req) + s.calls++ + } else { + if s.AllowUnhandledRequests { + ioutil.ReadAll(req.Body) + req.Body.Close() + w.WriteHeader(s.UnhandledRequestStatusCode) + } else { + Ω(req).Should(BeNil(), "Received Unhandled Request") + } + } + s.receivedRequests = append(s.receivedRequests, req) +} + +//ReceivedRequests is an array containing all requests received by the server (both handled and unhandled requests) +func (s *Server) ReceivedRequests() []*http.Request { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + return s.receivedRequests +} + +//RouteToHandler can be used to register handlers that will always handle requests that match +//the passed in method and path. +// +//The path may be either a string object or a *regexp.Regexp. +func (s *Server) RouteToHandler(method string, path interface{}, handler http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + rh := routedHandler{ + method: method, + handler: handler, + } + + switch p := path.(type) { + case *regexp.Regexp: + rh.pathRegexp = p + case string: + rh.path = p + default: + panic("path must be a string or a regular expression") + } + + for i, existingRH := range s.routedHandlers { + if existingRH.method == method && + reflect.DeepEqual(existingRH.pathRegexp, rh.pathRegexp) && + existingRH.path == rh.path { + s.routedHandlers[i] = rh + return + } + } + s.routedHandlers = append(s.routedHandlers, rh) +} + +func (s *Server) handlerForRoute(method string, path string) (http.HandlerFunc, bool) { + for _, rh := range s.routedHandlers { + if rh.method == method { + if rh.pathRegexp != nil { + if rh.pathRegexp.Match([]byte(path)) { + return rh.handler, true + } + } else if rh.path == path { + return rh.handler, true + } + } + } + + return nil, false +} + +//AppendHandlers will appends http.HandlerFuncs to the server's list of registered handlers. The first incoming request is handled by the first handler, the second by the second, etc... +func (s *Server) AppendHandlers(handlers ...http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.requestHandlers = append(s.requestHandlers, handlers...) +} + +//SetHandler overrides the registered handler at the passed in index with the passed in handler +//This is useful, for example, when a server has been set up in a shared context, but must be tweaked +//for a particular test. +func (s *Server) SetHandler(index int, handler http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.requestHandlers[index] = handler +} + +//GetHandler returns the handler registered at the passed in index. +func (s *Server) GetHandler(index int) http.HandlerFunc { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + return s.requestHandlers[index] +} + +//WrapHandler combines the passed in handler with the handler registered at the passed in index. +//This is useful, for example, when a server has been set up in a shared context but must be tweaked +//for a particular test. +// +//If the currently registered handler is A, and the new passed in handler is B then +//WrapHandler will generate a new handler that first calls A, then calls B, and assign it to index +func (s *Server) WrapHandler(index int, handler http.HandlerFunc) { + existingHandler := s.GetHandler(index) + s.SetHandler(index, CombineHandlers(existingHandler, handler)) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_suite_test.go new file mode 100644 index 00000000000..7c123608275 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_suite_test.go @@ -0,0 +1,13 @@ +package ghttp_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGHTTP(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GHTTP Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_test.go new file mode 100644 index 00000000000..d992fb425a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/ghttp/test_server_test.go @@ -0,0 +1,555 @@ +package ghttp_test + +import ( + "bytes" + "io/ioutil" + "net/http" + "regexp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/ghttp" +) + +var _ = Describe("TestServer", func() { + var ( + resp *http.Response + err error + s *Server + ) + + BeforeEach(func() { + s = NewServer() + }) + + AfterEach(func() { + s.Close() + }) + + Describe("allowing unhandled requests", func() { + Context("when true", func() { + BeforeEach(func() { + s.AllowUnhandledRequests = true + s.UnhandledRequestStatusCode = http.StatusForbidden + resp, err = http.Get(s.URL() + "/foo") + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should allow unhandled requests and respond with the passed in status code", func() { + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusForbidden)) + + data, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(data).Should(BeEmpty()) + }) + + It("should record the requests", func() { + Ω(s.ReceivedRequests()).Should(HaveLen(1)) + Ω(s.ReceivedRequests()[0].URL.Path).Should(Equal("/foo")) + }) + }) + + Context("when false", func() { + It("should fail when attempting a request", func() { + failures := InterceptGomegaFailures(func() { + http.Get(s.URL() + "/foo") + }) + + Ω(failures[0]).Should(ContainSubstring("Received Unhandled Request")) + }) + }) + }) + + Describe("Managing Handlers", func() { + var called []string + BeforeEach(func() { + called = []string{} + s.RouteToHandler("GET", "/routed", func(w http.ResponseWriter, req *http.Request) { + called = append(called, "r1") + }) + s.RouteToHandler("POST", regexp.MustCompile(`/routed\d`), func(w http.ResponseWriter, req *http.Request) { + called = append(called, "r2") + }) + s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) { + called = append(called, "A") + }, func(w http.ResponseWriter, req *http.Request) { + called = append(called, "B") + }) + }) + + It("should prefer routed handlers if there is a match", func() { + http.Get(s.URL() + "/routed") + http.Post(s.URL()+"/routed7", "application/json", nil) + http.Get(s.URL() + "/foo") + http.Get(s.URL() + "/routed") + http.Post(s.URL()+"/routed9", "application/json", nil) + http.Get(s.URL() + "/bar") + + failures := InterceptGomegaFailures(func() { + http.Get(s.URL() + "/foo") + http.Get(s.URL() + "/routed/not/a/match") + http.Get(s.URL() + "/routed7") + http.Post(s.URL()+"/routed", "application/json", nil) + }) + + Ω(failures[0]).Should(ContainSubstring("Received Unhandled Request")) + Ω(failures).Should(HaveLen(4)) + + http.Post(s.URL()+"/routed3", "application/json", nil) + + Ω(called).Should(Equal([]string{"r1", "r2", "A", "r1", "r2", "B", "r2"})) + }) + + It("should override routed handlers when reregistered", func() { + s.RouteToHandler("GET", "/routed", func(w http.ResponseWriter, req *http.Request) { + called = append(called, "r3") + }) + s.RouteToHandler("POST", regexp.MustCompile(`/routed\d`), func(w http.ResponseWriter, req *http.Request) { + called = append(called, "r4") + }) + + http.Get(s.URL() + "/routed") + http.Post(s.URL()+"/routed7", "application/json", nil) + + Ω(called).Should(Equal([]string{"r3", "r4"})) + }) + + It("should call the appended handlers, in order, as requests come in", func() { + http.Get(s.URL() + "/foo") + Ω(called).Should(Equal([]string{"A"})) + + http.Get(s.URL() + "/foo") + Ω(called).Should(Equal([]string{"A", "B"})) + + failures := InterceptGomegaFailures(func() { + http.Get(s.URL() + "/foo") + }) + + Ω(failures[0]).Should(ContainSubstring("Received Unhandled Request")) + }) + + Describe("Overwriting an existing handler", func() { + BeforeEach(func() { + s.SetHandler(0, func(w http.ResponseWriter, req *http.Request) { + called = append(called, "C") + }) + }) + + It("should override the specified handler", func() { + http.Get(s.URL() + "/foo") + http.Get(s.URL() + "/foo") + Ω(called).Should(Equal([]string{"C", "B"})) + }) + }) + + Describe("Getting an existing handler", func() { + It("should return the handler func", func() { + s.GetHandler(1)(nil, nil) + Ω(called).Should(Equal([]string{"B"})) + }) + }) + + Describe("Wrapping an existing handler", func() { + BeforeEach(func() { + s.WrapHandler(0, func(w http.ResponseWriter, req *http.Request) { + called = append(called, "C") + }) + }) + + It("should wrap the existing handler in a new handler", func() { + http.Get(s.URL() + "/foo") + http.Get(s.URL() + "/foo") + Ω(called).Should(Equal([]string{"A", "C", "B"})) + }) + }) + }) + + Describe("Request Handlers", func() { + Describe("VerifyRequest", func() { + BeforeEach(func() { + s.AppendHandlers(VerifyRequest("GET", "/foo")) + }) + + It("should verify the method, path", func() { + resp, err = http.Get(s.URL() + "/foo?baz=bar") + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the method, path", func() { + failures := InterceptGomegaFailures(func() { + http.Get(s.URL() + "/foo2") + }) + Ω(failures).Should(HaveLen(1)) + }) + + It("should verify the method, path", func() { + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/foo", "application/json", nil) + }) + Ω(failures).Should(HaveLen(1)) + }) + + Context("when passed a rawQuery", func() { + It("should also be possible to verify the rawQuery", func() { + s.SetHandler(0, VerifyRequest("GET", "/foo", "baz=bar")) + resp, err = http.Get(s.URL() + "/foo?baz=bar") + Ω(err).ShouldNot(HaveOccurred()) + }) + }) + + Context("when passed a matcher for path", func() { + It("should apply the matcher", func() { + s.SetHandler(0, VerifyRequest("GET", MatchRegexp(`/foo/[a-f]*/3`))) + resp, err = http.Get(s.URL() + "/foo/abcdefa/3") + Ω(err).ShouldNot(HaveOccurred()) + }) + }) + }) + + Describe("VerifyContentType", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyContentType("application/octet-stream"), + )) + }) + + It("should verify the content type", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/octet-stream") + + resp, err = http.DefaultClient.Do(req) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the content type", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + + failures := InterceptGomegaFailures(func() { + http.DefaultClient.Do(req) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Describe("Verify BasicAuth", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyBasicAuth("bob", "password"), + )) + }) + + It("should verify basic auth", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.SetBasicAuth("bob", "password") + + resp, err = http.DefaultClient.Do(req) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify basic auth", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.SetBasicAuth("bob", "bassword") + + failures := InterceptGomegaFailures(func() { + http.DefaultClient.Do(req) + }) + Ω(failures).Should(HaveLen(1)) + }) + + }) + + Describe("VerifyHeader", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyHeader(http.Header{ + "accept": []string{"jpeg", "png"}, + "cache-control": []string{"omicron"}, + "Return-Path": []string{"hobbiton"}, + }), + )) + }) + + It("should verify the headers", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Add("Accept", "jpeg") + req.Header.Add("Accept", "png") + req.Header.Add("Cache-Control", "omicron") + req.Header.Add("return-path", "hobbiton") + + resp, err = http.DefaultClient.Do(req) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the headers", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Add("Schmaccept", "jpeg") + req.Header.Add("Schmaccept", "png") + req.Header.Add("Cache-Control", "omicron") + req.Header.Add("return-path", "hobbiton") + + failures := InterceptGomegaFailures(func() { + http.DefaultClient.Do(req) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Describe("VerifyHeaderKV", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/foo"), + VerifyHeaderKV("accept", "jpeg", "png"), + VerifyHeaderKV("cache-control", "omicron"), + VerifyHeaderKV("Return-Path", "hobbiton"), + )) + }) + + It("should verify the headers", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Add("Accept", "jpeg") + req.Header.Add("Accept", "png") + req.Header.Add("Cache-Control", "omicron") + req.Header.Add("return-path", "hobbiton") + + resp, err = http.DefaultClient.Do(req) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the headers", func() { + req, err := http.NewRequest("GET", s.URL()+"/foo", nil) + Ω(err).ShouldNot(HaveOccurred()) + req.Header.Add("Accept", "jpeg") + req.Header.Add("Cache-Control", "omicron") + req.Header.Add("return-path", "hobbiton") + + failures := InterceptGomegaFailures(func() { + http.DefaultClient.Do(req) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Describe("VerifyJSON", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + VerifyJSON(`{"a":3, "b":2}`), + )) + }) + + It("should verify the json body and the content type", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", bytes.NewReader([]byte(`{"b":2, "a":3}`))) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the json body and the content type", func() { + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/foo", "application/json", bytes.NewReader([]byte(`{"b":2, "a":4}`))) + }) + Ω(failures).Should(HaveLen(1)) + }) + + It("should verify the json body and the content type", func() { + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/foo", "application/not-json", bytes.NewReader([]byte(`{"b":2, "a":3}`))) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Describe("VerifyJSONRepresenting", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + VerifyJSONRepresenting([]int{1, 3, 5}), + )) + }) + + It("should verify the json body and the content type", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", bytes.NewReader([]byte(`[1,3,5]`))) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should verify the json body and the content type", func() { + failures := InterceptGomegaFailures(func() { + http.Post(s.URL()+"/foo", "application/json", bytes.NewReader([]byte(`[1,3]`))) + }) + Ω(failures).Should(HaveLen(1)) + }) + }) + + Describe("RespondWith", func() { + Context("without headers", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWith(http.StatusCreated, "sweet"), + ), CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWith(http.StatusOK, []byte("sour")), + )) + }) + + It("should return the response", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(Equal([]byte("sweet"))) + + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusOK)) + + body, err = ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(Equal([]byte("sour"))) + }) + }) + + Context("with headers", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWith(http.StatusCreated, "sweet", http.Header{"X-Custom-Header": []string{"my header"}}), + )) + }) + + It("should return the headers too", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + Ω(ioutil.ReadAll(resp.Body)).Should(Equal([]byte("sweet"))) + Ω(resp.Header.Get("X-Custom-Header")).Should(Equal("my header")) + }) + }) + }) + + Describe("RespondWithPtr", func() { + var code int + var byteBody []byte + var stringBody string + BeforeEach(func() { + code = http.StatusOK + byteBody = []byte("sweet") + stringBody = "sour" + + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithPtr(&code, &byteBody), + ), CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithPtr(&code, &stringBody), + )) + }) + + It("should return the response", func() { + code = http.StatusCreated + byteBody = []byte("tasty") + stringBody = "treat" + + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(Equal([]byte("tasty"))) + + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err = ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(Equal([]byte("treat"))) + }) + + Context("when passed a nil body", func() { + BeforeEach(func() { + s.SetHandler(0, CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithPtr(&code, nil), + )) + }) + + It("should return an empty body and not explode", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp.StatusCode).Should(Equal(http.StatusOK)) + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(BeEmpty()) + + Ω(s.ReceivedRequests()).Should(HaveLen(1)) + }) + }) + }) + + Describe("RespondWithJSON", func() { + BeforeEach(func() { + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncoded(http.StatusCreated, []int{1, 2, 3}), + )) + }) + + It("should return the response", func() { + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(MatchJSON("[1,2,3]")) + }) + }) + + Describe("RespondWithJSONPtr", func() { + var code int + var object interface{} + BeforeEach(func() { + code = http.StatusOK + object = []int{1, 2, 3} + + s.AppendHandlers(CombineHandlers( + VerifyRequest("POST", "/foo"), + RespondWithJSONEncodedPtr(&code, &object), + )) + }) + + It("should return the response", func() { + code = http.StatusCreated + object = []int{4, 5, 6} + resp, err = http.Post(s.URL()+"/foo", "application/json", nil) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(resp.StatusCode).Should(Equal(http.StatusCreated)) + + body, err := ioutil.ReadAll(resp.Body) + Ω(err).ShouldNot(HaveOccurred()) + Ω(body).Should(MatchJSON("[4,5,6]")) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/gomega_dsl.go b/Godeps/_workspace/src/github.com/onsi/gomega/gomega_dsl.go new file mode 100644 index 00000000000..78bd188c072 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/gomega_dsl.go @@ -0,0 +1,335 @@ +/* +Gomega is the Ginkgo BDD-style testing framework's preferred matcher library. + +The godoc documentation describes Gomega's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/gomega/ + +Gomega on Github: http://github.com/onsi/gomega + +Learn more about Ginkgo online: http://onsi.github.io/ginkgo + +Ginkgo on Github: http://github.com/onsi/ginkgo + +Gomega is MIT-Licensed +*/ +package gomega + +import ( + "fmt" + "reflect" + "time" + + "github.com/onsi/gomega/internal/assertion" + "github.com/onsi/gomega/internal/asyncassertion" + "github.com/onsi/gomega/internal/testingtsupport" + "github.com/onsi/gomega/types" +) + +const GOMEGA_VERSION = "1.0" + +const nilFailHandlerPanic = `You are trying to make an assertion, but Gomega's fail handler is nil. +If you're using Ginkgo then you probably forgot to put your assertion in an It(). +Alternatively, you may have forgotten to register a fail handler with RegisterFailHandler() or RegisterTestingT(). +` + +var globalFailHandler types.GomegaFailHandler + +var defaultEventuallyTimeout = time.Second +var defaultEventuallyPollingInterval = 10 * time.Millisecond +var defaultConsistentlyDuration = 100 * time.Millisecond +var defaultConsistentlyPollingInterval = 10 * time.Millisecond + +//RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails +//the fail handler passed into RegisterFailHandler is called. +func RegisterFailHandler(handler types.GomegaFailHandler) { + globalFailHandler = handler +} + +//RegisterTestingT connects Gomega to Golang's XUnit style +//Testing.T tests. You'll need to call this at the top of each XUnit style test: +// +// func TestFarmHasCow(t *testing.T) { +// RegisterTestingT(t) +// +// f := farm.New([]string{"Cow", "Horse"}) +// Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") +// } +// +// Note that this *testing.T is registered *globally* by Gomega (this is why you don't have to +// pass `t` down to the matcher itself). This means that you cannot run the XUnit style tests +// in parallel as the global fail handler cannot point to more than one testing.T at a time. +// +// (As an aside: Ginkgo gets around this limitation by running parallel tests in different *processes*). +func RegisterTestingT(t types.GomegaTestingT) { + RegisterFailHandler(testingtsupport.BuildTestingTGomegaFailHandler(t)) +} + +//InterceptGomegaHandlers runs a given callback and returns an array of +//failure messages generated by any Gomega assertions within the callback. +// +//This is accomplished by temporarily replacing the *global* fail handler +//with a fail handler that simply annotates failures. The original fail handler +//is reset when InterceptGomegaFailures returns. +// +//This is most useful when testing custom matchers, but can also be used to check +//on a value using a Gomega assertion without causing a test failure. +func InterceptGomegaFailures(f func()) []string { + originalHandler := globalFailHandler + failures := []string{} + RegisterFailHandler(func(message string, callerSkip ...int) { + failures = append(failures, message) + }) + f() + RegisterFailHandler(originalHandler) + return failures +} + +//Ω wraps an actual value allowing assertions to be made on it: +// Ω("foo").Should(Equal("foo")) +// +//If Ω is passed more than one argument it will pass the *first* argument to the matcher. +//All subsequent arguments will be required to be nil/zero. +// +//This is convenient if you want to make an assertion on a method/function that returns +//a value and an error - a common patter in Go. +// +//For example, given a function with signature: +// func MyAmazingThing() (int, error) +// +//Then: +// Ω(MyAmazingThing()).Should(Equal(3)) +//Will succeed only if `MyAmazingThing()` returns `(3, nil)` +// +//Ω and Expect are identical +func Ω(actual interface{}, extra ...interface{}) GomegaAssertion { + return ExpectWithOffset(0, actual, extra...) +} + +//Expect wraps an actual value allowing assertions to be made on it: +// Expect("foo").To(Equal("foo")) +// +//If Expect is passed more than one argument it will pass the *first* argument to the matcher. +//All subsequent arguments will be required to be nil/zero. +// +//This is convenient if you want to make an assertion on a method/function that returns +//a value and an error - a common patter in Go. +// +//For example, given a function with signature: +// func MyAmazingThing() (int, error) +// +//Then: +// Expect(MyAmazingThing()).Should(Equal(3)) +//Will succeed only if `MyAmazingThing()` returns `(3, nil)` +// +//Expect and Ω are identical +func Expect(actual interface{}, extra ...interface{}) GomegaAssertion { + return ExpectWithOffset(0, actual, extra...) +} + +//ExpectWithOffset wraps an actual value allowing assertions to be made on it: +// ExpectWithOffset(1, "foo").To(Equal("foo")) +// +//Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument +//this is used to modify the call-stack offset when computing line numbers. +// +//This is most useful in helper functions that make assertions. If you want Gomega's +//error message to refer to the calling line in the test (as opposed to the line in the helper function) +//set the first argument of `ExpectWithOffset` appropriately. +func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) GomegaAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } + return assertion.New(actual, globalFailHandler, offset, extra...) +} + +//Eventually wraps an actual value allowing assertions to be made on it. +//The assertion is tried periodically until it passes or a timeout occurs. +// +//Both the timeout and polling interval are configurable as optional arguments: +//The first optional argument is the timeout +//The second optional argument is the polling interval +// +//Both intervals can either be specified as time.Duration, parsable duration strings or as floats/integers. In the +//last case they are interpreted as seconds. +// +//If Eventually is passed an actual that is a function taking no arguments and returning at least one value, +//then Eventually will call the function periodically and try the matcher against the function's first return value. +// +//Example: +// +// Eventually(func() int { +// return thingImPolling.Count() +// }).Should(BeNumerically(">=", 17)) +// +//Note that this example could be rewritten: +// +// Eventually(thingImPolling.Count).Should(BeNumerically(">=", 17)) +// +//If the function returns more than one value, then Eventually will pass the first value to the matcher and +//assert that all other values are nil/zero. +//This allows you to pass Eventually a function that returns a value and an error - a common pattern in Go. +// +//For example, consider a method that returns a value and an error: +// func FetchFromDB() (string, error) +// +//Then +// Eventually(FetchFromDB).Should(Equal("hasselhoff")) +// +//Will pass only if the the returned error is nil and the returned string passes the matcher. +// +//Eventually's default timeout is 1 second, and its default polling interval is 10ms +func Eventually(actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + return EventuallyWithOffset(0, actual, intervals...) +} + +//EventuallyWithOffset operates like Eventually but takes an additional +//initial argument to indicate an offset in the call stack. This is useful when building helper +//functions that contain matchers. To learn more, read about `ExpectWithOffset`. +func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } + timeoutInterval := defaultEventuallyTimeout + pollingInterval := defaultEventuallyPollingInterval + if len(intervals) > 0 { + timeoutInterval = toDuration(intervals[0]) + } + if len(intervals) > 1 { + pollingInterval = toDuration(intervals[1]) + } + return asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, actual, globalFailHandler, timeoutInterval, pollingInterval, offset) +} + +//Consistently wraps an actual value allowing assertions to be made on it. +//The assertion is tried periodically and is required to pass for a period of time. +// +//Both the total time and polling interval are configurable as optional arguments: +//The first optional argument is the duration that Consistently will run for +//The second optional argument is the polling interval +// +//Both intervals can either be specified as time.Duration, parsable duration strings or as floats/integers. In the +//last case they are interpreted as seconds. +// +//If Consistently is passed an actual that is a function taking no arguments and returning at least one value, +//then Consistently will call the function periodically and try the matcher against the function's first return value. +// +//If the function returns more than one value, then Consistently will pass the first value to the matcher and +//assert that all other values are nil/zero. +//This allows you to pass Consistently a function that returns a value and an error - a common pattern in Go. +// +//Consistently is useful in cases where you want to assert that something *does not happen* over a period of tiem. +//For example, you want to assert that a goroutine does *not* send data down a channel. In this case, you could: +// +// Consistently(channel).ShouldNot(Receive()) +// +//Consistently's default duration is 100ms, and its default polling interval is 10ms +func Consistently(actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + return ConsistentlyWithOffset(0, actual, intervals...) +} + +//ConsistentlyWithOffset operates like Consistnetly but takes an additional +//initial argument to indicate an offset in the call stack. This is useful when building helper +//functions that contain matchers. To learn more, read about `ExpectWithOffset`. +func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) GomegaAsyncAssertion { + if globalFailHandler == nil { + panic(nilFailHandlerPanic) + } + timeoutInterval := defaultConsistentlyDuration + pollingInterval := defaultConsistentlyPollingInterval + if len(intervals) > 0 { + timeoutInterval = toDuration(intervals[0]) + } + if len(intervals) > 1 { + pollingInterval = toDuration(intervals[1]) + } + return asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, actual, globalFailHandler, timeoutInterval, pollingInterval, offset) +} + +//Set the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses. +func SetDefaultEventuallyTimeout(t time.Duration) { + defaultEventuallyTimeout = t +} + +//Set the default polling interval for Eventually. +func SetDefaultEventuallyPollingInterval(t time.Duration) { + defaultEventuallyPollingInterval = t +} + +//Set the default duration for Consistently. Consistently will verify that your condition is satsified for this long. +func SetDefaultConsistentlyDuration(t time.Duration) { + defaultConsistentlyDuration = t +} + +//Set the default polling interval for Consistently. +func SetDefaultConsistentlyPollingInterval(t time.Duration) { + defaultConsistentlyPollingInterval = t +} + +//GomegaAsyncAssertion is returned by Eventually and Consistently and polls the actual value passed into Eventually against +//the matcher passed to the Should and ShouldNot methods. +// +//Both Should and ShouldNot take a variadic optionalDescription argument. This is passed on to +//fmt.Sprintf() and is used to annotate failure messages. This allows you to make your failure messages more +//descriptive +// +//Both Should and ShouldNot return a boolean that is true if the assertion passed and false if it failed. +// +//Example: +// +// Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.") +// Consistently(myChannel).ShouldNot(Receive(), "Nothing should have come down the pipe.") +type GomegaAsyncAssertion interface { + Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool +} + +//GomegaAssertion is returned by Ω and Expect and compares the actual value to the matcher +//passed to the Should/ShouldNot and To/ToNot/NotTo methods. +// +//Typically Should/ShouldNot are used with Ω and To/ToNot/NotTo are used with Expect +//though this is not enforced. +// +//All methods take a variadic optionalDescription argument. This is passed on to fmt.Sprintf() +//and is used to annotate failure messages. +// +//All methods return a bool that is true if hte assertion passed and false if it failed. +// +//Example: +// +// Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm) +type GomegaAssertion interface { + Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool + + To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool + ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool + NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool +} + +//OmegaMatcher is deprecated in favor of the better-named and better-organized types.GomegaMatcher but sticks around to support existing code that uses it +type OmegaMatcher types.GomegaMatcher + +func toDuration(input interface{}) time.Duration { + duration, ok := input.(time.Duration) + if ok { + return duration + } + + value := reflect.ValueOf(input) + kind := reflect.TypeOf(input).Kind() + + if reflect.Int <= kind && kind <= reflect.Int64 { + return time.Duration(value.Int()) * time.Second + } else if reflect.Uint <= kind && kind <= reflect.Uint64 { + return time.Duration(value.Uint()) * time.Second + } else if reflect.Float32 <= kind && kind <= reflect.Float64 { + return time.Duration(value.Float() * float64(time.Second)) + } else if reflect.String == kind { + duration, err := time.ParseDuration(value.String()) + if err != nil { + panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input)) + } + return duration + } + + panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input)) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion.go new file mode 100644 index 00000000000..b73673f21ed --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion.go @@ -0,0 +1,98 @@ +package assertion + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/types" +) + +type Assertion struct { + actualInput interface{} + fail types.GomegaFailHandler + offset int + extra []interface{} +} + +func New(actualInput interface{}, fail types.GomegaFailHandler, offset int, extra ...interface{}) *Assertion { + return &Assertion{ + actualInput: actualInput, + fail: fail, + offset: offset, + extra: extra, + } +} + +func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string { + switch len(optionalDescription) { + case 0: + return "" + default: + return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" + } +} + +func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { + matches, err := matcher.Match(assertion.actualInput) + description := assertion.buildDescription(optionalDescription...) + if err != nil { + assertion.fail(description+err.Error(), 2+assertion.offset) + return false + } + if matches != desiredMatch { + var message string + if desiredMatch { + message = matcher.FailureMessage(assertion.actualInput) + } else { + message = matcher.NegatedFailureMessage(assertion.actualInput) + } + assertion.fail(description+message, 2+assertion.offset) + return false + } + + return true +} + +func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool { + success, message := vetExtras(assertion.extra) + if success { + return true + } + + description := assertion.buildDescription(optionalDescription...) + assertion.fail(description+message, 2+assertion.offset) + return false +} + +func vetExtras(extras []interface{}) (bool, string) { + for i, extra := range extras { + if extra != nil { + zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface() + if !reflect.DeepEqual(zeroValue, extra) { + message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) + return false, message + } + } + } + return true, "" +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_suite_test.go new file mode 100644 index 00000000000..dae47a48bf9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_suite_test.go @@ -0,0 +1,13 @@ +package assertion_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAssertion(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Assertion Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_test.go new file mode 100644 index 00000000000..de9eff2d288 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/assertion/assertion_test.go @@ -0,0 +1,252 @@ +package assertion_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/internal/assertion" + "github.com/onsi/gomega/internal/fakematcher" +) + +var _ = Describe("Assertion", func() { + var ( + a *Assertion + failureMessage string + failureCallerSkip int + matcher *fakematcher.FakeMatcher + ) + + input := "The thing I'm testing" + + var fakeFailHandler = func(message string, callerSkip ...int) { + failureMessage = message + if len(callerSkip) == 1 { + failureCallerSkip = callerSkip[0] + } + } + + BeforeEach(func() { + matcher = &fakematcher.FakeMatcher{} + failureMessage = "" + failureCallerSkip = 0 + a = New(input, fakeFailHandler, 1) + }) + + Context("when called", func() { + It("should pass the provided input value to the matcher", func() { + a.Should(matcher) + + Ω(matcher.ReceivedActual).Should(Equal(input)) + matcher.ReceivedActual = "" + + a.ShouldNot(matcher) + + Ω(matcher.ReceivedActual).Should(Equal(input)) + matcher.ReceivedActual = "" + + a.To(matcher) + + Ω(matcher.ReceivedActual).Should(Equal(input)) + matcher.ReceivedActual = "" + + a.ToNot(matcher) + + Ω(matcher.ReceivedActual).Should(Equal(input)) + matcher.ReceivedActual = "" + + a.NotTo(matcher) + + Ω(matcher.ReceivedActual).Should(Equal(input)) + }) + }) + + Context("when the matcher succeeds", func() { + BeforeEach(func() { + matcher.MatchesToReturn = true + matcher.ErrToReturn = nil + }) + + Context("and a positive assertion is being made", func() { + It("should not call the failure callback", func() { + a.Should(matcher) + Ω(failureMessage).Should(Equal("")) + }) + + It("should be true", func() { + Ω(a.Should(matcher)).Should(BeTrue()) + }) + }) + + Context("and a negative assertion is being made", func() { + It("should call the failure callback", func() { + a.ShouldNot(matcher) + Ω(failureMessage).Should(Equal("negative: The thing I'm testing")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + + It("should be false", func() { + Ω(a.ShouldNot(matcher)).Should(BeFalse()) + }) + }) + }) + + Context("when the matcher fails", func() { + BeforeEach(func() { + matcher.MatchesToReturn = false + matcher.ErrToReturn = nil + }) + + Context("and a positive assertion is being made", func() { + It("should call the failure callback", func() { + a.Should(matcher) + Ω(failureMessage).Should(Equal("positive: The thing I'm testing")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + + It("should be false", func() { + Ω(a.Should(matcher)).Should(BeFalse()) + }) + }) + + Context("and a negative assertion is being made", func() { + It("should not call the failure callback", func() { + a.ShouldNot(matcher) + Ω(failureMessage).Should(Equal("")) + }) + + It("should be true", func() { + Ω(a.ShouldNot(matcher)).Should(BeTrue()) + }) + }) + }) + + Context("When reporting a failure", func() { + BeforeEach(func() { + matcher.MatchesToReturn = false + matcher.ErrToReturn = nil + }) + + Context("and there is an optional description", func() { + It("should append the description to the failure message", func() { + a.Should(matcher, "A description") + Ω(failureMessage).Should(Equal("A description\npositive: The thing I'm testing")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + }) + + Context("and there are multiple arguments to the optional description", func() { + It("should append the formatted description to the failure message", func() { + a.Should(matcher, "A description of [%d]", 3) + Ω(failureMessage).Should(Equal("A description of [3]\npositive: The thing I'm testing")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + }) + }) + + Context("When the matcher returns an error", func() { + BeforeEach(func() { + matcher.ErrToReturn = errors.New("Kaboom!") + }) + + Context("and a positive assertion is being made", func() { + It("should call the failure callback", func() { + matcher.MatchesToReturn = true + a.Should(matcher) + Ω(failureMessage).Should(Equal("Kaboom!")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + }) + + Context("and a negative assertion is being made", func() { + It("should call the failure callback", func() { + matcher.MatchesToReturn = false + a.ShouldNot(matcher) + Ω(failureMessage).Should(Equal("Kaboom!")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + }) + + It("should always be false", func() { + Ω(a.Should(matcher)).Should(BeFalse()) + Ω(a.ShouldNot(matcher)).Should(BeFalse()) + }) + }) + + Context("when there are extra parameters", func() { + It("(a simple example)", func() { + Ω(func() (string, int, error) { + return "foo", 0, nil + }()).Should(Equal("foo")) + }) + + Context("when the parameters are all nil or zero", func() { + It("should invoke the matcher", func() { + matcher.MatchesToReturn = true + matcher.ErrToReturn = nil + + var typedNil []string + a = New(input, fakeFailHandler, 1, 0, nil, typedNil) + + result := a.Should(matcher) + Ω(result).Should(BeTrue()) + Ω(matcher.ReceivedActual).Should(Equal(input)) + + Ω(failureMessage).Should(BeZero()) + }) + }) + + Context("when any of the parameters are not nil or zero", func() { + It("should call the failure callback", func() { + matcher.MatchesToReturn = false + matcher.ErrToReturn = nil + + a = New(input, fakeFailHandler, 1, errors.New("foo")) + result := a.Should(matcher) + Ω(result).Should(BeFalse()) + Ω(matcher.ReceivedActual).Should(BeZero(), "The matcher doesn't even get called") + Ω(failureMessage).Should(ContainSubstring("foo")) + failureMessage = "" + + a = New(input, fakeFailHandler, 1, nil, 1) + result = a.ShouldNot(matcher) + Ω(result).Should(BeFalse()) + Ω(failureMessage).Should(ContainSubstring("1")) + failureMessage = "" + + a = New(input, fakeFailHandler, 1, nil, 0, []string{"foo"}) + result = a.To(matcher) + Ω(result).Should(BeFalse()) + Ω(failureMessage).Should(ContainSubstring("foo")) + failureMessage = "" + + a = New(input, fakeFailHandler, 1, nil, 0, []string{"foo"}) + result = a.ToNot(matcher) + Ω(result).Should(BeFalse()) + Ω(failureMessage).Should(ContainSubstring("foo")) + failureMessage = "" + + a = New(input, fakeFailHandler, 1, nil, 0, []string{"foo"}) + result = a.NotTo(matcher) + Ω(result).Should(BeFalse()) + Ω(failureMessage).Should(ContainSubstring("foo")) + Ω(failureCallerSkip).Should(Equal(3)) + }) + }) + }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occured") + } + }() + + RegisterFailHandler(nil) + Ω(true).Should(BeTrue()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion.go new file mode 100644 index 00000000000..7bbec43b506 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion.go @@ -0,0 +1,197 @@ +package asyncassertion + +import ( + "errors" + "fmt" + "reflect" + "time" + + "github.com/onsi/gomega/types" +) + +type AsyncAssertionType uint + +const ( + AsyncAssertionTypeEventually AsyncAssertionType = iota + AsyncAssertionTypeConsistently +) + +type AsyncAssertion struct { + asyncType AsyncAssertionType + actualInput interface{} + timeoutInterval time.Duration + pollingInterval time.Duration + fail types.GomegaFailHandler + offset int +} + +func New(asyncType AsyncAssertionType, actualInput interface{}, fail types.GomegaFailHandler, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion { + actualType := reflect.TypeOf(actualInput) + if actualType.Kind() == reflect.Func { + if actualType.NumIn() != 0 || actualType.NumOut() == 0 { + panic("Expected a function with no arguments and one or more return values.") + } + } + + return &AsyncAssertion{ + asyncType: asyncType, + actualInput: actualInput, + fail: fail, + timeoutInterval: timeoutInterval, + pollingInterval: pollingInterval, + offset: offset, + } +} + +func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + return assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interface{}) string { + switch len(optionalDescription) { + case 0: + return "" + default: + return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" + } +} + +func (assertion *AsyncAssertion) actualInputIsAFunction() bool { + actualType := reflect.TypeOf(assertion.actualInput) + return actualType.Kind() == reflect.Func && actualType.NumIn() == 0 && actualType.NumOut() > 0 +} + +func (assertion *AsyncAssertion) pollActual() (interface{}, error) { + if assertion.actualInputIsAFunction() { + values := reflect.ValueOf(assertion.actualInput).Call([]reflect.Value{}) + + extras := []interface{}{} + for _, value := range values[1:] { + extras = append(extras, value.Interface()) + } + + success, message := vetExtras(extras) + + if !success { + return nil, errors.New(message) + } + + return values[0].Interface(), nil + } + + return assertion.actualInput, nil +} + +type oracleMatcher interface { + MatchMayChangeInTheFuture(actual interface{}) bool +} + +func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool { + if assertion.actualInputIsAFunction() { + return true + } + + oracleMatcher, ok := matcher.(oracleMatcher) + if !ok { + return true + } + + return oracleMatcher.MatchMayChangeInTheFuture(value) +} + +func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { + timer := time.Now() + timeout := time.After(assertion.timeoutInterval) + + description := assertion.buildDescription(optionalDescription...) + + var matches bool + var err error + mayChange := true + value, err := assertion.pollActual() + if err == nil { + mayChange = assertion.matcherMayChange(matcher, value) + matches, err = matcher.Match(value) + } + + fail := func(preamble string) { + errMsg := "" + message := "" + if err != nil { + errMsg = "Error: " + err.Error() + } else { + if desiredMatch { + message = matcher.FailureMessage(value) + } else { + message = matcher.NegatedFailureMessage(value) + } + } + assertion.fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset) + } + + if assertion.asyncType == AsyncAssertionTypeEventually { + for { + if err == nil && matches == desiredMatch { + return true + } + + if !mayChange { + fail("No future change is possible. Bailing out early") + return false + } + + select { + case <-time.After(assertion.pollingInterval): + value, err = assertion.pollActual() + if err == nil { + mayChange = assertion.matcherMayChange(matcher, value) + matches, err = matcher.Match(value) + } + case <-timeout: + fail("Timed out") + return false + } + } + } else if assertion.asyncType == AsyncAssertionTypeConsistently { + for { + if !(err == nil && matches == desiredMatch) { + fail("Failed") + return false + } + + if !mayChange { + return true + } + + select { + case <-time.After(assertion.pollingInterval): + value, err = assertion.pollActual() + if err == nil { + mayChange = assertion.matcherMayChange(matcher, value) + matches, err = matcher.Match(value) + } + case <-timeout: + return true + } + } + } + + return false +} + +func vetExtras(extras []interface{}) (bool, string) { + for i, extra := range extras { + if extra != nil { + zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface() + if !reflect.DeepEqual(zeroValue, extra) { + message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) + return false, message + } + } + } + return true, "" +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_suite_test.go new file mode 100644 index 00000000000..bdb0c3d2202 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_suite_test.go @@ -0,0 +1,13 @@ +package asyncassertion_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAsyncAssertion(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "AsyncAssertion Suite") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_test.go new file mode 100644 index 00000000000..95e4e823ce2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/asyncassertion/async_assertion_test.go @@ -0,0 +1,350 @@ +package asyncassertion_test + +import ( + "errors" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/internal/asyncassertion" +) + +var _ = Describe("Async Assertion", func() { + var ( + failureMessage string + callerSkip int + ) + + var fakeFailHandler = func(message string, skip ...int) { + failureMessage = message + callerSkip = skip[0] + } + + BeforeEach(func() { + failureMessage = "" + callerSkip = 0 + }) + + Describe("Eventually", func() { + Context("the positive case", func() { + It("should poll the function and matcher", func() { + arr := []int{} + a := New(AsyncAssertionTypeEventually, func() []int { + arr = append(arr, 1) + return arr + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(HaveLen(10)) + + Ω(arr).Should(HaveLen(10)) + Ω(failureMessage).Should(BeZero()) + }) + + It("should continue when the matcher errors", func() { + var arr = []int{} + a := New(AsyncAssertionTypeEventually, func() interface{} { + arr = append(arr, 1) + if len(arr) == 4 { + return 0 //this should cause the matcher to error + } + return arr + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(HaveLen(4), "My description %d", 2) + + Ω(failureMessage).Should(ContainSubstring("Timed out after")) + Ω(failureMessage).Should(ContainSubstring("My description 2")) + Ω(callerSkip).Should(Equal(4)) + }) + + It("should be able to timeout", func() { + arr := []int{} + a := New(AsyncAssertionTypeEventually, func() []int { + arr = append(arr, 1) + return arr + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(HaveLen(11), "My description %d", 2) + + Ω(len(arr)).Should(BeNumerically(">", 8)) + Ω(len(arr)).Should(BeNumerically("<=", 10)) + Ω(failureMessage).Should(ContainSubstring("Timed out after")) + Ω(failureMessage).Should(ContainSubstring("<[]int | len:10"), "Should pass the correct value to the matcher message formatter.") + Ω(failureMessage).Should(ContainSubstring("My description 2")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + + Context("the negative case", func() { + It("should poll the function and matcher", func() { + counter := 0 + arr := []int{} + a := New(AsyncAssertionTypeEventually, func() []int { + counter += 1 + if counter >= 10 { + arr = append(arr, 1) + } + return arr + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(HaveLen(0)) + + Ω(arr).Should(HaveLen(1)) + Ω(failureMessage).Should(BeZero()) + }) + + It("should timeout when the matcher errors", func() { + a := New(AsyncAssertionTypeEventually, func() interface{} { + return 0 //this should cause the matcher to error + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(HaveLen(0), "My description %d", 2) + + Ω(failureMessage).Should(ContainSubstring("Timed out after")) + Ω(failureMessage).Should(ContainSubstring("Error:")) + Ω(failureMessage).Should(ContainSubstring("My description 2")) + Ω(callerSkip).Should(Equal(4)) + }) + + It("should be able to timeout", func() { + a := New(AsyncAssertionTypeEventually, func() []int { + return []int{} + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(HaveLen(0), "My description %d", 2) + + Ω(failureMessage).Should(ContainSubstring("Timed out after")) + Ω(failureMessage).Should(ContainSubstring("<[]int | len:0"), "Should pass the correct value to the matcher message formatter.") + Ω(failureMessage).Should(ContainSubstring("My description 2")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + + Context("with a function that returns multiple values", func() { + It("should eventually succeed if the additional arguments are nil", func() { + i := 0 + Eventually(func() (int, error) { + i++ + return i, nil + }).Should(Equal(10)) + }) + + It("should eventually timeout if the additional arguments are not nil", func() { + i := 0 + a := New(AsyncAssertionTypeEventually, func() (int, error) { + i++ + return i, errors.New("bam") + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + a.Should(Equal(2)) + + Ω(failureMessage).Should(ContainSubstring("Timed out after")) + Ω(failureMessage).Should(ContainSubstring("Error:")) + Ω(failureMessage).Should(ContainSubstring("bam")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occured") + } + }() + + RegisterFailHandler(nil) + c := make(chan bool, 1) + c <- true + Eventually(c).Should(Receive()) + }) + }) + }) + + Describe("Consistently", func() { + Describe("The positive case", func() { + Context("when the matcher consistently passes for the duration", func() { + It("should pass", func() { + calls := 0 + a := New(AsyncAssertionTypeConsistently, func() string { + calls++ + return "foo" + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(Equal("foo")) + Ω(calls).Should(Equal(10)) + Ω(failureMessage).Should(BeZero()) + }) + }) + + Context("when the matcher fails at some point", func() { + It("should fail", func() { + calls := 0 + a := New(AsyncAssertionTypeConsistently, func() interface{} { + calls++ + if calls > 9 { + return "bar" + } + return "foo" + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(Equal("foo")) + Ω(failureMessage).Should(ContainSubstring("to equal")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + + Context("when the matcher errors at some point", func() { + It("should fail", func() { + calls := 0 + a := New(AsyncAssertionTypeConsistently, func() interface{} { + calls++ + if calls > 5 { + return 3 + } + return []int{1, 2, 3} + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.Should(HaveLen(3)) + Ω(failureMessage).Should(ContainSubstring("HaveLen matcher expects")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + }) + + Describe("The negative case", func() { + Context("when the matcher consistently passes for the duration", func() { + It("should pass", func() { + c := make(chan bool) + a := New(AsyncAssertionTypeConsistently, c, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(Receive()) + Ω(failureMessage).Should(BeZero()) + }) + }) + + Context("when the matcher fails at some point", func() { + It("should fail", func() { + c := make(chan bool) + go func() { + time.Sleep(time.Duration(100 * time.Millisecond)) + c <- true + }() + + a := New(AsyncAssertionTypeConsistently, c, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(Receive()) + Ω(failureMessage).Should(ContainSubstring("not to receive anything")) + }) + }) + + Context("when the matcher errors at some point", func() { + It("should fail", func() { + calls := 0 + a := New(AsyncAssertionTypeConsistently, func() interface{} { + calls++ + return calls + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + + a.ShouldNot(BeNumerically(">", 5)) + Ω(failureMessage).Should(ContainSubstring("not to be >")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + }) + + Context("with a function that returns multiple values", func() { + It("should consistently succeed if the additional arguments are nil", func() { + i := 2 + Consistently(func() (int, error) { + i++ + return i, nil + }).Should(BeNumerically(">=", 2)) + }) + + It("should eventually timeout if the additional arguments are not nil", func() { + i := 2 + a := New(AsyncAssertionTypeEventually, func() (int, error) { + i++ + return i, errors.New("bam") + }, fakeFailHandler, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) + a.Should(BeNumerically(">=", 2)) + + Ω(failureMessage).Should(ContainSubstring("Error:")) + Ω(failureMessage).Should(ContainSubstring("bam")) + Ω(callerSkip).Should(Equal(4)) + }) + }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occured") + } + }() + + RegisterFailHandler(nil) + c := make(chan bool) + Consistently(c).ShouldNot(Receive()) + }) + }) + }) + + Context("when passed a function with the wrong # or arguments & returns", func() { + It("should panic", func() { + Ω(func() { + New(AsyncAssertionTypeEventually, func() {}, fakeFailHandler, 0, 0, 1) + }).Should(Panic()) + + Ω(func() { + New(AsyncAssertionTypeEventually, func(a string) int { return 0 }, fakeFailHandler, 0, 0, 1) + }).Should(Panic()) + + Ω(func() { + New(AsyncAssertionTypeEventually, func() int { return 0 }, fakeFailHandler, 0, 0, 1) + }).ShouldNot(Panic()) + + Ω(func() { + New(AsyncAssertionTypeEventually, func() (int, error) { return 0, nil }, fakeFailHandler, 0, 0, 1) + }).ShouldNot(Panic()) + }) + }) + + Describe("bailing early", func() { + Context("when actual is a value", func() { + It("Eventually should bail out and fail early if the matcher says to", func() { + c := make(chan bool) + close(c) + + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(c, 0.1).Should(Receive()) + }) + Ω(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond)) + + Ω(failures).Should(HaveLen(1)) + }) + }) + + Context("when actual is a function", func() { + It("should never bail early", func() { + c := make(chan bool) + close(c) + + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(func() chan bool { + return c + }, 0.1).Should(Receive()) + }) + Ω(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond)) + + Ω(failures).Should(HaveLen(1)) + }) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/fakematcher/fake_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/fakematcher/fake_matcher.go new file mode 100644 index 00000000000..6e351a7de57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/fakematcher/fake_matcher.go @@ -0,0 +1,23 @@ +package fakematcher + +import "fmt" + +type FakeMatcher struct { + ReceivedActual interface{} + MatchesToReturn bool + ErrToReturn error +} + +func (matcher *FakeMatcher) Match(actual interface{}) (bool, error) { + matcher.ReceivedActual = actual + + return matcher.MatchesToReturn, matcher.ErrToReturn +} + +func (matcher *FakeMatcher) FailureMessage(actual interface{}) string { + return fmt.Sprintf("positive: %v", actual) +} + +func (matcher *FakeMatcher) NegatedFailureMessage(actual interface{}) string { + return fmt.Sprintf("negative: %v", actual) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support.go new file mode 100644 index 00000000000..7871fd43953 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support.go @@ -0,0 +1,40 @@ +package testingtsupport + +import ( + "regexp" + "runtime/debug" + "strings" + + "github.com/onsi/gomega/types" +) + +type gomegaTestingT interface { + Errorf(format string, args ...interface{}) +} + +func BuildTestingTGomegaFailHandler(t gomegaTestingT) types.GomegaFailHandler { + return func(message string, callerSkip ...int) { + skip := 1 + if len(callerSkip) > 0 { + skip = callerSkip[0] + } + stackTrace := pruneStack(string(debug.Stack()), skip) + t.Errorf("\n%s\n%s", stackTrace, message) + } +} + +func pruneStack(fullStackTrace string, skip int) string { + stack := strings.Split(fullStackTrace, "\n") + if len(stack) > 2*(skip+1) { + stack = stack[2*(skip+1):] + } + prunedStack := []string{} + re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) + for i := 0; i < len(stack)/2; i++ { + if !re.Match([]byte(stack[i*2])) { + prunedStack = append(prunedStack, stack[i*2]) + prunedStack = append(prunedStack, stack[i*2+1]) + } + } + return strings.Join(prunedStack, "\n") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support_test.go new file mode 100644 index 00000000000..b9fbd6c6404 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/internal/testingtsupport/testing_t_support_test.go @@ -0,0 +1,12 @@ +package testingtsupport_test + +import ( + . "github.com/onsi/gomega" + + "testing" +) + +func TestTestingT(t *testing.T) { + RegisterTestingT(t) + Ω(true).Should(BeTrue()) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers.go new file mode 100644 index 00000000000..708e697a2ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers.go @@ -0,0 +1,313 @@ +package gomega + +import ( + "time" + + "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/types" +) + +//Equal uses reflect.DeepEqual to compare actual with expected. Equal is strict about +//types when performing comparisons. +//It is an error for both actual and expected to be nil. Use BeNil() instead. +func Equal(expected interface{}) types.GomegaMatcher { + return &matchers.EqualMatcher{ + Expected: expected, + } +} + +//BeEquivalentTo is more lax than Equal, allowing equality between different types. +//This is done by converting actual to have the type of expected before +//attempting equality with reflect.DeepEqual. +//It is an error for actual and expected to be nil. Use BeNil() instead. +func BeEquivalentTo(expected interface{}) types.GomegaMatcher { + return &matchers.BeEquivalentToMatcher{ + Expected: expected, + } +} + +//BeNil succeeds if actual is nil +func BeNil() types.GomegaMatcher { + return &matchers.BeNilMatcher{} +} + +//BeTrue succeeds if actual is true +func BeTrue() types.GomegaMatcher { + return &matchers.BeTrueMatcher{} +} + +//BeFalse succeeds if actual is false +func BeFalse() types.GomegaMatcher { + return &matchers.BeFalseMatcher{} +} + +//HaveOccurred succeeds if actual is a non-nil error +//The typical Go error checking pattern looks like: +// err := SomethingThatMightFail() +// Ω(err).ShouldNot(HaveOccurred()) +func HaveOccurred() types.GomegaMatcher { + return &matchers.HaveOccurredMatcher{} +} + +//MatchError succeeds if actual is a non-nil error that matches the passed in string/error. +// +//These are valid use-cases: +// Ω(err).Should(MatchError("an error")) //asserts that err.Error() == "an error" +// Ω(err).Should(MatchError(SomeError)) //asserts that err == SomeError (via reflect.DeepEqual) +// +//It is an error for err to be nil or an object that does not implement the Error interface +func MatchError(expected interface{}) types.GomegaMatcher { + return &matchers.MatchErrorMatcher{ + Expected: expected, + } +} + +//BeClosed succeeds if actual is a closed channel. +//It is an error to pass a non-channel to BeClosed, it is also an error to pass nil +// +//In order to check whether or not the channel is closed, Gomega must try to read from the channel +//(even in the `ShouldNot(BeClosed())` case). You should keep this in mind if you wish to make subsequent assertions about +//values coming down the channel. +// +//Also, if you are testing that a *buffered* channel is closed you must first read all values out of the channel before +//asserting that it is closed (it is not possible to detect that a buffered-channel has been closed until all its buffered values are read). +// +//Finally, as a corollary: it is an error to check whether or not a send-only channel is closed. +func BeClosed() types.GomegaMatcher { + return &matchers.BeClosedMatcher{} +} + +//Receive succeeds if there is a value to be received on actual. +//Actual must be a channel (and cannot be a send-only channel) -- anything else is an error. +// +//Receive returns immediately and never blocks: +// +//- If there is nothing on the channel `c` then Ω(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. +// +//- If the channel `c` is closed then Ω(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. +// +//- If there is something on the channel `c` ready to be read, then Ω(c).Should(Receive()) will pass and Ω(c).ShouldNot(Receive()) will fail. +// +//If you have a go-routine running in the background that will write to channel `c` you can: +// Eventually(c).Should(Receive()) +// +//This will timeout if nothing gets sent to `c` (you can modify the timeout interval as you normally do with `Eventually`) +// +//A similar use-case is to assert that no go-routine writes to a channel (for a period of time). You can do this with `Consistently`: +// Consistently(c).ShouldNot(Receive()) +// +//You can pass `Receive` a matcher. If you do so, it will match the received object against the matcher. For example: +// Ω(c).Should(Receive(Equal("foo"))) +// +//When given a matcher, `Receive` will always fail if there is nothing to be received on the channel. +// +//Passing Receive a matcher is especially useful when paired with Eventually: +// +// Eventually(c).Should(Receive(ContainSubstring("bar"))) +// +//will repeatedly attempt to pull values out of `c` until a value matching "bar" is received. +// +//Finally, if you want to have a reference to the value *sent* to the channel you can pass the `Receive` matcher a pointer to a variable of the appropriate type: +// var myThing thing +// Eventually(thingChan).Should(Receive(&myThing)) +// Ω(myThing.Sprocket).Should(Equal("foo")) +// Ω(myThing.IsValid()).Should(BeTrue()) +func Receive(args ...interface{}) types.GomegaMatcher { + var arg interface{} + if len(args) > 0 { + arg = args[0] + } + + return &matchers.ReceiveMatcher{ + Arg: arg, + } +} + +//BeSent succeeds if a value can be sent to actual. +//Actual must be a channel (and cannot be a receive-only channel) that can sent the type of the value passed into BeSent -- anything else is an error. +//In addition, actual must not be closed. +// +//BeSent never blocks: +// +//- If the channel `c` is not ready to receive then Ω(c).Should(BeSent("foo")) will fail immediately +//- If the channel `c` is eventually ready to receive then Eventually(c).Should(BeSent("foo")) will succeed.. presuming the channel becomes ready to receive before Eventually's timeout +//- If the channel `c` is closed then Ω(c).Should(BeSent("foo")) and Ω(c).ShouldNot(BeSent("foo")) will both fail immediately +// +//Of course, the value is actually sent to the channel. The point of `BeSent` is less to make an assertion about the availability of the channel (which is typically an implementation detail that your test should not be concerned with). +//Rather, the point of `BeSent` is to make it possible to easily and expressively write tests that can timeout on blocked channel sends. +func BeSent(arg interface{}) types.GomegaMatcher { + return &matchers.BeSentMatcher{ + Arg: arg, + } +} + +//MatchRegexp succeeds if actual is a string or stringer that matches the +//passed-in regexp. Optional arguments can be provided to construct a regexp +//via fmt.Sprintf(). +func MatchRegexp(regexp string, args ...interface{}) types.GomegaMatcher { + return &matchers.MatchRegexpMatcher{ + Regexp: regexp, + Args: args, + } +} + +//ContainSubstring succeeds if actual is a string or stringer that contains the +//passed-in regexp. Optional arguments can be provided to construct the substring +//via fmt.Sprintf(). +func ContainSubstring(substr string, args ...interface{}) types.GomegaMatcher { + return &matchers.ContainSubstringMatcher{ + Substr: substr, + Args: args, + } +} + +//HavePrefix succeeds if actual is a string or stringer that contains the +//passed-in string as a prefix. Optional arguments can be provided to construct +//via fmt.Sprintf(). +func HavePrefix(prefix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HavePrefixMatcher{ + Prefix: prefix, + Args: args, + } +} + +//HaveSuffix succeeds if actual is a string or stringer that contains the +//passed-in string as a suffix. Optional arguments can be provided to construct +//via fmt.Sprintf(). +func HaveSuffix(suffix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HaveSuffixMatcher{ + Suffix: suffix, + Args: args, + } +} + +//MatchJSON succeeds if actual is a string or stringer of JSON that matches +//the expected JSON. The JSONs are decoded and the resulting objects are compared via +//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +func MatchJSON(json interface{}) types.GomegaMatcher { + return &matchers.MatchJSONMatcher{ + JSONToMatch: json, + } +} + +//BeEmpty succeeds if actual is empty. Actual must be of type string, array, map, chan, or slice. +func BeEmpty() types.GomegaMatcher { + return &matchers.BeEmptyMatcher{} +} + +//HaveLen succeeds if actual has the passed-in length. Actual must be of type string, array, map, chan, or slice. +func HaveLen(count int) types.GomegaMatcher { + return &matchers.HaveLenMatcher{ + Count: count, + } +} + +//BeZero succeeds if actual is the zero value for its type or if actual is nil. +func BeZero() types.GomegaMatcher { + return &matchers.BeZeroMatcher{} +} + +//ContainElement succeeds if actual contains the passed in element. +//By default ContainElement() uses Equal() to perform the match, however a +//matcher can be passed in instead: +// Ω([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar"))) +// +//Actual must be an array, slice or map. +//For maps, ContainElement searches through the map's values. +func ContainElement(element interface{}) types.GomegaMatcher { + return &matchers.ContainElementMatcher{ + Element: element, + } +} + +//ConsistOf succeeds if actual contains preciely the elements passed into the matcher. The ordering of the elements does not matter. +//By default ConsistOf() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples: +// +// Ω([]string{"Foo", "FooBar"}).Should(ConsistOf("FooBar", "Foo")) +// Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Bar"), "Foo")) +// Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Foo"), ContainSubstring("Foo"))) +// +//Actual must be an array, slice or map. For maps, ConsistOf matches against the map's values. +// +//You typically pass variadic arguments to ConsistOf (as in the examples above). However, if you need to pass in a slice you can provided that it +//is the only element passed in to ConsistOf: +// +// Ω([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"})) +// +//Note that Go's type system does not allow you to write this as ConsistOf([]string{"FooBar", "Foo"}...) as []string and []interface{} are different types - hence the need for this special rule. + +func ConsistOf(elements ...interface{}) types.GomegaMatcher { + return &matchers.ConsistOfMatcher{ + Elements: elements, + } +} + +//HaveKey succeeds if actual is a map with the passed in key. +//By default HaveKey uses Equal() to perform the match, however a +//matcher can be passed in instead: +// Ω(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKey(MatchRegexp(`.+Foo$`))) +func HaveKey(key interface{}) types.GomegaMatcher { + return &matchers.HaveKeyMatcher{ + Key: key, + } +} + +//HaveKeyWithValue succeeds if actual is a map with the passed in key and value. +//By default HaveKeyWithValue uses Equal() to perform the match, however a +//matcher can be passed in instead: +// Ω(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKeyWithValue("Foo", "Bar")) +// Ω(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKeyWithValue(MatchRegexp(`.+Foo$`), "Bar")) +func HaveKeyWithValue(key interface{}, value interface{}) types.GomegaMatcher { + return &matchers.HaveKeyWithValueMatcher{ + Key: key, + Value: value, + } +} + +//BeNumerically performs numerical assertions in a type-agnostic way. +//Actual and expected should be numbers, though the specific type of +//number is irrelevant (floa32, float64, uint8, etc...). +// +//There are six, self-explanatory, supported comparators: +// Ω(1.0).Should(BeNumerically("==", 1)) +// Ω(1.0).Should(BeNumerically("~", 0.999, 0.01)) +// Ω(1.0).Should(BeNumerically(">", 0.9)) +// Ω(1.0).Should(BeNumerically(">=", 1.0)) +// Ω(1.0).Should(BeNumerically("<", 3)) +// Ω(1.0).Should(BeNumerically("<=", 1.0)) +func BeNumerically(comparator string, compareTo ...interface{}) types.GomegaMatcher { + return &matchers.BeNumericallyMatcher{ + Comparator: comparator, + CompareTo: compareTo, + } +} + +//BeTemporally compares time.Time's like BeNumerically +//Actual and expected must be time.Time. The comparators are the same as for BeNumerically +// Ω(time.Now()).Should(BeTemporally(">", time.Time{})) +// Ω(time.Now()).Should(BeTemporally("~", time.Now(), time.Second)) +func BeTemporally(comparator string, compareTo time.Time, threshold ...time.Duration) types.GomegaMatcher { + return &matchers.BeTemporallyMatcher{ + Comparator: comparator, + CompareTo: compareTo, + Threshold: threshold, + } +} + +//BeAssignableToTypeOf succeeds if actual is assignable to the type of expected. +//It will return an error when one of the values is nil. +// Ω(0).Should(BeAssignableToTypeOf(0)) // Same values +// Ω(5).Should(BeAssignableToTypeOf(-1)) // different values same type +// Ω("foo").Should(BeAssignableToTypeOf("bar")) // different values same type +// Ω(struct{ Foo string }{}).Should(BeAssignableToTypeOf(struct{ Foo string }{})) +func BeAssignableToTypeOf(expected interface{}) types.GomegaMatcher { + return &matchers.AssignableToTypeOfMatcher{ + Expected: expected, + } +} + +//Panic succeeds if actual is a function that, when invoked, panics. +//Actual must be a function that takes no arguments and returns no results. +func Panic() types.GomegaMatcher { + return &matchers.PanicMatcher{} +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go new file mode 100644 index 00000000000..7f8897b3c0d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go @@ -0,0 +1,30 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type AssignableToTypeOfMatcher struct { + Expected interface{} +} + +func (matcher *AssignableToTypeOfMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil || matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .") + } + + actualType := reflect.TypeOf(actual) + expectedType := reflect.TypeOf(matcher.Expected) + + return actualType.AssignableTo(expectedType), nil +} + +func (matcher *AssignableToTypeOfMatcher) FailureMessage(actual interface{}) string { + return format.Message(actual, fmt.Sprintf("to be assignable to the type: %T", matcher.Expected)) +} + +func (matcher *AssignableToTypeOfMatcher) NegatedFailureMessage(actual interface{}) string { + return format.Message(actual, fmt.Sprintf("not to be assignable to the type: %T", matcher.Expected)) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher_test.go new file mode 100644 index 00000000000..d2280e0506d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher_test.go @@ -0,0 +1,30 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("AssignableToTypeOf", func() { + Context("When asserting assignability between types", func() { + It("should do the right thing", func() { + Ω(0).Should(BeAssignableToTypeOf(0)) + Ω(5).Should(BeAssignableToTypeOf(-1)) + Ω("foo").Should(BeAssignableToTypeOf("bar")) + Ω(struct{ Foo string }{}).Should(BeAssignableToTypeOf(struct{ Foo string }{})) + + Ω(0).ShouldNot(BeAssignableToTypeOf("bar")) + Ω(5).ShouldNot(BeAssignableToTypeOf(struct{ Foo string }{})) + Ω("foo").ShouldNot(BeAssignableToTypeOf(42)) + }) + }) + + Context("When asserting nil values", func() { + It("should error", func() { + success, err := (&AssignableToTypeOfMatcher{Expected: nil}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher.go new file mode 100644 index 00000000000..c1b499597d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher.go @@ -0,0 +1,45 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type BeClosedMatcher struct { +} + +func (matcher *BeClosedMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("BeClosed matcher expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.SendDir { + return false, fmt.Errorf("BeClosed matcher cannot determine if a send-only channel is closed or open. Got:\n%s", format.Object(actual, 1)) + } + + winnerIndex, _, open := reflect.Select([]reflect.SelectCase{ + reflect.SelectCase{Dir: reflect.SelectRecv, Chan: channelValue}, + reflect.SelectCase{Dir: reflect.SelectDefault}, + }) + + var closed bool + if winnerIndex == 0 { + closed = !open + } else if winnerIndex == 1 { + closed = false + } + + return closed, nil +} + +func (matcher *BeClosedMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be closed") +} + +func (matcher *BeClosedMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be open") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher_test.go new file mode 100644 index 00000000000..b2c40c91039 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_closed_matcher_test.go @@ -0,0 +1,70 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeClosedMatcher", func() { + Context("when passed a channel", func() { + It("should do the right thing", func() { + openChannel := make(chan bool) + Ω(openChannel).ShouldNot(BeClosed()) + + var openReaderChannel <-chan bool + openReaderChannel = openChannel + Ω(openReaderChannel).ShouldNot(BeClosed()) + + closedChannel := make(chan bool) + close(closedChannel) + + Ω(closedChannel).Should(BeClosed()) + + var closedReaderChannel <-chan bool + closedReaderChannel = closedChannel + Ω(closedReaderChannel).Should(BeClosed()) + }) + }) + + Context("when passed a send-only channel", func() { + It("should error", func() { + openChannel := make(chan bool) + var openWriterChannel chan<- bool + openWriterChannel = openChannel + + success, err := (&BeClosedMatcher{}).Match(openWriterChannel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + closedChannel := make(chan bool) + close(closedChannel) + + var closedWriterChannel chan<- bool + closedWriterChannel = closedChannel + + success, err = (&BeClosedMatcher{}).Match(closedWriterChannel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + }) + }) + + Context("when passed something else", func() { + It("should error", func() { + var nilChannel chan bool + + success, err := (&BeClosedMatcher{}).Match(nilChannel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeClosedMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeClosedMatcher{}).Match(7) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher.go new file mode 100644 index 00000000000..55bdd7d15df --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher.go @@ -0,0 +1,26 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type BeEmptyMatcher struct { +} + +func (matcher *BeEmptyMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := lengthOf(actual) + if !ok { + return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == 0, nil +} + +func (matcher *BeEmptyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be empty") +} + +func (matcher *BeEmptyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be empty") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher_test.go new file mode 100644 index 00000000000..541c1b951ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_empty_matcher_test.go @@ -0,0 +1,52 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeEmpty", func() { + Context("when passed a supported type", func() { + It("should do the right thing", func() { + Ω("").Should(BeEmpty()) + Ω(" ").ShouldNot(BeEmpty()) + + Ω([0]int{}).Should(BeEmpty()) + Ω([1]int{1}).ShouldNot(BeEmpty()) + + Ω([]int{}).Should(BeEmpty()) + Ω([]int{1}).ShouldNot(BeEmpty()) + + Ω(map[string]int{}).Should(BeEmpty()) + Ω(map[string]int{"a": 1}).ShouldNot(BeEmpty()) + + c := make(chan bool, 1) + Ω(c).Should(BeEmpty()) + c <- true + Ω(c).ShouldNot(BeEmpty()) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should be true", func() { + var nilSlice []int + Ω(nilSlice).Should(BeEmpty()) + + var nilMap map[int]string + Ω(nilMap).Should(BeEmpty()) + }) + }) + + Context("when passed an unsupported type", func() { + It("should error", func() { + success, err := (&BeEmptyMatcher{}).Match(0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeEmptyMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go new file mode 100644 index 00000000000..32a0c3108a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go @@ -0,0 +1,33 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type BeEquivalentToMatcher struct { + Expected interface{} +} + +func (matcher *BeEquivalentToMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Both actual and expected must not be nil.") + } + + convertedActual := actual + + if actual != nil && matcher.Expected != nil && reflect.TypeOf(actual).ConvertibleTo(reflect.TypeOf(matcher.Expected)) { + convertedActual = reflect.ValueOf(actual).Convert(reflect.TypeOf(matcher.Expected)).Interface() + } + + return reflect.DeepEqual(convertedActual, matcher.Expected), nil +} + +func (matcher *BeEquivalentToMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be equivalent to", matcher.Expected) +} + +func (matcher *BeEquivalentToMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be equivalent to", matcher.Expected) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher_test.go new file mode 100644 index 00000000000..def5104fa75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_equivalent_to_matcher_test.go @@ -0,0 +1,50 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeEquivalentTo", func() { + Context("when asserting that nil is equivalent to nil", func() { + It("should error", func() { + success, err := (&BeEquivalentToMatcher{Expected: nil}).Match(nil) + + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("When asserting on nil", func() { + It("should do the right thing", func() { + Ω("foo").ShouldNot(BeEquivalentTo(nil)) + Ω(nil).ShouldNot(BeEquivalentTo(3)) + Ω([]int{1, 2}).ShouldNot(BeEquivalentTo(nil)) + }) + }) + + Context("When asserting on type aliases", func() { + It("should the right thing", func() { + Ω(StringAlias("foo")).Should(BeEquivalentTo("foo")) + Ω("foo").Should(BeEquivalentTo(StringAlias("foo"))) + Ω(StringAlias("foo")).ShouldNot(BeEquivalentTo("bar")) + Ω("foo").ShouldNot(BeEquivalentTo(StringAlias("bar"))) + }) + }) + + Context("When asserting on numbers", func() { + It("should convert actual to expected and do the right thing", func() { + Ω(5).Should(BeEquivalentTo(5)) + Ω(5.0).Should(BeEquivalentTo(5.0)) + Ω(5).Should(BeEquivalentTo(5.0)) + + Ω(5).ShouldNot(BeEquivalentTo("5")) + Ω(5).ShouldNot(BeEquivalentTo(3)) + + //Here be dragons! + Ω(5.1).Should(BeEquivalentTo(5)) + Ω(5).ShouldNot(BeEquivalentTo(5.1)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher.go new file mode 100644 index 00000000000..0b224cbbc64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher.go @@ -0,0 +1,25 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type BeFalseMatcher struct { +} + +func (matcher *BeFalseMatcher) Match(actual interface{}) (success bool, err error) { + if !isBool(actual) { + return false, fmt.Errorf("Expected a boolean. Got:\n%s", format.Object(actual, 1)) + } + + return actual == false, nil +} + +func (matcher *BeFalseMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be false") +} + +func (matcher *BeFalseMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be false") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher_test.go new file mode 100644 index 00000000000..3965a2c5392 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_false_matcher_test.go @@ -0,0 +1,20 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeFalse", func() { + It("should handle true and false correctly", func() { + Ω(true).ShouldNot(BeFalse()) + Ω(false).Should(BeFalse()) + }) + + It("should only support booleans", func() { + success, err := (&BeFalseMatcher{}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher.go new file mode 100644 index 00000000000..7ee84fe1bcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher.go @@ -0,0 +1,18 @@ +package matchers + +import "github.com/onsi/gomega/format" + +type BeNilMatcher struct { +} + +func (matcher *BeNilMatcher) Match(actual interface{}) (success bool, err error) { + return isNil(actual), nil +} + +func (matcher *BeNilMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be nil") +} + +func (matcher *BeNilMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be nil") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher_test.go new file mode 100644 index 00000000000..75332536329 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_nil_matcher_test.go @@ -0,0 +1,28 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BeNil", func() { + It("should succeed when passed nil", func() { + Ω(nil).Should(BeNil()) + }) + + It("should succeed when passed a typed nil", func() { + var a []int + Ω(a).Should(BeNil()) + }) + + It("should succeed when passing nil pointer", func() { + var f *struct{} + Ω(f).Should(BeNil()) + }) + + It("should not succeed when not passed nil", func() { + Ω(0).ShouldNot(BeNil()) + Ω(false).ShouldNot(BeNil()) + Ω("").ShouldNot(BeNil()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher.go new file mode 100644 index 00000000000..52f83fe3f39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher.go @@ -0,0 +1,119 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "math" +) + +type BeNumericallyMatcher struct { + Comparator string + CompareTo []interface{} +} + +func (matcher *BeNumericallyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be %s", matcher.Comparator), matcher.CompareTo[0]) +} + +func (matcher *BeNumericallyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not to be %s", matcher.Comparator), matcher.CompareTo[0]) +} + +func (matcher *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) { + if len(matcher.CompareTo) == 0 || len(matcher.CompareTo) > 2 { + return false, fmt.Errorf("BeNumerically requires 1 or 2 CompareTo arguments. Got:\n%s", format.Object(matcher.CompareTo, 1)) + } + if !isNumber(actual) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(actual, 1)) + } + if !isNumber(matcher.CompareTo[0]) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1)) + } + if len(matcher.CompareTo) == 2 && !isNumber(matcher.CompareTo[1]) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1)) + } + + switch matcher.Comparator { + case "==", "~", ">", ">=", "<", "<=": + default: + return false, fmt.Errorf("Unknown comparator: %s", matcher.Comparator) + } + + if isFloat(actual) || isFloat(matcher.CompareTo[0]) { + var secondOperand float64 = 1e-8 + if len(matcher.CompareTo) == 2 { + secondOperand = toFloat(matcher.CompareTo[1]) + } + success = matcher.matchFloats(toFloat(actual), toFloat(matcher.CompareTo[0]), secondOperand) + } else if isInteger(actual) { + var secondOperand int64 = 0 + if len(matcher.CompareTo) == 2 { + secondOperand = toInteger(matcher.CompareTo[1]) + } + success = matcher.matchIntegers(toInteger(actual), toInteger(matcher.CompareTo[0]), secondOperand) + } else if isUnsignedInteger(actual) { + var secondOperand uint64 = 0 + if len(matcher.CompareTo) == 2 { + secondOperand = toUnsignedInteger(matcher.CompareTo[1]) + } + success = matcher.matchUnsignedIntegers(toUnsignedInteger(actual), toUnsignedInteger(matcher.CompareTo[0]), secondOperand) + } else { + return false, fmt.Errorf("Failed to compare:\n%s\n%s:\n%s", format.Object(actual, 1), matcher.Comparator, format.Object(matcher.CompareTo[0], 1)) + } + + return success, nil +} + +func (matcher *BeNumericallyMatcher) matchIntegers(actual, compareTo, threshold int64) (success bool) { + switch matcher.Comparator { + case "==", "~": + diff := actual - compareTo + return -threshold <= diff && diff <= threshold + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} + +func (matcher *BeNumericallyMatcher) matchUnsignedIntegers(actual, compareTo, threshold uint64) (success bool) { + switch matcher.Comparator { + case "==", "~": + if actual < compareTo { + actual, compareTo = compareTo, actual + } + return actual-compareTo <= threshold + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} + +func (matcher *BeNumericallyMatcher) matchFloats(actual, compareTo, threshold float64) (success bool) { + switch matcher.Comparator { + case "~": + return math.Abs(actual-compareTo) <= threshold + case "==": + return (actual == compareTo) + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher_test.go new file mode 100644 index 00000000000..43fdb1fe0b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_numerically_matcher_test.go @@ -0,0 +1,148 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeNumerically", func() { + Context("when passed a number", func() { + It("should support ==", func() { + Ω(uint32(5)).Should(BeNumerically("==", 5)) + Ω(float64(5.0)).Should(BeNumerically("==", 5)) + Ω(int8(5)).Should(BeNumerically("==", 5)) + }) + + It("should not have false positives", func() { + Ω(5.1).ShouldNot(BeNumerically("==", 5)) + Ω(5).ShouldNot(BeNumerically("==", 5.1)) + }) + + It("should support >", func() { + Ω(uint32(5)).Should(BeNumerically(">", 4)) + Ω(float64(5.0)).Should(BeNumerically(">", 4.9)) + Ω(int8(5)).Should(BeNumerically(">", 4)) + + Ω(uint32(5)).ShouldNot(BeNumerically(">", 5)) + Ω(float64(5.0)).ShouldNot(BeNumerically(">", 5.0)) + Ω(int8(5)).ShouldNot(BeNumerically(">", 5)) + }) + + It("should support <", func() { + Ω(uint32(5)).Should(BeNumerically("<", 6)) + Ω(float64(5.0)).Should(BeNumerically("<", 5.1)) + Ω(int8(5)).Should(BeNumerically("<", 6)) + + Ω(uint32(5)).ShouldNot(BeNumerically("<", 5)) + Ω(float64(5.0)).ShouldNot(BeNumerically("<", 5.0)) + Ω(int8(5)).ShouldNot(BeNumerically("<", 5)) + }) + + It("should support >=", func() { + Ω(uint32(5)).Should(BeNumerically(">=", 4)) + Ω(float64(5.0)).Should(BeNumerically(">=", 4.9)) + Ω(int8(5)).Should(BeNumerically(">=", 4)) + + Ω(uint32(5)).Should(BeNumerically(">=", 5)) + Ω(float64(5.0)).Should(BeNumerically(">=", 5.0)) + Ω(int8(5)).Should(BeNumerically(">=", 5)) + + Ω(uint32(5)).ShouldNot(BeNumerically(">=", 6)) + Ω(float64(5.0)).ShouldNot(BeNumerically(">=", 5.1)) + Ω(int8(5)).ShouldNot(BeNumerically(">=", 6)) + }) + + It("should support <=", func() { + Ω(uint32(5)).Should(BeNumerically("<=", 6)) + Ω(float64(5.0)).Should(BeNumerically("<=", 5.1)) + Ω(int8(5)).Should(BeNumerically("<=", 6)) + + Ω(uint32(5)).Should(BeNumerically("<=", 5)) + Ω(float64(5.0)).Should(BeNumerically("<=", 5.0)) + Ω(int8(5)).Should(BeNumerically("<=", 5)) + + Ω(uint32(5)).ShouldNot(BeNumerically("<=", 4)) + Ω(float64(5.0)).ShouldNot(BeNumerically("<=", 4.9)) + Ω(int8(5)).Should(BeNumerically("<=", 5)) + }) + + Context("when passed ~", func() { + Context("when passed a float", func() { + Context("and there is no precision parameter", func() { + It("should default to 1e-8", func() { + Ω(5.00000001).Should(BeNumerically("~", 5.00000002)) + Ω(5.00000001).ShouldNot(BeNumerically("~", 5.0000001)) + }) + }) + + Context("and there is a precision parameter", func() { + It("should use the precision parameter", func() { + Ω(5.1).Should(BeNumerically("~", 5.19, 0.1)) + Ω(5.1).Should(BeNumerically("~", 5.01, 0.1)) + Ω(5.1).ShouldNot(BeNumerically("~", 5.22, 0.1)) + Ω(5.1).ShouldNot(BeNumerically("~", 4.98, 0.1)) + }) + }) + }) + + Context("when passed an int/uint", func() { + Context("and there is no precision parameter", func() { + It("should just do strict equality", func() { + Ω(5).Should(BeNumerically("~", 5)) + Ω(5).ShouldNot(BeNumerically("~", 6)) + Ω(uint(5)).ShouldNot(BeNumerically("~", 6)) + }) + }) + + Context("and there is a precision parameter", func() { + It("should use precision paramter", func() { + Ω(5).Should(BeNumerically("~", 6, 2)) + Ω(5).ShouldNot(BeNumerically("~", 8, 2)) + Ω(uint(5)).Should(BeNumerically("~", 6, 1)) + }) + }) + }) + }) + }) + + Context("when passed a non-number", func() { + It("should error", func() { + success, err := (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{5}}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "=="}).Match(5) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "~", CompareTo: []interface{}{3.0, "foo"}}).Match(5.0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{"bar"}}).Match(5) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{"bar"}}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{nil}}).Match(0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{0}}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed an unsupported comparator", func() { + It("should error", func() { + success, err := (&BeNumericallyMatcher{Comparator: "!=", CompareTo: []interface{}{5}}).Match(4) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher.go new file mode 100644 index 00000000000..d7c32233ec4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher.go @@ -0,0 +1,71 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeSentMatcher struct { + Arg interface{} + channelClosed bool +} + +func (matcher *BeSentMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("BeSent expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.RecvDir { + return false, fmt.Errorf("BeSent matcher cannot be passed a receive-only channel. Got:\n%s", format.Object(actual, 1)) + } + + argType := reflect.TypeOf(matcher.Arg) + assignable := argType.AssignableTo(channelType.Elem()) + + if !assignable { + return false, fmt.Errorf("Cannot pass:\n%s to the channel:\n%s\nThe types don't match.", format.Object(matcher.Arg, 1), format.Object(actual, 1)) + } + + argValue := reflect.ValueOf(matcher.Arg) + + defer func() { + if e := recover(); e != nil { + success = false + err = fmt.Errorf("Cannot send to a closed channel") + matcher.channelClosed = true + } + }() + + winnerIndex, _, _ := reflect.Select([]reflect.SelectCase{ + reflect.SelectCase{Dir: reflect.SelectSend, Chan: channelValue, Send: argValue}, + reflect.SelectCase{Dir: reflect.SelectDefault}, + }) + + var didSend bool + if winnerIndex == 0 { + didSend = true + } + + return didSend, nil +} + +func (matcher *BeSentMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to send:", matcher.Arg) +} + +func (matcher *BeSentMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to send:", matcher.Arg) +} + +func (matcher *BeSentMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + if !isChan(actual) { + return false + } + + return !matcher.channelClosed +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher_test.go new file mode 100644 index 00000000000..381c2b40289 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_sent_matcher_test.go @@ -0,0 +1,106 @@ +package matchers_test + +import ( + "time" + . "github.com/onsi/gomega/matchers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BeSent", func() { + Context("when passed a channel and a matching type", func() { + Context("when the channel is ready to receive", func() { + It("should succeed and send the value down the channel", func() { + c := make(chan string) + d := make(chan string) + go func() { + val := <-c + d <- val + }() + + time.Sleep(10 * time.Millisecond) + + Ω(c).Should(BeSent("foo")) + Eventually(d).Should(Receive(Equal("foo"))) + }) + + It("should succeed (with a buffered channel)", func() { + c := make(chan string, 1) + Ω(c).Should(BeSent("foo")) + Ω(<-c).Should(Equal("foo")) + }) + }) + + Context("when the channel is not ready to receive", func() { + It("should fail and not send down the channel", func() { + c := make(chan string) + Ω(c).ShouldNot(BeSent("foo")) + Consistently(c).ShouldNot(Receive()) + }) + }) + + Context("when the channel is eventually ready to receive", func() { + It("should succeed", func() { + c := make(chan string) + d := make(chan string) + go func() { + time.Sleep(30 * time.Millisecond) + val := <-c + d <- val + }() + + Eventually(c).Should(BeSent("foo")) + Eventually(d).Should(Receive(Equal("foo"))) + }) + }) + + Context("when the channel is closed", func() { + It("should error", func() { + c := make(chan string) + close(c) + success, err := (&BeSentMatcher{Arg: "foo"}).Match(c) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + + It("should short-circuit Eventually", func() { + c := make(chan string) + close(c) + + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(c, 10.0).Should(BeSent("foo")) + }) + Ω(failures).Should(HaveLen(1)) + Ω(time.Since(t)).Should(BeNumerically("<", time.Second)) + }) + }) + }) + + Context("when passed a channel and a non-matching type", func() { + It("should error", func() { + success, err := (&BeSentMatcher{Arg: "foo"}).Match(make(chan int, 1)) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed a receive-only channel", func() { + It("should error", func() { + var c <-chan string + c = make(chan string, 1) + success, err := (&BeSentMatcher{Arg: "foo"}).Match(c) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed a nonchannel", func() { + It("should error", func() { + success, err := (&BeSentMatcher{Arg: "foo"}).Match("bar") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher.go new file mode 100644 index 00000000000..abda4eb1e7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher.go @@ -0,0 +1,65 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "time" +) + +type BeTemporallyMatcher struct { + Comparator string + CompareTo time.Time + Threshold []time.Duration +} + +func (matcher *BeTemporallyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be %s", matcher.Comparator), matcher.CompareTo) +} + +func (matcher *BeTemporallyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not to be %s", matcher.Comparator), matcher.CompareTo) +} + +func (matcher *BeTemporallyMatcher) Match(actual interface{}) (bool, error) { + // predicate to test for time.Time type + isTime := func(t interface{}) bool { + _, ok := t.(time.Time) + return ok + } + + if !isTime(actual) { + return false, fmt.Errorf("Expected a time.Time. Got:\n%s", format.Object(actual, 1)) + } + + switch matcher.Comparator { + case "==", "~", ">", ">=", "<", "<=": + default: + return false, fmt.Errorf("Unknown comparator: %s", matcher.Comparator) + } + + var threshold = time.Millisecond + if len(matcher.Threshold) == 1 { + threshold = matcher.Threshold[0] + } + + return matcher.matchTimes(actual.(time.Time), matcher.CompareTo, threshold), nil +} + +func (matcher *BeTemporallyMatcher) matchTimes(actual, compareTo time.Time, threshold time.Duration) (success bool) { + switch matcher.Comparator { + case "==": + return actual.Equal(compareTo) + case "~": + diff := actual.Sub(compareTo) + return -threshold <= diff && diff <= threshold + case ">": + return actual.After(compareTo) + case ">=": + return !actual.Before(compareTo) + case "<": + return actual.Before(compareTo) + case "<=": + return !actual.After(compareTo) + } + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher_test.go new file mode 100644 index 00000000000..feb33e5dc13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_temporally_matcher_test.go @@ -0,0 +1,98 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" + "time" +) + +var _ = Describe("BeTemporally", func() { + + var t0, t1, t2 time.Time + BeforeEach(func() { + t0 = time.Now() + t1 = t0.Add(time.Second) + t2 = t0.Add(-time.Second) + }) + + Context("When comparing times", func() { + + It("should support ==", func() { + Ω(t0).Should(BeTemporally("==", t0)) + Ω(t1).ShouldNot(BeTemporally("==", t0)) + Ω(t0).ShouldNot(BeTemporally("==", t1)) + Ω(t0).ShouldNot(BeTemporally("==", time.Time{})) + }) + + It("should support >", func() { + Ω(t0).Should(BeTemporally(">", t2)) + Ω(t0).ShouldNot(BeTemporally(">", t0)) + Ω(t2).ShouldNot(BeTemporally(">", t0)) + }) + + It("should support <", func() { + Ω(t0).Should(BeTemporally("<", t1)) + Ω(t0).ShouldNot(BeTemporally("<", t0)) + Ω(t1).ShouldNot(BeTemporally("<", t0)) + }) + + It("should support >=", func() { + Ω(t0).Should(BeTemporally(">=", t2)) + Ω(t0).Should(BeTemporally(">=", t0)) + Ω(t0).ShouldNot(BeTemporally(">=", t1)) + }) + + It("should support <=", func() { + Ω(t0).Should(BeTemporally("<=", t1)) + Ω(t0).Should(BeTemporally("<=", t0)) + Ω(t0).ShouldNot(BeTemporally("<=", t2)) + }) + + Context("when passed ~", func() { + Context("and there is no precision parameter", func() { + BeforeEach(func() { + t1 = t0.Add(time.Millisecond / 2) + t2 = t0.Add(-2 * time.Millisecond) + }) + It("should approximate", func() { + Ω(t0).Should(BeTemporally("~", t0)) + Ω(t0).Should(BeTemporally("~", t1)) + Ω(t0).ShouldNot(BeTemporally("~", t2)) + }) + }) + + Context("and there is a precision parameter", func() { + BeforeEach(func() { + t2 = t0.Add(3 * time.Second) + }) + It("should use precision paramter", func() { + d := 2 * time.Second + Ω(t0).Should(BeTemporally("~", t0, d)) + Ω(t0).Should(BeTemporally("~", t1, d)) + Ω(t0).ShouldNot(BeTemporally("~", t2, d)) + }) + }) + }) + }) + + Context("when passed a non-time", func() { + It("should error", func() { + success, err := (&BeTemporallyMatcher{Comparator: "==", CompareTo: t0}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&BeTemporallyMatcher{Comparator: "=="}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed an unsupported comparator", func() { + It("should error", func() { + success, err := (&BeTemporallyMatcher{Comparator: "!=", CompareTo: t0}).Match(t2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher.go new file mode 100644 index 00000000000..1275e5fc9d8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher.go @@ -0,0 +1,25 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type BeTrueMatcher struct { +} + +func (matcher *BeTrueMatcher) Match(actual interface{}) (success bool, err error) { + if !isBool(actual) { + return false, fmt.Errorf("Expected a boolean. Got:\n%s", format.Object(actual, 1)) + } + + return actual.(bool), nil +} + +func (matcher *BeTrueMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be true") +} + +func (matcher *BeTrueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be true") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher_test.go new file mode 100644 index 00000000000..ca32e56beaf --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_true_matcher_test.go @@ -0,0 +1,20 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("BeTrue", func() { + It("should handle true and false correctly", func() { + Ω(true).Should(BeTrue()) + Ω(false).ShouldNot(BeTrue()) + }) + + It("should only support booleans", func() { + success, err := (&BeTrueMatcher{}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher.go new file mode 100644 index 00000000000..b39c9144be7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher.go @@ -0,0 +1,27 @@ +package matchers + +import ( + "github.com/onsi/gomega/format" + "reflect" +) + +type BeZeroMatcher struct { +} + +func (matcher *BeZeroMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return true, nil + } + zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface() + + return reflect.DeepEqual(zeroValue, actual), nil + +} + +func (matcher *BeZeroMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be zero-valued") +} + +func (matcher *BeZeroMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be zero-valued") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher_test.go new file mode 100644 index 00000000000..8ec3643c28a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/be_zero_matcher_test.go @@ -0,0 +1,30 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BeZero", func() { + It("should succeed if the passed in object is the zero value for its type", func() { + Ω(nil).Should(BeZero()) + + Ω("").Should(BeZero()) + Ω(" ").ShouldNot(BeZero()) + + Ω(0).Should(BeZero()) + Ω(1).ShouldNot(BeZero()) + + Ω(0.0).Should(BeZero()) + Ω(0.1).ShouldNot(BeZero()) + + // Ω([]int{}).Should(BeZero()) + Ω([]int{1}).ShouldNot(BeZero()) + + // Ω(map[string]int{}).Should(BeZero()) + Ω(map[string]int{"a": 1}).ShouldNot(BeZero()) + + Ω(myCustomType{}).Should(BeZero()) + Ω(myCustomType{s: "a"}).ShouldNot(BeZero()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of.go new file mode 100644 index 00000000000..7b0e0886842 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of.go @@ -0,0 +1,80 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph" +) + +type ConsistOfMatcher struct { + Elements []interface{} +} + +func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + } + + elements := matcher.Elements + if len(matcher.Elements) == 1 && isArrayOrSlice(matcher.Elements[0]) { + elements = []interface{}{} + value := reflect.ValueOf(matcher.Elements[0]) + for i := 0; i < value.Len(); i++ { + elements = append(elements, value.Index(i).Interface()) + } + } + + matchers := []interface{}{} + for _, element := range elements { + matcher, isMatcher := element.(omegaMatcher) + if !isMatcher { + matcher = &EqualMatcher{Expected: element} + } + matchers = append(matchers, matcher) + } + + values := matcher.valuesOf(actual) + + if len(values) != len(matchers) { + return false, nil + } + + neighbours := func(v, m interface{}) (bool, error) { + match, err := m.(omegaMatcher).Match(v) + return match && err == nil, nil + } + + bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(values, matchers, neighbours) + if err != nil { + return false, err + } + + return len(bipartiteGraph.LargestMatching()) == len(values), nil +} + +func (matcher *ConsistOfMatcher) valuesOf(actual interface{}) []interface{} { + value := reflect.ValueOf(actual) + values := []interface{}{} + if isMap(actual) { + keys := value.MapKeys() + for i := 0; i < value.Len(); i++ { + values = append(values, value.MapIndex(keys[i]).Interface()) + } + } else { + for i := 0; i < value.Len(); i++ { + values = append(values, value.Index(i).Interface()) + } + } + + return values +} + +func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to consist of", matcher.Elements) +} + +func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to consist of", matcher.Elements) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of_test.go new file mode 100644 index 00000000000..0b230e390b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/consist_of_test.go @@ -0,0 +1,75 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ConsistOf", func() { + Context("with a slice", func() { + It("should do the right thing", func() { + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf("foo", "bar", "baz")) + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf("foo", "bar", "baz")) + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf("baz", "bar", "foo")) + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("baz", "bar", "foo", "foo")) + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("baz", "foo")) + }) + }) + + Context("with an array", func() { + It("should do the right thing", func() { + Ω([3]string{"foo", "bar", "baz"}).Should(ConsistOf("foo", "bar", "baz")) + Ω([3]string{"foo", "bar", "baz"}).Should(ConsistOf("baz", "bar", "foo")) + Ω([3]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("baz", "bar", "foo", "foo")) + Ω([3]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("baz", "foo")) + }) + }) + + Context("with a map", func() { + It("should apply to the values", func() { + Ω(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ConsistOf("foo", "bar", "baz")) + Ω(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ConsistOf("baz", "bar", "foo")) + Ω(map[int]string{1: "foo", 2: "bar", 3: "baz"}).ShouldNot(ConsistOf("baz", "bar", "foo", "foo")) + Ω(map[int]string{1: "foo", 2: "bar", 3: "baz"}).ShouldNot(ConsistOf("baz", "foo")) + }) + + }) + + Context("with anything else", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Ω("foo").Should(ConsistOf("f", "o", "o")) + }) + + Ω(failures).Should(HaveLen(1)) + }) + }) + + Context("when passed matchers", func() { + It("should pass if the matchers pass", func() { + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf("foo", MatchRegexp("^ba"), "baz")) + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("foo", MatchRegexp("^ba"))) + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("foo", MatchRegexp("^ba"), MatchRegexp("foo"))) + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf("foo", MatchRegexp("^ba"), MatchRegexp("^ba"))) + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf("foo", MatchRegexp("^ba"), MatchRegexp("turducken"))) + }) + + It("should not depend on the order of the matchers", func() { + Ω([][]int{[]int{1, 2}, []int{2}}).Should(ConsistOf(ContainElement(1), ContainElement(2))) + Ω([][]int{[]int{1, 2}, []int{2}}).Should(ConsistOf(ContainElement(2), ContainElement(1))) + }) + + Context("when a matcher errors", func() { + It("should soldier on", func() { + Ω([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf(BeFalse(), "foo", "bar")) + Ω([]interface{}{"foo", "bar", false}).Should(ConsistOf(BeFalse(), ContainSubstring("foo"), "bar")) + }) + }) + }) + + Context("when passed exactly one argument, and that argument is a slice", func() { + It("should match against the elements of that argument", func() { + Ω([]string{"foo", "bar", "baz"}).Should(ConsistOf([]string{"foo", "bar", "baz"})) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher.go new file mode 100644 index 00000000000..014a20ffb62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher.go @@ -0,0 +1,53 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type ContainElementMatcher struct { + Element interface{} +} + +func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + } + + elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) + if !elementIsMatcher { + elemMatcher = &EqualMatcher{Expected: matcher.Element} + } + + value := reflect.ValueOf(actual) + var keys []reflect.Value + if isMap(actual) { + keys = value.MapKeys() + } + for i := 0; i < value.Len(); i++ { + var success bool + var err error + if isMap(actual) { + success, err = elemMatcher.Match(value.MapIndex(keys[i]).Interface()) + } else { + success, err = elemMatcher.Match(value.Index(i).Interface()) + } + if err != nil { + return false, fmt.Errorf("ContainElement's element matcher failed with:\n\t%s", err.Error()) + } + if success { + return true, nil + } + } + + return false, nil +} + +func (matcher *ContainElementMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain element matching", matcher.Element) +} + +func (matcher *ContainElementMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain element matching", matcher.Element) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher_test.go new file mode 100644 index 00000000000..4d29eeb5bf2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_element_matcher_test.go @@ -0,0 +1,72 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("ContainElement", func() { + Context("when passed a supported type", func() { + Context("and expecting a non-matcher", func() { + It("should do the right thing", func() { + Ω([2]int{1, 2}).Should(ContainElement(2)) + Ω([2]int{1, 2}).ShouldNot(ContainElement(3)) + + Ω([]int{1, 2}).Should(ContainElement(2)) + Ω([]int{1, 2}).ShouldNot(ContainElement(3)) + + Ω(map[string]int{"foo": 1, "bar": 2}).Should(ContainElement(2)) + Ω(map[int]int{3: 1, 4: 2}).ShouldNot(ContainElement(3)) + + arr := make([]myCustomType, 2) + arr[0] = myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}} + arr[1] = myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "c"}} + Ω(arr).Should(ContainElement(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}})) + Ω(arr).ShouldNot(ContainElement(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"b", "c"}})) + }) + }) + + Context("and expecting a matcher", func() { + It("should pass each element through the matcher", func() { + Ω([]int{1, 2, 3}).Should(ContainElement(BeNumerically(">=", 3))) + Ω([]int{1, 2, 3}).ShouldNot(ContainElement(BeNumerically(">", 3))) + Ω(map[string]int{"foo": 1, "bar": 2}).Should(ContainElement(BeNumerically(">=", 2))) + Ω(map[string]int{"foo": 1, "bar": 2}).ShouldNot(ContainElement(BeNumerically(">", 2))) + }) + + It("should fail if the matcher ever fails", func() { + actual := []interface{}{1, 2, "3", 4} + success, err := (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(actual) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilSlice []int + Ω(nilSlice).ShouldNot(ContainElement(1)) + + var nilMap map[int]string + Ω(nilMap).ShouldNot(ContainElement("foo")) + }) + }) + + Context("when passed an unsupported type", func() { + It("should error", func() { + success, err := (&ContainElementMatcher{Element: 0}).Match(0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&ContainElementMatcher{Element: 0}).Match("abc") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&ContainElementMatcher{Element: 0}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher.go new file mode 100644 index 00000000000..2e7608921ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher.go @@ -0,0 +1,37 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "strings" +) + +type ContainSubstringMatcher struct { + Substr string + Args []interface{} +} + +func (matcher *ContainSubstringMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("ContainSubstring matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + + return strings.Contains(actualString, matcher.stringToMatch()), nil +} + +func (matcher *ContainSubstringMatcher) stringToMatch() string { + stringToMatch := matcher.Substr + if len(matcher.Args) > 0 { + stringToMatch = fmt.Sprintf(matcher.Substr, matcher.Args...) + } + return stringToMatch +} + +func (matcher *ContainSubstringMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain substring", matcher.stringToMatch()) +} + +func (matcher *ContainSubstringMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain substring", matcher.stringToMatch()) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher_test.go new file mode 100644 index 00000000000..6935168e5c0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/contain_substring_matcher_test.go @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("ContainSubstringMatcher", func() { + Context("when actual is a string", func() { + It("should match against the string", func() { + Ω("Marvelous").Should(ContainSubstring("rve")) + Ω("Marvelous").ShouldNot(ContainSubstring("boo")) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω("Marvelous3").Should(ContainSubstring("velous%d", 3)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match agains the returned string", func() { + Ω(&myStringer{a: "Abc3"}).Should(ContainSubstring("bc3")) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&ContainSubstringMatcher{Substr: "2"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher.go new file mode 100644 index 00000000000..9f8f80a8973 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher.go @@ -0,0 +1,26 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type EqualMatcher struct { + Expected interface{} +} + +func (matcher *EqualMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .") + } + return reflect.DeepEqual(actual, matcher.Expected), nil +} + +func (matcher *EqualMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to equal", matcher.Expected) +} + +func (matcher *EqualMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal", matcher.Expected) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher_test.go new file mode 100644 index 00000000000..ef0d137dda4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/equal_matcher_test.go @@ -0,0 +1,44 @@ +package matchers_test + +import ( + "errors" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("Equal", func() { + Context("when asserting that nil equals nil", func() { + It("should error", func() { + success, err := (&EqualMatcher{Expected: nil}).Match(nil) + + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("When asserting equality between objects", func() { + It("should do the right thing", func() { + Ω(5).Should(Equal(5)) + Ω(5.0).Should(Equal(5.0)) + + Ω(5).ShouldNot(Equal("5")) + Ω(5).ShouldNot(Equal(5.0)) + Ω(5).ShouldNot(Equal(3)) + + Ω("5").Should(Equal("5")) + Ω([]int{1, 2}).Should(Equal([]int{1, 2})) + Ω([]int{1, 2}).ShouldNot(Equal([]int{2, 1})) + Ω(map[string]string{"a": "b", "c": "d"}).Should(Equal(map[string]string{"a": "b", "c": "d"})) + Ω(map[string]string{"a": "b", "c": "d"}).ShouldNot(Equal(map[string]string{"a": "b", "c": "e"})) + Ω(errors.New("foo")).Should(Equal(errors.New("foo"))) + Ω(errors.New("foo")).ShouldNot(Equal(errors.New("bar"))) + + Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).Should(Equal(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}})) + Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).ShouldNot(Equal(myCustomType{s: "bar", n: 3, f: 2.0, arr: []string{"a", "b"}})) + Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).ShouldNot(Equal(myCustomType{s: "foo", n: 2, f: 2.0, arr: []string{"a", "b"}})) + Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).ShouldNot(Equal(myCustomType{s: "foo", n: 3, f: 3.0, arr: []string{"a", "b"}})) + Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).ShouldNot(Equal(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b", "c"}})) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher.go new file mode 100644 index 00000000000..5701ba6e24c --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher.go @@ -0,0 +1,53 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type HaveKeyMatcher struct { + Key interface{} +} + +func (matcher *HaveKeyMatcher) Match(actual interface{}) (success bool, err error) { + if !isMap(actual) { + return false, fmt.Errorf("HaveKey matcher expects a map. Got:%s", format.Object(actual, 1)) + } + + keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) + if !keyIsMatcher { + keyMatcher = &EqualMatcher{Expected: matcher.Key} + } + + keys := reflect.ValueOf(actual).MapKeys() + for i := 0; i < len(keys); i++ { + success, err := keyMatcher.Match(keys[i].Interface()) + if err != nil { + return false, fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) + } + if success { + return true, nil + } + } + + return false, nil +} + +func (matcher *HaveKeyMatcher) FailureMessage(actual interface{}) (message string) { + switch matcher.Key.(type) { + case omegaMatcher: + return format.Message(actual, "to have key matching", matcher.Key) + default: + return format.Message(actual, "to have key", matcher.Key) + } +} + +func (matcher *HaveKeyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + switch matcher.Key.(type) { + case omegaMatcher: + return format.Message(actual, "not to have key matching", matcher.Key) + default: + return format.Message(actual, "not to have key", matcher.Key) + } +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher_test.go new file mode 100644 index 00000000000..c663e302bac --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_matcher_test.go @@ -0,0 +1,73 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveKey", func() { + var ( + stringKeys map[string]int + intKeys map[int]string + objKeys map[*myCustomType]string + + customA *myCustomType + customB *myCustomType + ) + BeforeEach(func() { + stringKeys = map[string]int{"foo": 2, "bar": 3} + intKeys = map[int]string{2: "foo", 3: "bar"} + + customA = &myCustomType{s: "a", n: 2, f: 2.3, arr: []string{"ice", "cream"}} + customB = &myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"cake"}} + objKeys = map[*myCustomType]string{customA: "aardvark", customB: "kangaroo"} + }) + + Context("when passed a map", func() { + It("should do the right thing", func() { + Ω(stringKeys).Should(HaveKey("foo")) + Ω(stringKeys).ShouldNot(HaveKey("baz")) + + Ω(intKeys).Should(HaveKey(2)) + Ω(intKeys).ShouldNot(HaveKey(4)) + + Ω(objKeys).Should(HaveKey(customA)) + Ω(objKeys).Should(HaveKey(&myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"cake"}})) + Ω(objKeys).ShouldNot(HaveKey(&myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"apple", "pie"}})) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilMap map[int]string + Ω(nilMap).ShouldNot(HaveKey("foo")) + }) + }) + + Context("when the passed in key is actually a matcher", func() { + It("should pass each element through the matcher", func() { + Ω(stringKeys).Should(HaveKey(ContainSubstring("oo"))) + Ω(stringKeys).ShouldNot(HaveKey(ContainSubstring("foobar"))) + }) + + It("should fail if the matcher ever fails", func() { + actual := map[int]string{1: "a", 3: "b", 2: "c"} + success, err := (&HaveKeyMatcher{Key: ContainSubstring("ar")}).Match(actual) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed something that is not a map", func() { + It("should error", func() { + success, err := (&HaveKeyMatcher{Key: "foo"}).Match([]string{"foo"}) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&HaveKeyMatcher{Key: "foo"}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go new file mode 100644 index 00000000000..464ac187e90 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go @@ -0,0 +1,73 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type HaveKeyWithValueMatcher struct { + Key interface{} + Value interface{} +} + +func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool, err error) { + if !isMap(actual) { + return false, fmt.Errorf("HaveKeyWithValue matcher expects a map. Got:%s", format.Object(actual, 1)) + } + + keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) + if !keyIsMatcher { + keyMatcher = &EqualMatcher{Expected: matcher.Key} + } + + valueMatcher, valueIsMatcher := matcher.Value.(omegaMatcher) + if !valueIsMatcher { + valueMatcher = &EqualMatcher{Expected: matcher.Value} + } + + keys := reflect.ValueOf(actual).MapKeys() + for i := 0; i < len(keys); i++ { + success, err := keyMatcher.Match(keys[i].Interface()) + if err != nil { + return false, fmt.Errorf("HaveKeyWithValue's key matcher failed with:\n%s%s", format.Indent, err.Error()) + } + if success { + actualValue := reflect.ValueOf(actual).MapIndex(keys[i]) + success, err := valueMatcher.Match(actualValue.Interface()) + if err != nil { + return false, fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) + } + return success, nil + } + } + + return false, nil +} + +func (matcher *HaveKeyWithValueMatcher) FailureMessage(actual interface{}) (message string) { + str := "to have {key: value}" + if _, ok := matcher.Key.(omegaMatcher); ok { + str += " matching" + } else if _, ok := matcher.Value.(omegaMatcher); ok { + str += " matching" + } + + expect := make(map[interface{}]interface{}, 1) + expect[matcher.Key] = matcher.Value + return format.Message(actual, str, expect) +} + +func (matcher *HaveKeyWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + kStr := "not to have key" + if _, ok := matcher.Key.(omegaMatcher); ok { + kStr = "not to have key matching" + } + + vStr := "or that key's value not be" + if _, ok := matcher.Value.(omegaMatcher); ok { + vStr = "or to have that key's value not matching" + } + + return format.Message(actual, kStr, matcher.Key, vStr, matcher.Value) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher_test.go new file mode 100644 index 00000000000..06a2242aeca --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_key_with_value_matcher_test.go @@ -0,0 +1,82 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveKeyWithValue", func() { + var ( + stringKeys map[string]int + intKeys map[int]string + objKeys map[*myCustomType]*myCustomType + + customA *myCustomType + customB *myCustomType + ) + BeforeEach(func() { + stringKeys = map[string]int{"foo": 2, "bar": 3} + intKeys = map[int]string{2: "foo", 3: "bar"} + + customA = &myCustomType{s: "a", n: 2, f: 2.3, arr: []string{"ice", "cream"}} + customB = &myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"cake"}} + objKeys = map[*myCustomType]*myCustomType{customA: customA, customB: customA} + }) + + Context("when passed a map", func() { + It("should do the right thing", func() { + Ω(stringKeys).Should(HaveKeyWithValue("foo", 2)) + Ω(stringKeys).ShouldNot(HaveKeyWithValue("foo", 1)) + Ω(stringKeys).ShouldNot(HaveKeyWithValue("baz", 2)) + Ω(stringKeys).ShouldNot(HaveKeyWithValue("baz", 1)) + + Ω(intKeys).Should(HaveKeyWithValue(2, "foo")) + Ω(intKeys).ShouldNot(HaveKeyWithValue(4, "foo")) + Ω(intKeys).ShouldNot(HaveKeyWithValue(2, "baz")) + + Ω(objKeys).Should(HaveKeyWithValue(customA, customA)) + Ω(objKeys).Should(HaveKeyWithValue(&myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"cake"}}, &myCustomType{s: "a", n: 2, f: 2.3, arr: []string{"ice", "cream"}})) + Ω(objKeys).ShouldNot(HaveKeyWithValue(&myCustomType{s: "b", n: 4, f: 3.1, arr: []string{"apple", "pie"}}, customA)) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilMap map[int]string + Ω(nilMap).ShouldNot(HaveKeyWithValue("foo", "bar")) + }) + }) + + Context("when the passed in key or value is actually a matcher", func() { + It("should pass each element through the matcher", func() { + Ω(stringKeys).Should(HaveKeyWithValue(ContainSubstring("oo"), 2)) + Ω(intKeys).Should(HaveKeyWithValue(2, ContainSubstring("oo"))) + Ω(stringKeys).ShouldNot(HaveKeyWithValue(ContainSubstring("foobar"), 2)) + }) + + It("should fail if the matcher ever fails", func() { + actual := map[int]string{1: "a", 3: "b", 2: "c"} + success, err := (&HaveKeyWithValueMatcher{Key: ContainSubstring("ar"), Value: 2}).Match(actual) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + otherActual := map[string]int{"a": 1, "b": 2, "c": 3} + success, err = (&HaveKeyWithValueMatcher{Key: "a", Value: ContainSubstring("1")}).Match(otherActual) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed something that is not a map", func() { + It("should error", func() { + success, err := (&HaveKeyWithValueMatcher{Key: "foo", Value: "bar"}).Match([]string{"foo"}) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&HaveKeyWithValueMatcher{Key: "foo", Value: "bar"}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher.go new file mode 100644 index 00000000000..a1837755701 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher.go @@ -0,0 +1,27 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HaveLenMatcher struct { + Count int +} + +func (matcher *HaveLenMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := lengthOf(actual) + if !ok { + return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == matcher.Count, nil +} + +func (matcher *HaveLenMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto have length %d", format.Object(actual, 1), matcher.Count) +} + +func (matcher *HaveLenMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to have length %d", format.Object(actual, 1), matcher.Count) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher_test.go new file mode 100644 index 00000000000..1e6aa69d9d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_len_matcher_test.go @@ -0,0 +1,53 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveLen", func() { + Context("when passed a supported type", func() { + It("should do the right thing", func() { + Ω("").Should(HaveLen(0)) + Ω("AA").Should(HaveLen(2)) + + Ω([0]int{}).Should(HaveLen(0)) + Ω([2]int{1, 2}).Should(HaveLen(2)) + + Ω([]int{}).Should(HaveLen(0)) + Ω([]int{1, 2, 3}).Should(HaveLen(3)) + + Ω(map[string]int{}).Should(HaveLen(0)) + Ω(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}).Should(HaveLen(4)) + + c := make(chan bool, 3) + Ω(c).Should(HaveLen(0)) + c <- true + c <- true + Ω(c).Should(HaveLen(2)) + }) + }) + + Context("when passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilSlice []int + Ω(nilSlice).Should(HaveLen(0)) + + var nilMap map[int]string + Ω(nilMap).Should(HaveLen(0)) + }) + }) + + Context("when passed an unsupported type", func() { + It("should error", func() { + success, err := (&HaveLenMatcher{Count: 0}).Match(0) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&HaveLenMatcher{Count: 0}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher.go new file mode 100644 index 00000000000..b5095f1147b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher.go @@ -0,0 +1,29 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HaveOccurredMatcher struct { +} + +func (matcher *HaveOccurredMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, nil + } + + if isError(actual) { + return true, nil + } + + return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1)) +} + +func (matcher *HaveOccurredMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected an error to have occured. Got:\n%s", format.Object(actual, 1)) +} + +func (matcher *HaveOccurredMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected error:\n%s\n%s\n%s", format.Object(actual, 1), format.IndentString(actual.(error).Error(), 1), "not to have occurred") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher_test.go new file mode 100644 index 00000000000..ef971aa6fd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_occurred_matcher_test.go @@ -0,0 +1,28 @@ +package matchers_test + +import ( + "errors" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveOccurred", func() { + It("should succeed if matching an error", func() { + Ω(errors.New("Foo")).Should(HaveOccurred()) + }) + + It("should not succeed with nil", func() { + Ω(nil).ShouldNot(HaveOccurred()) + }) + + It("should only support errors and nil", func() { + success, err := (&HaveOccurredMatcher{}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&HaveOccurredMatcher{}).Match("") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher.go new file mode 100644 index 00000000000..8b63a89997b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher.go @@ -0,0 +1,35 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HavePrefixMatcher struct { + Prefix string + Args []interface{} +} + +func (matcher *HavePrefixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("HavePrefix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + prefix := matcher.prefix() + return len(actualString) >= len(prefix) && actualString[0:len(prefix)] == prefix, nil +} + +func (matcher *HavePrefixMatcher) prefix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Prefix, matcher.Args...) + } + return matcher.Prefix +} + +func (matcher *HavePrefixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have prefix", matcher.prefix()) +} + +func (matcher *HavePrefixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have prefix", matcher.prefix()) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher_test.go new file mode 100644 index 00000000000..bec3f975827 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_prefix_matcher_test.go @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HavePrefixMatcher", func() { + Context("when actual is a string", func() { + It("should match a string prefix", func() { + Ω("Ab").Should(HavePrefix("A")) + Ω("A").ShouldNot(HavePrefix("Ab")) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω("C3PO").Should(HavePrefix("C%dP", 3)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match against the returned string", func() { + Ω(&myStringer{a: "Ab"}).Should(HavePrefix("A")) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&HavePrefixMatcher{Prefix: "2"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher.go new file mode 100644 index 00000000000..eb1b284da13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher.go @@ -0,0 +1,35 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" +) + +type HaveSuffixMatcher struct { + Suffix string + Args []interface{} +} + +func (matcher *HaveSuffixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("HaveSuffix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + suffix := matcher.suffix() + return len(actualString) >= len(suffix) && actualString[len(actualString) - len(suffix):] == suffix, nil +} + +func (matcher *HaveSuffixMatcher) suffix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Suffix, matcher.Args...) + } + return matcher.Suffix +} + +func (matcher *HaveSuffixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have suffix", matcher.suffix()) +} + +func (matcher *HaveSuffixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have suffix", matcher.suffix()) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher_test.go new file mode 100644 index 00000000000..72e8975bae0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/have_suffix_matcher_test.go @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("HaveSuffixMatcher", func() { + Context("when actual is a string", func() { + It("should match a string suffix", func() { + Ω("Ab").Should(HaveSuffix("b")) + Ω("A").ShouldNot(HaveSuffix("Ab")) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω("C3PO").Should(HaveSuffix("%dPO", 3)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match against the returned string", func() { + Ω(&myStringer{a: "Ab"}).Should(HaveSuffix("b")) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&HaveSuffixMatcher{Suffix: "2"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher.go new file mode 100644 index 00000000000..ad00fef807b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher.go @@ -0,0 +1,41 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type MatchErrorMatcher struct { + Expected interface{} +} + +func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err error) { + if isNil(actual) { + return false, fmt.Errorf("Expected an error, got nil") + } + + if !isError(actual) { + return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1)) + } + + actualErr := actual.(error) + + if isString(matcher.Expected) { + return reflect.DeepEqual(actualErr.Error(), matcher.Expected), nil + } + + if isError(matcher.Expected) { + return reflect.DeepEqual(actualErr, matcher.Expected), nil + } + + return false, fmt.Errorf("MatchError must be passed an error or string. Got:\n%s", format.Object(matcher.Expected, 1)) +} + +func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to match error", matcher.Expected) +} + +func (matcher *MatchErrorMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match error", matcher.Expected) +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher_test.go new file mode 100644 index 00000000000..b331e2f2f7a --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_error_matcher_test.go @@ -0,0 +1,80 @@ +package matchers_test + +import ( + "errors" + "fmt" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +type CustomError struct { +} + +func (c CustomError) Error() string { + return "an error" +} + +var _ = Describe("MatchErrorMatcher", func() { + Context("When asserting against an error", func() { + It("should succeed when matching with an error", func() { + err := errors.New("an error") + fmtErr := fmt.Errorf("an error") + customErr := CustomError{} + + Ω(err).Should(MatchError(errors.New("an error"))) + Ω(err).ShouldNot(MatchError(errors.New("another error"))) + + Ω(fmtErr).Should(MatchError(errors.New("an error"))) + Ω(customErr).Should(MatchError(CustomError{})) + }) + + It("should succeed when matching with a string", func() { + err := errors.New("an error") + fmtErr := fmt.Errorf("an error") + customErr := CustomError{} + + Ω(err).Should(MatchError("an error")) + Ω(err).ShouldNot(MatchError("another error")) + + Ω(fmtErr).Should(MatchError("an error")) + Ω(customErr).Should(MatchError("an error")) + }) + + It("should fail when passed anything else", func() { + actualErr := errors.New("an error") + _, err := (&MatchErrorMatcher{ + Expected: []byte("an error"), + }).Match(actualErr) + Ω(err).Should(HaveOccurred()) + + _, err = (&MatchErrorMatcher{ + Expected: 3, + }).Match(actualErr) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed nil", func() { + It("should fail", func() { + _, err := (&MatchErrorMatcher{ + Expected: "an error", + }).Match(nil) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed a non-error", func() { + It("should fail", func() { + _, err := (&MatchErrorMatcher{ + Expected: "an error", + }).Match("an error") + Ω(err).Should(HaveOccurred()) + + _, err = (&MatchErrorMatcher{ + Expected: "an error", + }).Match(3) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher.go new file mode 100644 index 00000000000..bedf8510268 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher.go @@ -0,0 +1,61 @@ +package matchers + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type MatchJSONMatcher struct { + JSONToMatch interface{} +} + +func (matcher *MatchJSONMatcher) Match(actual interface{}) (success bool, err error) { + actualString, expectedString, err := matcher.prettyPrint(actual) + if err != nil { + return false, err + } + + var aval interface{} + var eval interface{} + + // this is guarded by prettyPrint + json.Unmarshal([]byte(actualString), &aval) + json.Unmarshal([]byte(expectedString), &eval) + + return reflect.DeepEqual(aval, eval), nil +} + +func (matcher *MatchJSONMatcher) FailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.prettyPrint(actual) + return format.Message(actualString, "to match JSON of", expectedString) +} + +func (matcher *MatchJSONMatcher) NegatedFailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.prettyPrint(actual) + return format.Message(actualString, "not to match JSON of", expectedString) +} + +func (matcher *MatchJSONMatcher) prettyPrint(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, aok := toString(actual) + expectedString, eok := toString(matcher.JSONToMatch) + + if !(aok && eok) { + return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + + abuf := new(bytes.Buffer) + ebuf := new(bytes.Buffer) + + if err := json.Indent(abuf, []byte(actualString), "", " "); err != nil { + return "", "", err + } + + if err := json.Indent(ebuf, []byte(expectedString), "", " "); err != nil { + return "", "", err + } + + return actualString, expectedString, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher_test.go new file mode 100644 index 00000000000..c1924baac2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_json_matcher_test.go @@ -0,0 +1,59 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("MatchJSONMatcher", func() { + Context("When passed stringifiables", func() { + It("should succeed if the JSON matches", func() { + Ω("{}").Should(MatchJSON("{}")) + Ω(`{"a":1}`).Should(MatchJSON(`{"a":1}`)) + Ω(`{ + "a":1 + }`).Should(MatchJSON(`{"a":1}`)) + Ω(`{"a":1, "b":2}`).Should(MatchJSON(`{"b":2, "a":1}`)) + Ω(`{"a":1}`).ShouldNot(MatchJSON(`{"b":2, "a":1}`)) + }) + + It("should work with byte arrays", func() { + Ω([]byte("{}")).Should(MatchJSON([]byte("{}"))) + Ω("{}").Should(MatchJSON([]byte("{}"))) + Ω([]byte("{}")).Should(MatchJSON("{}")) + }) + }) + + Context("when either side is not valid JSON", func() { + It("should error", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: `oops`}).Match(`{}`) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&MatchJSONMatcher{JSONToMatch: `{}`}).Match(`oops`) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when either side is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&MatchJSONMatcher{JSONToMatch: "{}"}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match("{}") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&MatchJSONMatcher{JSONToMatch: nil}).Match("{}") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&MatchJSONMatcher{JSONToMatch: 2}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher.go new file mode 100644 index 00000000000..7ca79a15be2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "regexp" +) + +type MatchRegexpMatcher struct { + Regexp string + Args []interface{} +} + +func (matcher *MatchRegexpMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("RegExp matcher requires a string or stringer.\nGot:%s", format.Object(actual, 1)) + } + + match, err := regexp.Match(matcher.regexp(), []byte(actualString)) + if err != nil { + return false, fmt.Errorf("RegExp match failed to compile with error:\n\t%s", err.Error()) + } + + return match, nil +} + +func (matcher *MatchRegexpMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to match regular expression", matcher.regexp()) +} + +func (matcher *MatchRegexpMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match regular expression", matcher.regexp()) +} + +func (matcher *MatchRegexpMatcher) regexp() string { + re := matcher.Regexp + if len(matcher.Args) > 0 { + re = fmt.Sprintf(matcher.Regexp, matcher.Args...) + } + return re +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher_test.go new file mode 100644 index 00000000000..bb521cce347 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/match_regexp_matcher_test.go @@ -0,0 +1,44 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("MatchRegexp", func() { + Context("when actual is a string", func() { + It("should match against the string", func() { + Ω(" a2!bla").Should(MatchRegexp(`\d!`)) + Ω(" a2!bla").ShouldNot(MatchRegexp(`[A-Z]`)) + }) + }) + + Context("when actual is a stringer", func() { + It("should call the stringer and match agains the returned string", func() { + Ω(&myStringer{a: "Abc3"}).Should(MatchRegexp(`[A-Z][a-z]+\d`)) + }) + }) + + Context("when the matcher is called with multiple arguments", func() { + It("should pass the string and arguments to sprintf", func() { + Ω(" a23!bla").Should(MatchRegexp(`\d%d!`, 3)) + }) + }) + + Context("when actual is neither a string nor a stringer", func() { + It("should error", func() { + success, err := (&MatchRegexpMatcher{Regexp: `\d`}).Match(2) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when the passed in regexp fails to compile", func() { + It("should error", func() { + success, err := (&MatchRegexpMatcher{Regexp: "("}).Match("Foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/matcher_tests_suite_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/matcher_tests_suite_test.go new file mode 100644 index 00000000000..4bc6d9d0c27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/matcher_tests_suite_test.go @@ -0,0 +1,29 @@ +package matchers_test + +import ( + "testing" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type myStringer struct { + a string +} + +func (s *myStringer) String() string { + return s.a +} + +type StringAlias string + +type myCustomType struct { + s string + n int + f float32 + arr []string +} + +func Test(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gomega") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher.go new file mode 100644 index 00000000000..75ab251bce9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "reflect" +) + +type PanicMatcher struct{} + +func (matcher *PanicMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, fmt.Errorf("PanicMatcher expects a non-nil actual.") + } + + actualType := reflect.TypeOf(actual) + if actualType.Kind() != reflect.Func { + return false, fmt.Errorf("PanicMatcher expects a function. Got:\n%s", format.Object(actual, 1)) + } + if !(actualType.NumIn() == 0 && actualType.NumOut() == 0) { + return false, fmt.Errorf("PanicMatcher expects a function with no arguments and no return value. Got:\n%s", format.Object(actual, 1)) + } + + success = false + defer func() { + if e := recover(); e != nil { + success = true + } + }() + + reflect.ValueOf(actual).Call([]reflect.Value{}) + + return +} + +func (matcher *PanicMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to panic") +} + +func (matcher *PanicMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to panic") +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher_test.go new file mode 100644 index 00000000000..17f3935e64b --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/panic_matcher_test.go @@ -0,0 +1,36 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +var _ = Describe("Panic", func() { + Context("when passed something that's not a function that takes zero arguments and returns nothing", func() { + It("should error", func() { + success, err := (&PanicMatcher{}).Match("foo") + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&PanicMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&PanicMatcher{}).Match(func(foo string) {}) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&PanicMatcher{}).Match(func() string { return "bar" }) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when passed a function of the correct type", func() { + It("should call the function and pass if the function panics", func() { + Ω(func() { panic("ack!") }).Should(Panic()) + Ω(func() {}).ShouldNot(Panic()) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher.go new file mode 100644 index 00000000000..7a8c2cda519 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher.go @@ -0,0 +1,126 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type ReceiveMatcher struct { + Arg interface{} + receivedValue reflect.Value + channelClosed bool +} + +func (matcher *ReceiveMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("ReceiveMatcher expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.SendDir { + return false, fmt.Errorf("ReceiveMatcher matcher cannot be passed a send-only channel. Got:\n%s", format.Object(actual, 1)) + } + + var subMatcher omegaMatcher + var hasSubMatcher bool + + if matcher.Arg != nil { + subMatcher, hasSubMatcher = (matcher.Arg).(omegaMatcher) + if !hasSubMatcher { + argType := reflect.TypeOf(matcher.Arg) + if argType.Kind() != reflect.Ptr { + return false, fmt.Errorf("Cannot assign a value from the channel:\n%s\nTo:\n%s\nYou need to pass a pointer!", format.Object(actual, 1), format.Object(matcher.Arg, 1)) + } + + assignable := channelType.Elem().AssignableTo(argType.Elem()) + if !assignable { + return false, fmt.Errorf("Cannot assign a value from the channel:\n%s\nTo:\n%s", format.Object(actual, 1), format.Object(matcher.Arg, 1)) + } + } + } + + winnerIndex, value, open := reflect.Select([]reflect.SelectCase{ + reflect.SelectCase{Dir: reflect.SelectRecv, Chan: channelValue}, + reflect.SelectCase{Dir: reflect.SelectDefault}, + }) + + var closed bool + var didReceive bool + if winnerIndex == 0 { + closed = !open + didReceive = open + } + matcher.channelClosed = closed + + if closed { + return false, nil + } + + if hasSubMatcher { + if didReceive { + matcher.receivedValue = value + return subMatcher.Match(matcher.receivedValue.Interface()) + } else { + return false, nil + } + } + + if didReceive { + if matcher.Arg != nil { + outValue := reflect.ValueOf(matcher.Arg) + reflect.Indirect(outValue).Set(value) + } + + return true, nil + } else { + return false, nil + } +} + +func (matcher *ReceiveMatcher) FailureMessage(actual interface{}) (message string) { + subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + + if hasSubMatcher { + if matcher.receivedValue.IsValid() { + return subMatcher.FailureMessage(matcher.receivedValue.Interface()) + } + return "When passed a matcher, ReceiveMatcher's channel *must* receive something." + } else { + return format.Message(actual, "to receive something."+closedAddendum) + } +} + +func (matcher *ReceiveMatcher) NegatedFailureMessage(actual interface{}) (message string) { + subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + + if hasSubMatcher { + if matcher.receivedValue.IsValid() { + return subMatcher.NegatedFailureMessage(matcher.receivedValue.Interface()) + } + return "When passed a matcher, ReceiveMatcher's channel *must* receive something." + } else { + return format.Message(actual, "not to receive anything."+closedAddendum) + } +} + +func (matcher *ReceiveMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + if !isChan(actual) { + return false + } + + return !matcher.channelClosed +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher_test.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher_test.go new file mode 100644 index 00000000000..938c078e6f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/receive_matcher_test.go @@ -0,0 +1,280 @@ +package matchers_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/matchers" +) + +type kungFuActor interface { + DrunkenMaster() bool +} + +type jackie struct { + name string +} + +func (j *jackie) DrunkenMaster() bool { + return true +} + +var _ = Describe("ReceiveMatcher", func() { + Context("with no argument", func() { + Context("for a buffered channel", func() { + It("should succeed", func() { + channel := make(chan bool, 1) + + Ω(channel).ShouldNot(Receive()) + + channel <- true + + Ω(channel).Should(Receive()) + }) + }) + + Context("for an unbuffered channel", func() { + It("should succeed (eventually)", func() { + channel := make(chan bool) + + Ω(channel).ShouldNot(Receive()) + + go func() { + time.Sleep(10 * time.Millisecond) + channel <- true + }() + + Eventually(channel).Should(Receive()) + }) + }) + }) + + Context("with a pointer argument", func() { + Context("of the correct type", func() { + It("should write the value received on the channel to the pointer", func() { + channel := make(chan int, 1) + + var value int + + Ω(channel).ShouldNot(Receive(&value)) + Ω(value).Should(BeZero()) + + channel <- 17 + + Ω(channel).Should(Receive(&value)) + Ω(value).Should(Equal(17)) + }) + }) + + Context("to various types of objects", func() { + It("should work", func() { + //channels of strings + stringChan := make(chan string, 1) + stringChan <- "foo" + + var s string + Ω(stringChan).Should(Receive(&s)) + Ω(s).Should(Equal("foo")) + + //channels of slices + sliceChan := make(chan []bool, 1) + sliceChan <- []bool{true, true, false} + + var sl []bool + Ω(sliceChan).Should(Receive(&sl)) + Ω(sl).Should(Equal([]bool{true, true, false})) + + //channels of channels + chanChan := make(chan chan bool, 1) + c := make(chan bool) + chanChan <- c + + var receivedC chan bool + Ω(chanChan).Should(Receive(&receivedC)) + Ω(receivedC).Should(Equal(c)) + + //channels of interfaces + jackieChan := make(chan kungFuActor, 1) + aJackie := &jackie{name: "Jackie Chan"} + jackieChan <- aJackie + + var theJackie kungFuActor + Ω(jackieChan).Should(Receive(&theJackie)) + Ω(theJackie).Should(Equal(aJackie)) + }) + }) + + Context("of the wrong type", func() { + It("should error", func() { + channel := make(chan int) + var incorrectType bool + + success, err := (&ReceiveMatcher{Arg: &incorrectType}).Match(channel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + var notAPointer int + success, err = (&ReceiveMatcher{Arg: notAPointer}).Match(channel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + }) + + Context("with a matcher", func() { + It("should defer to the underlying matcher", func() { + intChannel := make(chan int, 1) + intChannel <- 3 + Ω(intChannel).Should(Receive(Equal(3))) + + intChannel <- 2 + Ω(intChannel).ShouldNot(Receive(Equal(3))) + + stringChannel := make(chan []string, 1) + stringChannel <- []string{"foo", "bar", "baz"} + Ω(stringChannel).Should(Receive(ContainElement(ContainSubstring("fo")))) + + stringChannel <- []string{"foo", "bar", "baz"} + Ω(stringChannel).ShouldNot(Receive(ContainElement(ContainSubstring("archipelago")))) + }) + + It("should defer to the underlying matcher for the message", func() { + matcher := Receive(Equal(3)) + channel := make(chan int, 1) + channel <- 2 + matcher.Match(channel) + Ω(matcher.FailureMessage(channel)).Should(MatchRegexp(`Expected\s+: 2\s+to equal\s+: 3`)) + + channel <- 3 + matcher.Match(channel) + Ω(matcher.NegatedFailureMessage(channel)).Should(MatchRegexp(`Expected\s+: 3\s+not to equal\s+: 3`)) + }) + + It("should work just fine with Eventually", func() { + stringChannel := make(chan string) + + go func() { + time.Sleep(5 * time.Millisecond) + stringChannel <- "A" + time.Sleep(5 * time.Millisecond) + stringChannel <- "B" + }() + + Eventually(stringChannel).Should(Receive(Equal("B"))) + }) + + Context("if the matcher errors", func() { + It("should error", func() { + channel := make(chan int, 1) + channel <- 3 + success, err := (&ReceiveMatcher{Arg: ContainSubstring("three")}).Match(channel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("if nothing is received", func() { + It("should fail", func() { + channel := make(chan int, 1) + success, err := (&ReceiveMatcher{Arg: Equal(1)}).Match(channel) + Ω(success).Should(BeFalse()) + Ω(err).ShouldNot(HaveOccurred()) + }) + }) + }) + + Context("When actual is a *closed* channel", func() { + Context("for a buffered channel", func() { + It("should work until it hits the end of the buffer", func() { + channel := make(chan bool, 1) + channel <- true + + close(channel) + + Ω(channel).Should(Receive()) + Ω(channel).ShouldNot(Receive()) + }) + }) + + Context("for an unbuffered channel", func() { + It("should always fail", func() { + channel := make(chan bool) + close(channel) + + Ω(channel).ShouldNot(Receive()) + }) + }) + }) + + Context("When actual is a send-only channel", func() { + It("should error", func() { + channel := make(chan bool) + + var writerChannel chan<- bool + writerChannel = channel + + success, err := (&ReceiveMatcher{}).Match(writerChannel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when acutal is a non-channel", func() { + It("should error", func() { + var nilChannel chan bool + + success, err := (&ReceiveMatcher{}).Match(nilChannel) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&ReceiveMatcher{}).Match(nil) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + + success, err = (&ReceiveMatcher{}).Match(3) + Ω(success).Should(BeFalse()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Describe("when used with eventually and a custom matcher", func() { + It("should return the matcher's error when a failing value is received on the channel, instead of the must receive something failure", func() { + failures := InterceptGomegaFailures(func() { + c := make(chan string, 0) + Eventually(c, 0.01).Should(Receive(Equal("hello"))) + }) + Ω(failures[0]).Should(ContainSubstring("When passed a matcher, ReceiveMatcher's channel *must* receive something.")) + + failures = InterceptGomegaFailures(func() { + c := make(chan string, 1) + c <- "hi" + Eventually(c, 0.01).Should(Receive(Equal("hello"))) + }) + Ω(failures[0]).Should(ContainSubstring(": hello")) + }) + }) + + Describe("Bailing early", func() { + It("should bail early when passed a closed channel", func() { + c := make(chan bool) + close(c) + + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(c).Should(Receive()) + }) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + Ω(failures).Should(HaveLen(1)) + }) + + It("should bail early when passed a non-channel", func() { + t := time.Now() + failures := InterceptGomegaFailures(func() { + Eventually(3).Should(Receive()) + }) + Ω(time.Since(t)).Should(BeNumerically("<", 500*time.Millisecond)) + Ω(failures).Should(HaveLen(1)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/MIT.LICENSE b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/MIT.LICENSE new file mode 100644 index 00000000000..8edd8175abe --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/MIT.LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go new file mode 100644 index 00000000000..119d21ef317 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go @@ -0,0 +1,41 @@ +package bipartitegraph + +import "errors" +import "fmt" + +import . "github.com/onsi/gomega/matchers/support/goraph/node" +import . "github.com/onsi/gomega/matchers/support/goraph/edge" + +type BipartiteGraph struct { + Left NodeOrderedSet + Right NodeOrderedSet + Edges EdgeSet +} + +func NewBipartiteGraph(leftValues, rightValues []interface{}, neighbours func(interface{}, interface{}) (bool, error)) (*BipartiteGraph, error) { + left := NodeOrderedSet{} + for i, _ := range leftValues { + left = append(left, Node{i}) + } + + right := NodeOrderedSet{} + for j, _ := range rightValues { + right = append(right, Node{j + len(left)}) + } + + edges := EdgeSet{} + for i, leftValue := range leftValues { + for j, rightValue := range rightValues { + neighbours, err := neighbours(leftValue, rightValue) + if err != nil { + return nil, errors.New(fmt.Sprintf("error determining adjacency for %v and %v: %s", leftValue, rightValue, err.Error())) + } + + if neighbours { + edges = append(edges, Edge{left[i], right[j]}) + } + } + } + + return &BipartiteGraph{left, right, edges}, nil +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go new file mode 100644 index 00000000000..32529c51131 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go @@ -0,0 +1,161 @@ +package bipartitegraph + +import . "github.com/onsi/gomega/matchers/support/goraph/node" +import . "github.com/onsi/gomega/matchers/support/goraph/edge" +import "github.com/onsi/gomega/matchers/support/goraph/util" + +func (bg *BipartiteGraph) LargestMatching() (matching EdgeSet) { + paths := bg.maximalDisjointSLAPCollection(matching) + + for len(paths) > 0 { + for _, path := range paths { + matching = matching.SymmetricDifference(path) + } + paths = bg.maximalDisjointSLAPCollection(matching) + } + + return +} + +func (bg *BipartiteGraph) maximalDisjointSLAPCollection(matching EdgeSet) (result []EdgeSet) { + guideLayers := bg.createSLAPGuideLayers(matching) + if len(guideLayers) == 0 { + return + } + + used := make(map[Node]bool) + + for _, u := range guideLayers[len(guideLayers)-1] { + slap, found := bg.findDisjointSLAP(u, matching, guideLayers, used) + if found { + for _, edge := range slap { + used[edge.Node1] = true + used[edge.Node2] = true + } + result = append(result, slap) + } + } + + return +} + +func (bg *BipartiteGraph) findDisjointSLAP( + start Node, + matching EdgeSet, + guideLayers []NodeOrderedSet, + used map[Node]bool, +) ([]Edge, bool) { + return bg.findDisjointSLAPHelper(start, EdgeSet{}, len(guideLayers)-1, matching, guideLayers, used) +} + +func (bg *BipartiteGraph) findDisjointSLAPHelper( + currentNode Node, + currentSLAP EdgeSet, + currentLevel int, + matching EdgeSet, + guideLayers []NodeOrderedSet, + used map[Node]bool, +) (EdgeSet, bool) { + used[currentNode] = true + + if currentLevel == 0 { + return currentSLAP, true + } + + for _, nextNode := range guideLayers[currentLevel-1] { + if used[nextNode] { + continue + } + + edge, found := bg.Edges.FindByNodes(currentNode, nextNode) + if !found { + continue + } + + if matching.Contains(edge) == util.Odd(currentLevel) { + continue + } + + currentSLAP = append(currentSLAP, edge) + slap, found := bg.findDisjointSLAPHelper(nextNode, currentSLAP, currentLevel-1, matching, guideLayers, used) + if found { + return slap, true + } + currentSLAP = currentSLAP[:len(currentSLAP)-1] + } + + used[currentNode] = false + return nil, false +} + +func (bg *BipartiteGraph) createSLAPGuideLayers(matching EdgeSet) (guideLayers []NodeOrderedSet) { + used := make(map[Node]bool) + currentLayer := NodeOrderedSet{} + + for _, node := range bg.Left { + if matching.Free(node) { + used[node] = true + currentLayer = append(currentLayer, node) + } + } + + if len(currentLayer) == 0 { + return []NodeOrderedSet{} + } else { + guideLayers = append(guideLayers, currentLayer) + } + + done := false + + for !done { + lastLayer := currentLayer + currentLayer = NodeOrderedSet{} + + if util.Odd(len(guideLayers)) { + for _, leftNode := range lastLayer { + for _, rightNode := range bg.Right { + if used[rightNode] { + continue + } + + edge, found := bg.Edges.FindByNodes(leftNode, rightNode) + if !found || matching.Contains(edge) { + continue + } + + currentLayer = append(currentLayer, rightNode) + used[rightNode] = true + + if matching.Free(rightNode) { + done = true + } + } + } + } else { + for _, rightNode := range lastLayer { + for _, leftNode := range bg.Left { + if used[leftNode] { + continue + } + + edge, found := bg.Edges.FindByNodes(leftNode, rightNode) + if !found || !matching.Contains(edge) { + continue + } + + currentLayer = append(currentLayer, leftNode) + used[leftNode] = true + } + } + + } + + if len(currentLayer) == 0 { + return []NodeOrderedSet{} + } else { + guideLayers = append(guideLayers, currentLayer) + } + } + + return +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go new file mode 100644 index 00000000000..4fd15cc0694 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go @@ -0,0 +1,61 @@ +package edge + +import . "github.com/onsi/gomega/matchers/support/goraph/node" + +type Edge struct { + Node1 Node + Node2 Node +} + +type EdgeSet []Edge + +func (ec EdgeSet) Free(node Node) bool { + for _, e := range ec { + if e.Node1 == node || e.Node2 == node { + return false + } + } + + return true +} + +func (ec EdgeSet) Contains(edge Edge) bool { + for _, e := range ec { + if e == edge { + return true + } + } + + return false +} + +func (ec EdgeSet) FindByNodes(node1, node2 Node) (Edge, bool) { + for _, e := range ec { + if (e.Node1 == node1 && e.Node2 == node2) || (e.Node1 == node2 && e.Node2 == node1) { + return e, true + } + } + + return Edge{}, false +} + +func (ec EdgeSet) SymmetricDifference(ec2 EdgeSet) EdgeSet { + edgesToInclude := make(map[Edge]bool) + + for _, e := range ec { + edgesToInclude[e] = true + } + + for _, e := range ec2 { + edgesToInclude[e] = !edgesToInclude[e] + } + + result := EdgeSet{} + for e, include := range edgesToInclude { + if include { + result = append(result, e) + } + } + + return result +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/node/node.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/node/node.go new file mode 100644 index 00000000000..800c2ea8caf --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/node/node.go @@ -0,0 +1,7 @@ +package node + +type Node struct { + Id int +} + +type NodeOrderedSet []Node diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/util/util.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/util/util.go new file mode 100644 index 00000000000..a24cd275055 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/support/goraph/util/util.go @@ -0,0 +1,7 @@ +package util + +import "math" + +func Odd(n int) bool { + return math.Mod(float64(n), 2.0) == 1.0 +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/matchers/type_support.go b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/type_support.go new file mode 100644 index 00000000000..ef9b44835ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/matchers/type_support.go @@ -0,0 +1,165 @@ +/* +Gomega matchers + +This package implements the Gomega matchers and does not typically need to be imported. +See the docs for Gomega for documentation on the matchers + +http://onsi.github.io/gomega/ +*/ +package matchers + +import ( + "fmt" + "reflect" +) + +type omegaMatcher interface { + Match(actual interface{}) (success bool, err error) + FailureMessage(actual interface{}) (message string) + NegatedFailureMessage(actual interface{}) (message string) +} + +func isBool(a interface{}) bool { + return reflect.TypeOf(a).Kind() == reflect.Bool +} + +func isNumber(a interface{}) bool { + if a == nil { + return false + } + kind := reflect.TypeOf(a).Kind() + return reflect.Int <= kind && kind <= reflect.Float64 +} + +func isInteger(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Int <= kind && kind <= reflect.Int64 +} + +func isUnsignedInteger(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Uint <= kind && kind <= reflect.Uint64 +} + +func isFloat(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Float32 <= kind && kind <= reflect.Float64 +} + +func toInteger(a interface{}) int64 { + if isInteger(a) { + return reflect.ValueOf(a).Int() + } else if isUnsignedInteger(a) { + return int64(reflect.ValueOf(a).Uint()) + } else if isFloat(a) { + return int64(reflect.ValueOf(a).Float()) + } else { + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) + } +} + +func toUnsignedInteger(a interface{}) uint64 { + if isInteger(a) { + return uint64(reflect.ValueOf(a).Int()) + } else if isUnsignedInteger(a) { + return reflect.ValueOf(a).Uint() + } else if isFloat(a) { + return uint64(reflect.ValueOf(a).Float()) + } else { + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) + } +} + +func toFloat(a interface{}) float64 { + if isInteger(a) { + return float64(reflect.ValueOf(a).Int()) + } else if isUnsignedInteger(a) { + return float64(reflect.ValueOf(a).Uint()) + } else if isFloat(a) { + return reflect.ValueOf(a).Float() + } else { + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) + } +} + +func isError(a interface{}) bool { + _, ok := a.(error) + return ok +} + +func isChan(a interface{}) bool { + if isNil(a) { + return false + } + return reflect.TypeOf(a).Kind() == reflect.Chan +} + +func isMap(a interface{}) bool { + if a == nil { + return false + } + return reflect.TypeOf(a).Kind() == reflect.Map +} + +func isArrayOrSlice(a interface{}) bool { + if a == nil { + return false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Array, reflect.Slice: + return true + default: + return false + } +} + +func isString(a interface{}) bool { + if a == nil { + return false + } + return reflect.TypeOf(a).Kind() == reflect.String +} + +func toString(a interface{}) (string, bool) { + aString, isString := a.(string) + if isString { + return aString, true + } + + aBytes, isBytes := a.([]byte) + if isBytes { + return string(aBytes), true + } + + aStringer, isStringer := a.(fmt.Stringer) + if isStringer { + return aStringer.String(), true + } + + return "", false +} + +func lengthOf(a interface{}) (int, bool) { + if a == nil { + return 0, false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Map, reflect.Array, reflect.String, reflect.Chan, reflect.Slice: + return reflect.ValueOf(a).Len(), true + default: + return 0, false + } +} + +func isNil(a interface{}) bool { + if a == nil { + return true + } + + switch reflect.TypeOf(a).Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return reflect.ValueOf(a).IsNil() + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/onsi/gomega/types/types.go b/Godeps/_workspace/src/github.com/onsi/gomega/types/types.go new file mode 100644 index 00000000000..1c632ade291 --- /dev/null +++ b/Godeps/_workspace/src/github.com/onsi/gomega/types/types.go @@ -0,0 +1,17 @@ +package types + +type GomegaFailHandler func(message string, callerSkip ...int) + +//A simple *testing.T interface wrapper +type GomegaTestingT interface { + Errorf(format string, args ...interface{}) +} + +//All Gomega matchers must implement the GomegaMatcher interface +// +//For details on writing custom matchers, check out: http://onsi.github.io/gomega/#adding_your_own_matchers +type GomegaMatcher interface { + Match(actual interface{}) (success bool, err error) + FailureMessage(actual interface{}) (message string) + NegatedFailureMessage(actual interface{}) (message string) +} diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/.travis.yml b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/.travis.yml new file mode 100644 index 00000000000..b19c2e53535 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/.travis.yml @@ -0,0 +1,11 @@ +language: go +go: + - 1.2 +before_install: +- go get github.com/onsi/ginkgo/... +- go get github.com/onsi/gomega/... +- go install github.com/onsi/ginkgo/ginkgo +script: PATH=$PATH:$HOME/gopath/bin ginkgo -r . +branches: + only: + - master diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/README.md b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/README.md new file mode 100644 index 00000000000..a591f1d5d0b --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/README.md @@ -0,0 +1,44 @@ +# Jibber Jabber [![Build Status](https://travis-ci.org/pivotal-cf-experimental/jibber_jabber.svg?branch=master)](https://travis-ci.org/pivotal-cf-experimental/jibber_jabber) +Jibber Jabber is a GoLang Library that can be used to detect an operating system's current language. + +### OS Support + +OSX and Linux via the `LC_ALL` and `LANG` environment variables. These are standard variables that are used in ALL versions of UNIX for language detection. + +Windows via [GetUserDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318136.aspx) and [GetSystemDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318122.aspx) system calls. These calls are supported in Windows Vista and up. + +# Usage +Add the following line to your go `import`: + +``` + "github.com/pivotal-cf-experimental/jibber_jabber" +``` + +### DetectIETF +`DetectIETF` will return the current locale as a string. The format of the locale will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code, a DASH, then an [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. + +``` + userLocale, err := jibber_jabber.DetectIETF() + println("Locale:", userLocale) +``` + +### DetectLanguage +`DetectLanguage` will return the current languge as a string. The format will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code. + +``` + userLanguage, err := jibber_jabber.DetectLanguage() + println("Language:", userLanguage) +``` + +### DetectTerritory +`DetectTerritory` will return the current locale territory as a string. The format will be the [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. + +``` + localeTerritory, err := jibber_jabber.DetectTerritory() + println("Territory:", localeTerritory) +``` + +### Errors +All the Detect commands will return an error if they are unable to read the Locale from the system. + +For Windows, additional error information is provided due to the nature of the system call being used. diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/ci/scripts/windows-64-test.bat b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/ci/scripts/windows-64-test.bat new file mode 100644 index 00000000000..b9a87bf7a96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/ci/scripts/windows-64-test.bat @@ -0,0 +1,5 @@ +git fetch +git checkout %GIT_COMMIT% + +SET GOPATH=%CD%\Godeps\_workspace;c:\Users\Administrator\go +c:\Go\bin\go test -v . diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber.go new file mode 100644 index 00000000000..45d288ea87a --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber.go @@ -0,0 +1,22 @@ +package jibber_jabber + +import ( + "strings" +) + +const ( + COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE = "Could not detect Language" +) + +func splitLocale(locale string) (string, string) { + formattedLocale := strings.Split(locale, ".")[0] + formattedLocale = strings.Replace(formattedLocale, "-", "_", -1) + + pieces := strings.Split(formattedLocale, "_") + language := pieces[0] + territory := "" + if len(pieces) > 1 { + territory = strings.Split(formattedLocale, "_")[1] + } + return language, territory +} diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_suite_test.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_suite_test.go new file mode 100644 index 00000000000..3da19c84bf7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_suite_test.go @@ -0,0 +1,13 @@ +package jibber_jabber_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestJibberJabber(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Jibber Jabber Suite") +} diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix.go new file mode 100644 index 00000000000..374d7617630 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix.go @@ -0,0 +1,57 @@ +// +build darwin freebsd linux netbsd openbsd + +package jibber_jabber + +import ( + "errors" + "os" + "strings" +) + +func getLangFromEnv() (locale string) { + locale = os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LANG") + } + return +} + +func getUnixLocale() (unix_locale string, err error) { + unix_locale = getLangFromEnv() + if unix_locale == "" { + err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE) + } + + return +} + +func DetectIETF() (locale string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + language, territory := splitLocale(unix_locale) + locale = language + if territory != "" { + locale = strings.Join([]string{language, territory}, "-") + } + } + + return +} + +func DetectLanguage() (language string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + language, _ = splitLocale(unix_locale) + } + + return +} + +func DetectTerritory() (territory string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + _, territory = splitLocale(unix_locale) + } + + return +} diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix_test.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix_test.go new file mode 100644 index 00000000000..a32e538c7a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_unix_test.go @@ -0,0 +1,103 @@ +// +build darwin freebsd linux netbsd openbsd + +package jibber_jabber_test + +import ( + . "github.com/pivotal-cf-experimental/jibber_jabber" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Unix", func() { + AfterEach(func() { + os.Setenv("LC_ALL", "") + os.Setenv("LANG", "en_US.UTF-8") + }) + + Describe("#DetectIETF", func() { + Context("Returns IETF encoded locale", func() { + It("should return the locale set to LC_ALL", func() { + os.Setenv("LC_ALL", "fr_FR.UTF-8") + result, _ := DetectIETF() + Ω(result).Should(Equal("fr-FR")) + }) + + It("should return the locale set to LANG if LC_ALL isn't set", func() { + os.Setenv("LANG", "fr_FR.UTF-8") + + result, _ := DetectIETF() + Ω(result).Should(Equal("fr-FR")) + }) + + It("should return an error if it cannot detect a locale", func() { + os.Setenv("LANG", "") + + _, err := DetectIETF() + Ω(err.Error()).Should(Equal(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE)) + }) + }) + + Context("when the locale is simply 'fr'", func() { + BeforeEach(func() { + os.Setenv("LANG", "fr") + }) + + It("should return the locale without a territory", func() { + language, err := DetectIETF() + Ω(err).ShouldNot(HaveOccurred()) + Ω(language).Should(Equal("fr")) + }) + }) + }) + + Describe("#DetectLanguage", func() { + Context("Returns encoded language", func() { + It("should return the language set to LC_ALL", func() { + os.Setenv("LC_ALL", "fr_FR.UTF-8") + result, _ := DetectLanguage() + Ω(result).Should(Equal("fr")) + }) + + It("should return the language set to LANG if LC_ALL isn't set", func() { + os.Setenv("LANG", "fr_FR.UTF-8") + + result, _ := DetectLanguage() + Ω(result).Should(Equal("fr")) + }) + + It("should return an error if it cannot detect a language", func() { + os.Setenv("LANG", "") + + _, err := DetectLanguage() + Ω(err.Error()).Should(Equal(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE)) + }) + }) + }) + + Describe("#DetectTerritory", func() { + Context("Returns encoded territory", func() { + It("should return the territory set to LC_ALL", func() { + os.Setenv("LC_ALL", "fr_FR.UTF-8") + result, _ := DetectTerritory() + Ω(result).Should(Equal("FR")) + }) + + It("should return the territory set to LANG if LC_ALL isn't set", func() { + os.Setenv("LANG", "fr_FR.UTF-8") + + result, _ := DetectTerritory() + Ω(result).Should(Equal("FR")) + }) + + It("should return an error if it cannot detect a territory", func() { + os.Setenv("LANG", "") + + _, err := DetectTerritory() + Ω(err.Error()).Should(Equal(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE)) + }) + }) + }) + +}) diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows.go new file mode 100644 index 00000000000..7f148811cc9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows.go @@ -0,0 +1,114 @@ +// +build windows + +package jibber_jabber + +import ( + "errors" + "syscall" + "unsafe" +) + +const LOCALE_NAME_MAX_LENGTH uint32 = 85 + +var SUPPORTED_LOCALES = map[uintptr]string{ + 0x0407: "de-DE", + 0x0409: "en-US", + 0x0c0a: "es-ES", //or is it 0x040a + 0x040c: "fr-FR", + 0x0410: "it-IT", + 0x0411: "ja-JA", + //0x0412: "ko_KO", - Will add support for Korean when nicksnyder/go-i18n supports Korean + 0x0416: "pt-BR", + //0x0419: "ru_RU", - Will add support for Russian when nicksnyder/go-i18n supports Russian + 0x0804: "zh-CN", + 0x0c04: "zh-HK", + 0x0404: "zh-TW", +} + +func getWindowsLocaleFrom(sysCall string) (locale string, err error) { + buffer := make([]uint16, LOCALE_NAME_MAX_LENGTH) + + dll := syscall.MustLoadDLL("kernel32") + proc := dll.MustFindProc(sysCall) + r, _, dllError := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(LOCALE_NAME_MAX_LENGTH)) + if r == 0 { + err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) + return + } + + locale = syscall.UTF16ToString(buffer) + + return +} + +func getAllWindowsLocaleFrom(sysCall string) (string, error) { + dll, err := syscall.LoadDLL("kernel32") + if err != nil { + return "", errors.New("Could not find kernel32 dll") + } + + proc, err := dll.FindProc(sysCall) + if err != nil { + return "", err + } + + locale, _, dllError := proc.Call() + if locale == 0 { + return "", errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) + } + + return SUPPORTED_LOCALES[locale], nil +} + +func getWindowsLocale() (locale string, err error) { + dll, err := syscall.LoadDLL("kernel32") + if err != nil { + return "", errors.New("Could not find kernel32 dll") + } + + proc, err := dll.FindProc("GetVersion") + if err != nil { + return "", err + } + + v, _, _ := proc.Call() + windowsVersion := byte(v) + isVistaOrGreater := (windowsVersion >= 6) + + if isVistaOrGreater { + locale, err = getWindowsLocaleFrom("GetUserDefaultLocaleName") + if err != nil { + locale, err = getWindowsLocaleFrom("GetSystemDefaultLocaleName") + } + } else if !isVistaOrGreater { + locale, err = getAllWindowsLocaleFrom("GetUserDefaultLCID") + if err != nil { + locale, err = getAllWindowsLocaleFrom("GetSystemDefaultLCID") + } + } else { + panic(v) + } + return +} +func DetectIETF() (locale string, err error) { + locale, err = getWindowsLocale() + return +} + +func DetectLanguage() (language string, err error) { + windows_locale, err := getWindowsLocale() + if err == nil { + language, _ = splitLocale(windows_locale) + } + + return +} + +func DetectTerritory() (territory string, err error) { + windows_locale, err := getWindowsLocale() + if err == nil { + _, territory = splitLocale(windows_locale) + } + + return +} diff --git a/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows_test.go b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows_test.go new file mode 100644 index 00000000000..75506448c1e --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-cf-experimental/jibber_jabber/jibber_jabber_windows_test.go @@ -0,0 +1,50 @@ +// +build windows + +package jibber_jabber_test + +import ( + . "github.com/pivotal-cf-experimental/jibber_jabber" + "regexp" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + LOCALE_REGEXP = "^[a-z]{2}-[A-Z]{2}$" + LANGUAGE_REGEXP = "^[a-z]{2}$" + TERRITORY_REGEXP = "^[A-Z]{2}$" +) + +var _ = Describe("Windows", func() { + BeforeEach(func() { + locale, err := DetectIETF() + Ω(err).Should(BeNil()) + Ω(locale).ShouldNot(BeNil()) + Ω(locale).ShouldNot(Equal("")) + }) + + Describe("#DetectIETF", func() { + It("detects correct IETF locale", func() { + locale, _ := DetectIETF() + matched, _ := regexp.MatchString(LOCALE_REGEXP, locale) + Ω(matched).Should(BeTrue()) + }) + }) + + Describe("#DetectLanguage", func() { + It("detects correct Language", func() { + language, _ := DetectLanguage() + matched, _ := regexp.MatchString(LANGUAGE_REGEXP, language) + Ω(matched).Should(BeTrue()) + }) + }) + + Describe("#DetectTerritory", func() { + It("detects correct Territory", func() { + territory, _ := DetectTerritory() + matched, _ := regexp.MatchString(TERRITORY_REGEXP, territory) + Ω(matched).Should(BeTrue()) + }) + }) +}) diff --git a/INSTALL.md b/INSTALL.md index 053146a831c..a15cd8643c9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,8 +2,9 @@ 1. Download the CLI from github: https://github.com/cloudfoundry/cli/releases 2. Extract the zip file. -3. Move `gcf` to C:\Program Files\Cloud Foundry\ -4. Set your %PATH% to include C:\Program Files\Cloud Foundry [(see instructions)](http://www.wikihow.com/Create-a-Custom-Windows-Command-Prompt) +3. Create a folder in C:\Program Files\, named "Cloud Foundry" +4. Move `cf` to C:\Program Files\Cloud Foundry\ +5. Set your %PATH% to include C:\Program Files\Cloud Foundry [(see instructions)](http://www.wikihow.com/Create-a-Custom-Windows-Command-Prompt) 1. Right-click My Computer > Properties 2. Click on Advanced system settings 3. Click on Environment Variables @@ -12,14 +13,14 @@ 6. Append C:\Program Files\Cloud Foundry\ to the Variable value separated by a semicolon 7. Click OK 8. Click OK -5. Open up the command prompt and type `gcf` -6. You should see the CLI help if everything is successful +6. Open up the command prompt and type `cf` +7. You should see the CLI help if everything is successful ## Mac OSX and Linux 1. Download the CLI from github: https://github.com/cloudfoundry/cli/releases 2. Extract the tgz file. -3. Move `gcf` to /usr/local/bin +3. Move `cf` to /usr/local/bin 4. Confirm /usr/local/bin is in your PATH by typing `echo $PATH` at the command line -5. Type `gcf` at the command line +5. Type `cf` at the command line 6. You should see the CLI help if everything is successful diff --git a/LICENSE b/LICENSE index 11069edd790..915b208920b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2014 Pivotal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 6571f600d4d..49ca8b4386d 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,205 @@ -Cloud Foundry CLI written in Go [![Build Status](https://travis-ci.org/cloudfoundry/cli.png?branch=master)](https://travis-ci.org/cloudfoundry/cli) -=========== +Cloud Foundry CLI [![Build Status](https://travis-ci.org/cloudfoundry/cli.png?branch=master)](https://travis-ci.org/cloudfoundry/cli) +================= -Background -=========== +This is the official command line client for Cloud Foundry. -Project to rewrite the Cloud Foundry CLI tool using Go. This project should currently be considered alpha quality -software and should not be used in production environments. If you need something more stable, please check -out the [RubyGem](https://github.com/cloudfoundry/cf). +You can follow our development progress on [Pivotal Tracker](https://www.pivotaltracker.com/s/projects/892938). -For a view on the current status of the project, check [cftracker](http://cftracker.cfapps.io/cfcli). +Getting Started +=============== +Download and run the installer for your platform from the [Downloads Section](#downloads). -Cloning the repository -====================== +Once installed, you can log in and push an app. +``` +$ cd [my-app-directory] +$ cf api api.[my-cloudfoundry].com +Setting api endpoint to https://api.[my-cloudfoundry].com... +OK -1. Install Go ```brew install go --cross-compile-common``` -1. Clone (Fork before hand for development). -1. Run ```git submodule update --init --recursive``` +$ cf login +API endpoint: https://api.[my-cloudfoundry].com -Downloading Edge -======== -The latest binary builds are published to Amazon S3 buckets -- http://go-cli.s3.amazonaws.com/gcf-darwin-amd64.tgz -- http://go-cli.s3.amazonaws.com/gcf-linux-amd64.tgz -- http://go-cli.s3.amazonaws.com/gcf-windows-386.zip -- http://go-cli.s3.amazonaws.com/gcf-windows-amd64.zip +Email> [my-email] -Building -======== +Password> [my-password] +Authenticating... +OK + +$ cf push +``` +#Further Reading and Getting Help +You can find further documentation at the docs page for the CLI [here](http://docs.cloudfoundry.org/devguide/#cf). +There is also help available in the CLI itself; type `cf help` for more information. +Each command also has help output available via `cf [command] --help` or `cf [command] -h`. +Finally, if you are still stuck, feel free to open a GitHub issue. + +Downloads +========= +**WARNING:** Edge binaries are published with each new 'push' that passes though CI. These binaries are *not intended for wider use*; they're for developers to test new features and fixes as they are completed. + +| Stable Installers | Stable Binaries | Edge Binaries | +| :---------------: |:---------------:| :------------:| +| [Mac OS X 64 bit](https://cli.run.pivotal.io/stable?release=macosx64&source=github) | [Mac OS X 64 bit](https://cli.run.pivotal.io/stable?release=macosx64-binary&source=github) | [Mac OS X 64 bit](https://cli.run.pivotal.io/edge?arch=macosx64&source=github) | +| [Windows 32 bit](https://cli.run.pivotal.io/stable?release=windows32&source=github) | [Windows 32 bit](https://cli.run.pivotal.io/stable?release=windows32-exe&source=github) | [Windows 32 bit](https://cli.run.pivotal.io/edge?arch=windows32&source=github) | +| [Windows 64 bit](https://cli.run.pivotal.io/stable?release=windows64&source=github) | [Windows 64 bit](https://cli.run.pivotal.io/stable?release=windows64-exe&source=github) | [Windows 64 bit](https://cli.run.pivotal.io/edge?arch=windows64&source=github) | +| [Redhat 32 bit](https://cli.run.pivotal.io/stable?release=redhat32&source=github) | [Linux 32 bit](https://cli.run.pivotal.io/stable?release=linux32-binary&source=github) | [Linux 32 bit](https://cli.run.pivotal.io/edge?arch=linux32&source=github) | +| [Redhat 64 bit](https://cli.run.pivotal.io/stable?release=redhat64&source=github) | [Linux 64 bit](https://cli.run.pivotal.io/stable?release=linux64-binary&source=github) | [Linux 64 bit](https://cli.run.pivotal.io/edge?arch=linux64&source=github) | +| [Debian 32 bit](https://cli.run.pivotal.io/stable?release=debian32&source=github) +| [Debian 64 bit](https://cli.run.pivotal.io/stable?release=debian64&source=github) + + +**Experimental:** Install CF for OSX through [Homebrew](http://brew.sh/) via the [pivotal's homebrew-tap](https://github.com/pivotal/homebrew-tap): + +``` +$ brew tap pivotal/tap +$ brew install cloudfoundry-cli +``` + +**Releases:** Information about our releases can be found [here](https://github.com/cloudfoundry/cli/releases) + +Troubleshooting / FAQs +====================== -1. Run ```./bin/build``` -1. The binary will be built into the out directory. +Linux +----- +* "bash: .cf: No such file or directory". Ensure that you're using the correct binary or installer for your architecture. See http://askubuntu.com/questions/133389/no-such-file-or-directory-but-the-file-exists -Development +Filing Bugs =========== -NOTE: Currently only development on OSX 10.8 is supported +##### For simple bugs (eg: text formatting, help messages, etc), please provide -1. Write a test. -1. Run ``` bin/test ``` and watch test fail. -1. Make test pass. -1. Submit a pull request. +- the command you ran +- what occurred +- what you expected to occur -If you want to run the benchmark tests +##### For bugs related to HTTP requests or strange behavior, please run the command with env var `CF_TRACE=true` and provide - ./bin/go test -bench . -benchmem cf/... +- the command you ran +- the trace output +- a high-level description of the bug -Releasing -========= +##### For panics and other crashes, please provide + +- the command you ran +- the stack trace generated (if any) +- any other relevant information + +Forking the repository for development +====================================== + +1. Install [Go](https://golang.org) +1. [Ensure your $GOPATH is set correctly](http://golang.org/cmd/go/#hdr-GOPATH_environment_variable) +1. Install [godep](https://github.com/tools/godep) +1. Get the cli source code: `go get github.com/cloudfoundry/cli` + * (Ignore any warnings about "no buildable Go source files") +1. Run `godep restore` (note: this will modify the dependencies in your $GOPATH) +1. Fork the repository +1. Add your fork as a remote: `cd $GOPATH/src/github.com/cloudfoundry/cli && git remote add your_name https://github.com/your_name/cli` + +Building +======== +To prepare your build environment, run `go get github.com/jteeuwen/go-bindata/...` + +1. Run `./bin/build` +1. The binary will be built into the `./out` directory. -On linux: run ```bin/build-all``` +Optionally, you can use `bin/run` to compile and run the executable in one step. -On mac: run ```bin/build-all-osx``` +Developing +========== -This will create tgz files in the release folder. +1. Install [Mercurial](http://mercurial.selenic.com/) +1. Run `go get code.google.com/p/go.tools/cmd/vet` +1. Write a Ginkgo test. +1. Run `bin/test` and watch the test fail. +1. Make the test pass. +1. Submit a pull request to the `master` branch. Contributing ============ -Rough overview of the architecture ----------------------------------- +Major new feature proposals are given as a publically viewable google document with commenting allowed and discussed on the [vcap-dev](https://groups.google.com/a/cloudfoundry.org/forum/#!forum/vcap-dev) mailing list. + +Pull Requests +--------------------- + +Pull Requests should be made against the `master` branch. + +Architecture overview +--------------------- + +A command is a struct that implements this interface: + +``` +type Command interface { + Metadata() command_metadata.CommandMetadata + GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) + Run(c *cli.Context) +} +``` -The app (in ```src/cf/app/app.go```) declares the list of available commands. Help and flags are defined there. -It will instantiate a command, and run it using the runner (in ```src/cf/commands/runner.go```). +`Metadata()` is just a description of the command name, usage and flags: +``` +type CommandMetadata struct { + Name string + ShortName string + Usage string + Description string + Flags []cli.Flag + SkipFlagParsing bool +} +``` -A command has requirements, and a run function. Requirements are used as filters before running the command. -If any of them fails, the command will not run (see ```src/cf/requirements``` for examples of requirements). +`GetRequirements()` returns a list of requirements that need to be met before a command can be invoked. -When the command is run, it communicates with api using repositories (they are in ```src/cf/api```). +`Run()` is the method that your command implements to do whatever it's supposed to do. The `context` object +provides flags and arguments. -Repositories are injected into the command, so tests can inject a fake. +When the command is run, it communicates with api using repositories (they are in `cf/api`). -Repositories communicate with the api endpoints through a Gateway (see ```src/cf/net```). +Dependencies are injected into each command, so tests can inject a fake. This means that dependencies are +typically declared as an interface type, and not a concrete type. (see `cf/commands/factory.go`) -Repositories return a Domain Object and an ApiResponse object. +Some dependencies are managed by a repository locator in `cf/api/repository_locator.go`. -Domain objects are data structures related to Cloud Foundry (see ```src/cf/domain```). +Repositories communicate with the api endpoints through a Gateway (see `cf/net`). -ApiResponse objects convey a variety of important error conditions (see ```src/cf/net/api_status```). +Models are data structures related to Cloud Foundry (see `cf/models`). For example, some models are +apps, buildpacks, domains, etc. Managing dependencies --------------------- -Command dependencies are managed by the commands factory. The app uses the command factory (in ```src/cf/commands/factory.go```) +Command dependencies are managed by the commands factory. The app uses the command factory (in `cf/commands/factory.go`) to instantiate them, this allows not sharing the knowledge of their dependencies with the app itself. -As for repositories, we use the repository locator to handle their dependencies. You can find it in ```src/cf/api/repository_locator.go```. +As for repositories, we use the repository locator to handle their dependencies. You can find it in `cf/api/repository_locator.go`. Example command --------------- -Create Space is a good example of command. Its tests include checking arguments, having requirements, and the actual command itself. -You will find it in ```src/cf/commands/space/create_space.go```. +Create Space is a good example of a command. Its tests include checking arguments, requiring the user +to be logged in, and the actual behavior of the command itself. You can find it in `cf/commands/space/create_space.go`. -Current Conventions +Current conventions =================== Creating Commands ----------------- -Resources that include several commands have been broken out into their own sub-package using the Resource name. An example of this convention is the -Space resource and package. +Resources that include several commands have been broken out into their own sub-package using the Resource name. An example +of this convention is the Space resource and package (see `cf/commands/space`) -In addition, command file and methods naming follows a CRUD like convention. For example, the Space resource includes commands such a CreateSpace, ListSpaces, etc. +In addition, command file and methods naming follows a CRUD like convention. For example, the Space resource includes commands +such a CreateSpace, ListSpaces, DeleteSpace, etc. Creating Repositories --------------------- -Although not ideal, we use the name "Repository" for API related operations as opposed to "Service". Repository was chosen to avoid confusion with Service domain objects (i.e. creating Services and Service Instances within Cloud Foundry). +Although not ideal, we use the name "Repository" for API related operations as opposed to "Service". Repository was chosen +to avoid confusion with Service model objects (i.e. creating Services and Service Instances within Cloud Foundry). -By convention, Repository methods return a Domain object and an ApiResponse. Domain objects are used in both Commands and Repositories to model Cloud Foundry data. ApiResponse objects are used to communicate application errors, runtime errors, whether the resource was found, etc. -This convention provides a consistent method signature across repositories. +By convention, Repository methods return a model object and an error. Models are used in both Commands and Repositories +to model Cloud Foundry data. This convention provides a consistent method signature across repositories. diff --git a/VERSION b/VERSION new file mode 100644 index 00000000000..f0e13c50902 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +6.7.0 diff --git a/bin/build b/bin/build index c18c4d29174..a086812c130 100755 --- a/bin/build +++ b/bin/build @@ -1,5 +1,19 @@ -#!/bin/bash +#!/bin/bash set -e -$(dirname $0)/go build -o out/gcf main +echo -e "\nGenerating Binary..." + +ROOT_DIR=$(cd $(dirname $(dirname $0)) && pwd) + +$ROOT_DIR/bin/generate-language-resources + +CLI_GOPATH=$ROOT_DIR/tmp/cli_gopath +rm -rf $CLI_GOPATH +mkdir -p $CLI_GOPATH/src/github.com/cloudfoundry/ +ln -s $ROOT_DIR $CLI_GOPATH/src/github.com/cloudfoundry/cli + +GODEP_GOPATH=$ROOT_DIR/Godeps/_workspace + +GOPATH=$CLI_GOPATH:$GODEP_GOPATH:$GOPATH go build -o $ROOT_DIR/out/cf ./main +rm -rf $CLI_GOPATH diff --git a/bin/build-all b/bin/build-all deleted file mode 100755 index 68c9fcf8ff1..00000000000 --- a/bin/build-all +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -set -e - -mkdir -p release -echo "Created release dir." - -CURRENT_SHA=`git rev-parse HEAD | cut -c1-10` -# Linux specific -sed -i -e "s/SHA/$CURRENT_SHA/g" $(dirname $0)/../src/cf/app_constants.go -echo "Bumped SHA in version." - -PLATFORMS="darwin/amd64 linux/amd64 windows/amd64 windows/386" - -function build-architecture { - GOOS=${1%/*} - GOARCH=${1#*/} - printf "Creating $GOOS $GOARCH binary..." - - GOOS=$GOOS GOARCH=$GOARCH "$(dirname $0)/build" >/dev/null 2>&1 - cd out - - if [ $GOOS == windows ]; then - mv gcf gcf.exe - zip ../release/gcf-$GOOS-$GOARCH.zip gcf.exe >/dev/null 2>&1 - else - tar cvzf ../release/gcf-$GOOS-$GOARCH.tgz gcf >/dev/null 2>&1 - fi - - cd .. - echo " done." -} - -for PLATFORM in $PLATFORMS; do - build-architecture $PLATFORM -done - -git checkout $(dirname $0)/../src/cf/app_constants.go -echo "Cleaned up version." diff --git a/bin/build-all-osx b/bin/build-all-osx deleted file mode 100755 index 6384fd73f55..00000000000 --- a/bin/build-all-osx +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -set -e - -mkdir -p release -echo "Created release dir." - -CURRENT_SHA=`git rev-parse HEAD | cut -c1-10` -# Linux specific -sed -i "" -e "s/SHA/$CURRENT_SHA/g" $(dirname $0)/../src/cf/app_constants.go -echo "Bumped SHA in version." - -PLATFORMS="darwin/amd64 linux/amd64 windows/amd64 windows/386" - -function build-architecture { - GOOS=${1%/*} - GOARCH=${1#*/} - printf "Creating $GOOS $GOARCH binary..." - - GOOS=$GOOS GOARCH=$GOARCH "$(dirname $0)/build" >/dev/null 2>&1 - cd out - - if [ $GOOS == windows ]; then - mv gcf gcf.exe - tar cvzf ../release/gcf-$GOOS-$GOARCH.tgz gcf.exe >/dev/null 2>&1 - else - tar cvzf ../release/gcf-$GOOS-$GOARCH.tgz gcf >/dev/null 2>&1 - fi - - cd .. - echo " done." -} - -for PLATFORM in $PLATFORMS; do - build-architecture $PLATFORM -done - -git checkout $(dirname $0)/../src/cf/app_constants.go -echo "Cleaned up version." diff --git a/bin/build-all.sh b/bin/build-all.sh new file mode 100755 index 00000000000..79f09e24fb0 --- /dev/null +++ b/bin/build-all.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e +set -x + +OUTDIR=$(dirname $0)/../out + +GOARCH=amd64 GOOS=windows $(dirname $0)/build && cp $OUTDIR/cf $OUTDIR/cf-windows-amd64.exe +GOARCH=386 GOOS=windows $(dirname $0)/build && cp $OUTDIR/cf $OUTDIR/cf-windows-386.exe +GOARCH=amd64 GOOS=linux $(dirname $0)/build && cp $OUTDIR/cf $OUTDIR/cf-linux-amd64 +GOARCH=386 GOOS=linux $(dirname $0)/build && cp $OUTDIR/cf $OUTDIR/cf-linux-386 +GOARCH=amd64 GOOS=darwin $(dirname $0)/build && cp $OUTDIR/cf $OUTDIR/cf-darwin-amd64 diff --git a/bin/bump-version b/bin/bump-version new file mode 100755 index 00000000000..5da39db320e --- /dev/null +++ b/bin/bump-version @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +component=$1 + +version=$(cat VERSION) +major=$(echo $version | cut -d'.' -f 1) +minor=$(echo $version | cut -d'.' -f 2) +patch=$(echo $version | cut -d'.' -f 3) + +case "$component" in + major ) + major=$(expr $major + 1) + minor=0 + patch=0 + ;; + minor ) + minor=$(expr $minor + 1) + patch=0 + ;; + patch ) + patch=$(expr $patch + 1) + ;; + * ) + echo "Error - argument must be 'major', 'minor' or 'patch'" + echo "Usage: bump-version [major | minor | patch]" + exit 1 + ;; +esac + +version=$major.$minor.$patch + +echo "Updating VERSION file to $version" +echo $version > VERSION + +echo "Committing change" +git reset . +git add VERSION +git ci -m "Bump version to $version" + +echo "Creating v$version tag" +git tag v$version + +echo -e "All Done! You should go update \033[0;37;41mThe CLAW\033[m" diff --git a/bin/commit-version-bump b/bin/commit-version-bump new file mode 100755 index 00000000000..86fb2d73fad --- /dev/null +++ b/bin/commit-version-bump @@ -0,0 +1,8 @@ +echo "Adding CHANGELOG" +git add CHANGELOG.md +echo "Ammending commit with CHANGELOG update" +git ci --amend + +echo "Retagging" +git tag -d v$(cat VERSION) +git tag v$(cat VERSION) diff --git a/bin/env b/bin/env deleted file mode 100755 index 09e83ff4c05..00000000000 --- a/bin/env +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -e - -SCRIPT_HOME=$( cd "$( dirname "$0" )" && pwd ) - -base=$SCRIPT_HOME/.. - -exec env GOPATH=$base $@ \ No newline at end of file diff --git a/bin/fetch-binaries-and-installers b/bin/fetch-binaries-and-installers new file mode 100755 index 00000000000..b70adcd2310 --- /dev/null +++ b/bin/fetch-binaries-and-installers @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +mkdir -p tmp/release +wget http://go-cli.s3.amazonaws.com/builds/cf-darwin-amd64 -P tmp/release +wget http://go-cli.s3.amazonaws.com/builds/cf-linux-amd64 -P tmp/release +wget http://go-cli.s3.amazonaws.com/builds/cf-linux-386 -P tmp/release +wget http://go-cli.s3.amazonaws.com/builds/cf-windows-amd64.exe -P tmp/release +wget http://go-cli.s3.amazonaws.com/builds/cf-windows-386.exe -P tmp/release + +wget http://go-cli.s3.amazonaws.com/installer-osx-amd64.pkg -P tmp/release +wget http://go-cli.s3.amazonaws.com/cf-cli_i386.rpm -P tmp/release +wget http://go-cli.s3.amazonaws.com/cf-cli_amd64.rpm -P tmp/release +wget http://go-cli.s3.amazonaws.com/cf-cli_amd64.deb -P tmp/release +wget http://go-cli.s3.amazonaws.com/cf-cli_i386.deb -P tmp/release +wget http://go-cli.s3.amazonaws.com/installer-windows-amd64.zip -P tmp/release +wget http://go-cli.s3.amazonaws.com/installer-windows-386.zip -P tmp/release + +echo "all done" diff --git a/bin/generate-changelog b/bin/generate-changelog new file mode 100755 index 00000000000..f11e9896065 --- /dev/null +++ b/bin/generate-changelog @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +PREVIOUS_VERSION=$1 +CURRENT_VERSION=$2 + +if [ "$#" -ne 2 ]; then + cat <<-INFO +NAME: + generate-changelog - Generate changelog relative to a given version + +USAGE: + generate-changelog PREVIOUS_VERSION CURRENT_VERSION + +EXAMPLE: + generate-changelog v6.2.0 v6.3.0 +INFO + + exit 1 +fi + +git --no-pager log --grep \[.*\d*\] $PREVIOUS_VERSION..$CURRENT_VERSION --pretty='format:* %s %b' diff --git a/bin/generate-fakes b/bin/generate-fakes new file mode 100755 index 00000000000..1d7eece72db --- /dev/null +++ b/bin/generate-fakes @@ -0,0 +1,5 @@ +#!/bin/bash + +go get github.com/maxbrunsfeld/counterfeiter + +counterfeiter -o testhelpers/api/fake_app_events_repo.go cf/api AppEventsRepository diff --git a/bin/generate-i18n-files b/bin/generate-i18n-files new file mode 100755 index 00000000000..05d8aceea4b --- /dev/null +++ b/bin/generate-i18n-files @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -e + +echo "Generating i18n default (English translation) files" + +for locale in "$@" +do + # format locale, remove - for _ + aLocale=${locale%,} + aLocale=(${aLocale//-/_}) + + # extract language from locale + aLang=(${aLocale//.UTF*/}) + aLang=(${aLang//_*/}) + aLang=(${aLang//-*/}) + + echo "---> generating default files for: $aLocale" + files=`find cf/i18n/resources/en -name "en_US.all.json"` + count=0 + for file in $files + do + newFile=${file/en/$aLang} + newFile=${newFile/en_US/$aLocale} + newDir=${newFile/$aLocale.all.json/} + + mkdir -p -v $newDir + cp -v $file $newFile + + count=$[count + 1] + done + echo "---> created $count files for locale: $aLocale" + echo +done diff --git a/bin/generate-language-resources b/bin/generate-language-resources new file mode 100755 index 00000000000..474783f0ce4 --- /dev/null +++ b/bin/generate-language-resources @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +go get github.com/jteeuwen/go-bindata/... + +echo " Generating i18n Resource file" +go-bindata -pkg resources -ignore ".go" -o cf/resources/i18n_resources.go cf/i18n/resources/... cf/i18n/test_fixtures/... diff --git a/bin/generate-release-notes b/bin/generate-release-notes new file mode 100755 index 00000000000..8456e172aed --- /dev/null +++ b/bin/generate-release-notes @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +PREVIOUS_VERSION=$1 + +if [ -z $PREVIOUS_VERSION ]; then + cat <<-INFO +NAME: + generate-release-notes - Generate release notes relative to a given version + +USAGE: + generate-release-notes PREVIOUS_VERSION [--all] + +EXAMPLE: + generate-release-notes 6.0.1 +INFO + + exit 1 +fi + +if [ -z $(echo $PREVIOUS_VERSION | grep -e 'v') ]; then + echo "Error - PREVIOUS_VERSION argument should have this form: v6.0.1" + exit 1 +fi + +SHOW_ALL_COMMITS='' +if [ "$2" = "--all" ]; then + SHOW_ALL_COMMITS=true +fi + +VERSION=v$(cat VERSION) +URL_VERSION=$(cat VERSION) + +cat <<-NOTES +CF version $VERSION +=================== +Installers +---------- +- [Debian 32 bit](https://cli.run.pivotal.io/stable?release=debian32&version=$URL_VERSION&source=github-rel) +- [Debian 64 bit](https://cli.run.pivotal.io/stable?release=debian64&version=$URL_VERSION&source=github-rel) +- [Redhat 32 bit](https://cli.run.pivotal.io/stable?release=redhat32&version=$URL_VERSION&source=github-rel) +- [Redhat 64 bit](https://cli.run.pivotal.io/stable?release=redhat64&version=$URL_VERSION&source=github-rel) +- [Mac OS X 64 bit](https://cli.run.pivotal.io/stable?release=macosx64&version=$URL_VERSION&source=github-rel) +- [Windows 32 bit](https://cli.run.pivotal.io/stable?release=windows32&version=$URL_VERSION&source=github-rel) +- [Windows 64 bit](https://cli.run.pivotal.io/stable?release=windows64&version=$URL_VERSION&source=github-rel) + +Binaries +-------- +- [Linux 32 bit binary] (https://cli.run.pivotal.io/stable?release=linux32-binary&version=$URL_VERSION&source=github-rel) +- [Linux 64 bit binary] (https://cli.run.pivotal.io/stable?release=linux64-binary&version=$URL_VERSION&source=github-rel) +- [Mac OS X 64 bit binary](https://cli.run.pivotal.io/stable?release=macosx64-binary&version=$URL_VERSION&source=github-rel) +- [Windows 32 bit binary] (https://cli.run.pivotal.io/stable?release=windows32-exe&version=$URL_VERSION&source=github-rel) +- [Windows 64 bit binary] (https://cli.run.pivotal.io/stable?release=windows64-exe&version=$URL_VERSION&source=github-rel) + +Change Log +---------- +NOTES + +if [ -z $SHOW_ALL_COMMITS ]; then + git --no-pager log \ + $PREVIOUS_VERSION..$VERSION \ + --grep "\[.*\d*\]" \ + --pretty=format:'* %s%n%b' +else + git --no-pager log \ + $PREVIOUS_VERSION..$VERSION \ + --pretty=format:'* %s%n%b' +fi diff --git a/bin/generate-words b/bin/generate-words new file mode 100755 index 00000000000..7e50799d1c9 --- /dev/null +++ b/bin/generate-words @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e +go install github.com/jteeuwen/go-bindata/go-bindata +$(dirname $0)/go-bindata -pkg=words -o words/words.go words/dict diff --git a/bin/get-tools b/bin/get-tools new file mode 100755 index 00000000000..4b42d496cdc --- /dev/null +++ b/bin/get-tools @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +go get code.google.com/p/go.tools/cmd/vet +go get github.com/maximilien/i18n4go/... +go get github.com/jteeuwen/go-bindata diff --git a/bin/gi18n-checkup b/bin/gi18n-checkup new file mode 100755 index 00000000000..85bbf40ca02 --- /dev/null +++ b/bin/gi18n-checkup @@ -0,0 +1,21 @@ +#!/bin/bash + +set +e + +export GOPATH=$HOME/go +export PATH=$PATH:$GOPATH/bin + +go get -u github.com/maximilien/i18n4go/gi18n +OUTPUT=$? +if [ $OUTPUT -ne 0 ]; then + printf "Cannot install latest gi18n tool to verify strings:\n${OUTPUT}" + exit 1 +fi + +OUTPUT=`gi18n -c checkup` + +if [ "$OUTPUT" != "OK" ]; then + echo "Error:" + echo "$OUTPUT" + exit 1 +fi diff --git a/bin/go b/bin/go deleted file mode 100755 index 68aa0cbd6d4..00000000000 --- a/bin/go +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -e - -exec $(dirname $0)/env go $@ diff --git a/bin/log b/bin/log new file mode 100755 index 00000000000..3dec050bf8e --- /dev/null +++ b/bin/log @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +touch ./trace.log +rm ./trace.log +CF_TRACE=trace.log go run main/cf.go $* diff --git a/bin/remove-unused-translations.go b/bin/remove-unused-translations.go new file mode 100644 index 00000000000..2cc6d76d009 --- /dev/null +++ b/bin/remove-unused-translations.go @@ -0,0 +1,178 @@ +package main + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" +) + +// NB: this assumes that translation strings are globally unique +// as of the day we wrote this, they are not unique +func main() { + walkTranslationFilesAndPromptUser() +} + +func walkTranslationFilesAndPromptUser() { + stringsFromCode := readSourceCode() + + dir := "cf/i18n/resources" + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + panic(err.Error()) + } + + if info.IsDir() { + return nil + } + + if filepath.Ext(info.Name()) != ".json" { + return nil + } + + file, err := os.Open(path) + if err != nil { + panic(err.Error()) + } + data, err := ioutil.ReadAll(file) + if err != nil { + panic(err.Error()) + } + maps := []map[string]string{} + err = json.Unmarshal(data, &maps) + if err != nil { + panic(err.Error()) + } + + indicesToRemove := []int{} + for index, tmap := range maps { + str := tmap["id"] + + foundStr := false + for _, codeStr := range stringsFromCode { + if codeStr == str { + foundStr = true + break + } + } + + if !foundStr { + fmt.Printf("Did not find this string in the source code:\n") + fmt.Printf("'%s'\n", str) + println() + + answer := "" + fmt.Printf("Would you like to delete it from %s? [y|n]", path) + fmt.Fscanln(os.Stdin, &answer) + + if answer == "y" { + indicesToRemove = append(indicesToRemove, index) + } + } + } + + if len(indicesToRemove) > 0 { + println("Removing", len(indicesToRemove), "translations from", path) + + newMaps := []map[string]string{} + for i, mapp := range maps { + + foundIndex := false + for _, index := range indicesToRemove { + if index == i { + foundIndex = true + break + } + } + + if !foundIndex { + newMaps = append(newMaps, mapp) + } + } + + bytes, err := json.Marshal(newMaps) // consider json.MarshalIndent + if err != nil { + panic(err.Error()) + } + + newFile, err := os.Create(path) + if err != nil { + panic(err.Error()) + } + + _, err = newFile.Write(bytes) + if err != nil { + panic(err.Error()) + } + } + + return nil + }) +} + +func readSourceCode() []string { + strings := []string{} + + dir, err := os.Getwd() + if err != nil { + panic(err.Error()) + } + + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + panic(err.Error()) + } + + if info.IsDir() { + return nil + } + + if filepath.Ext(info.Name()) != ".go" { + return nil + } + + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, path, nil, 0) + if err != nil { + panic(err.Error()) + } + + for _, declaration := range astFile.Decls { + ast.Inspect(declaration, func(node ast.Node) bool { + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + funcNode, ok := callExpr.Fun.(*ast.Ident) + if !ok { + return true + } + + if funcNode.Name != "T" { + return true + } + + firstArg := callExpr.Args[0] + + argAsString, ok := firstArg.(*ast.BasicLit) + if !ok { + return true + } + + // remove quotes around string literal + end := len(argAsString.Value) - 1 + strings = append(strings, argAsString.Value[1:end]) + return true + }) + } + + return nil + }) + + return strings +} diff --git a/bin/replace-sha b/bin/replace-sha new file mode 100755 index 00000000000..5895930bade --- /dev/null +++ b/bin/replace-sha @@ -0,0 +1,15 @@ +#!/bin/bash + +CURRENT_SHA=$(git rev-parse --short HEAD) +CURRENT_VERSION=$(cat VERSION) +VERSION_STRING=$CURRENT_VERSION-$CURRENT_SHA + +if [ $(uname) == darwin ]; then + DATE_STRING=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00") + sed -i "" -e "s/BUILT_FROM_SOURCE/$VERSION_STRING/g" $(dirname $0)/../cf/app_constants.go + sed -i "" -e "s/BUILT_AT_UNKNOWN_TIME/$DATE_STRING/g" $(dirname $0)/../cf/app_constants.go +else + DATE_STRING=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00") + sed -i -e "s/BUILT_FROM_SOURCE/$VERSION_STRING/g" $(dirname $0)/../cf/app_constants.go + sed -i -e "s/BUILT_AT_UNKNOWN_TIME/$DATE_STRING/g" $(dirname $0)/../cf/app_constants.go +fi diff --git a/bin/replace-sha.ps1 b/bin/replace-sha.ps1 new file mode 100755 index 00000000000..8dbcb353691 --- /dev/null +++ b/bin/replace-sha.ps1 @@ -0,0 +1,12 @@ +$APP_CONST_FILE = $(split-path $MyInvocation.MyCommand.Definition) + "\..\cf\app_constants.go" +$APP_CONST_FILE_TMP = $APP_CONST_FILE + ".tmp" +$CURRENT_SHA = $(git rev-parse --short HEAD) +$CURRENT_VERSION = get-content VERSION +$VERSION_STRING = $CURRENT_VERSION + "-" + $CURRENT_SHA +$DATE = Get-Date -uformat "%Y-%m-%dT%H:%M:%S+00:00" + +get-content $APP_CONST_FILE | %{$_ -replace "BUILT_FROM_SOURCE", $VERSION_STRING} | Out-File -Encoding "UTF8" $APP_CONST_FILE_TMP +mv -Force $APP_CONST_FILE_TMP $APP_CONST_FILE + +get-content $APP_CONST_FILE | %{$_ -replace "BUILT_AT_UNKNOWN_TIME", $DATE} | Out-File -Encoding "UTF8" $APP_CONST_FILE_TMP +mv -Force $APP_CONST_FILE_TMP $APP_CONST_FILE diff --git a/bin/run b/bin/run index a3893b144d2..426d4a1fb0e 100755 --- a/bin/run +++ b/bin/run @@ -2,4 +2,13 @@ set -e -$(dirname $0)/go run src/main/cf.go $* \ No newline at end of file +bin/generate-language-resources + +GODEP=$(which godep) + +if [[ -z $GODEP ]] ; then + echo -e "godep is not installed. Run 'go get github.com/tools/godep'" + exit 1 +fi + +GOPATH=$($GODEP path):$GOPATH go run $(dirname $0)/../main/main.go "$@" diff --git a/bin/set-stable-release.sh b/bin/set-stable-release.sh new file mode 100755 index 00000000000..9ead2a8cf7f --- /dev/null +++ b/bin/set-stable-release.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e + +if [ -z "$AWS_ACCESS_KEY_ID" ]; then + echo "Need to set AWS_ACCESS_KEY_ID" + exit 1 +fi + +if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + echo "Need to set AWS_SECRET_ACCESS_KEY" + exit 1 +fi + +VERSION=$1 +if [ -z "$VERSION" ]; then + echo "Usage: set-stable-release VERSION" + echo "Example: set-stable-release v6.1.1" + exit 1 +fi + +root_dir=$(cd $(dirname $0) && pwd)/.. +s3_config_file=$root_dir/ci/s3cfg + +mkdir -p tmp +cd tmp +touch empty-file + +files=( + cf-linux-amd64.tgz + cf-linux-386.tgz + cf-darwin-amd64.tgz + cf-windows-amd64.zip + cf-windows-386.zip + cf-cli_amd64.deb + cf-cli_i386.deb + cf-cli_amd64.rpm + cf-cli_i386.rpm + installer-osx-amd64.pkg + installer-windows-amd64.zip + installer-windows-386.zip +) + +for file in ${files[*]} +do + echo "uploading file" $file + header="x-amz-website-redirect-location:/releases/$VERSION/$file" + s3cmd put empty-file s3://go-cli/releases/latest/$file --config=$s3_config_file --add-header $header > /dev/null 2>&1 +done diff --git a/bin/show-missing-strings b/bin/show-missing-strings new file mode 100755 index 00000000000..9f2495df65e --- /dev/null +++ b/bin/show-missing-strings @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +ENV['GOPATH']="#{ENV['HOME']}/go" +ENV['PATH']="#{ENV['PATH']}:#{ENV['GOPATH']}/bin" +CLI_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")) + +@exit_code = 0 + +def install_gi18n + output = `go get -u github.com/maximilien/i18n4go/gi18n` + unless $?.exitstatus == 0 + puts "Cannot install latest gi18n tool to verify strings:\n#{output}" + exit 1 + end +end + +def show_missing_strings(english_reference_file, directory_to_verify) + puts "\nVerifying: \n\t #{english_reference_file} \n\t #{directory_to_verify}\n\n" + + result = system("gi18n -c show-missing-strings -d #{directory_to_verify} --i18n-strings-filename #{english_reference_file}") + unless result + puts "===> Failed Verification!" + unless File.exist?(english_reference_file) + puts "#{english_reference_file} does not exist." + exit 1 + end + @exit_code = 1 + end +end + +def get_matching_directory(path_to_i18n) + i18n_resources_dir = File.join("cf", "i18n", "resources", "en") + File.expand_path(File.dirname(path_to_i18n.gsub(i18n_resources_dir, ''))) +end + +def run + path = File.join(CLI_ROOT, *%w[cf i18n resources en *]) + english_json_files = `find #{path} -type f`.split + + english_json_files.each do |english_reference_file| + show_missing_strings(english_reference_file, get_matching_directory(english_reference_file)) + end +end + +install_gi18n +run +exit(@exit_code) diff --git a/bin/test b/bin/test index 464d3574706..45dc62d32e4 100755 --- a/bin/test +++ b/bin/test @@ -1,31 +1,66 @@ #!/bin/bash -result=0 +( + set -e -echo -e "\n Formatting packages..." -$(dirname $0)/go fmt cf/... -let "result+=$?" + function printStatus { + if [ $? -eq 0 ]; then + echo -e "\nSWEET SUITE SUCCESS" + else + echo -e "\nSUITE FAILURE" + fi + } -echo -e "\n Installing package dependencies..." -$(dirname $0)/go test -i cf/... -let "result+=$?" + trap printStatus EXIT -echo -e "\n Testing packages:" -$(dirname $0)/go test cf/... -parallel 4$@ -let "result+=$?" + bin/generate-language-resources -echo -e "\n Vetting packages for potential issues..." -$(dirname $0)/go vet cf/... -let "result+=$?" + echo -e "\n Running gi18n checkup..." + bin/gi18n-checkup -echo -e "\n Running build script to confirm everything compiles..." -$(dirname $0)/build -let "result+=$?" + go get github.com/tools/godep -if [ $result -eq 0 ]; then - echo -e "\nSUITE SUCCESS" -else - echo -e "\nSUITE FAILURE" -fi + GODEP=$(which godep) -exit $result + echo -e "\n Cleaning build artifacts..." + + # Clean up old plugin binaries used in test + + rm -f fixtures/plugins/*.exe + rm -f plugin_examples/*.exe + + # Clean up old .a files in GOPATH + # It seems like `go clean` should do this but ... not so much + if [[ -d $GOPATH/pkg ]] ; then + pushd $GOPATH/pkg + rm -Rf * + popd + fi + + if [[ -d $($GODEP path)/pkg ]] ; then + pushd $($GODEP path)/pkg + rm -Rf * + popd + fi + + export LC_ALL="en_US.UTF-8" + export GOPATH=$($GODEP path):$GOPATH + export PATH=$($GODEP path)/bin:$PATH + go install github.com/onsi/ginkgo/ginkgo + + echo -e "\n Formatting packages..." + go fmt ./... + + echo -e "\n Testing packages..." + ginkgo -r $@ + + echo -e "\n Vetting packages for potential issues..." + go tool vet cf/. + for file in $(find {cf,fileutils,generic,glob,main,testhelpers,words} \( -name "*.go" -not -iname "*test.go" \)) + do + go tool vet -all -shadow=true $file + done + + echo -e "\n Running build script to confirm everything compiles..." + bin/build +) diff --git a/bin/test_packages b/bin/test_packages new file mode 100755 index 00000000000..347688400dc --- /dev/null +++ b/bin/test_packages @@ -0,0 +1,38 @@ +#!/bin/bash + +( + set -e + + function printStatus { + if [ $? -eq 0 ]; then + echo -e "\nSWEET SUITE SUCCESS" + else + echo -e "\nSUITE FAILURE" + fi + } + + trap printStatus EXIT + + bin/generate-language-resources + + GODEP=$(which godep) + if [[ -z $GODEP ]] ; then + echo "godep is not installed. Run 'go get github.com/tools/godep'" + exit 1 + fi + + export GOPATH=$($GODEP path):$GOPATH + + echo -e "\n Cleaning build artifacts..." + go clean + + echo -e "\n Formatting packages..." + go fmt ./cf/... ./testhelpers/... ./generic/... ./main/... ./glob/... ./words/... + + echo -e "\n Testing packages:" + + for PKG in $@ + do + go test ./$PKG + done +) diff --git a/bin/trace b/bin/trace index 3118f96570f..d4826900e98 100755 --- a/bin/trace +++ b/bin/trace @@ -2,4 +2,4 @@ set -e -CF_TRACE=true $(dirname $0)/go run src/main/cf.go $* \ No newline at end of file +CF_TRACE=true go run main/cf.go $* diff --git a/bin/verify-strings b/bin/verify-strings new file mode 100755 index 00000000000..07beec82947 --- /dev/null +++ b/bin/verify-strings @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby + +ENV['GOPATH']="#{ENV['HOME']}/go" +ENV['PATH']="#{ENV['PATH']}:#{ENV['GOPATH']}/bin" +CLI_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")) + +@verbose = false + +def split_locale(locale) + return locale.split('_') +end + +def install_gi18n + output = `go get -u github.com/maximilien/i18n4go/gi18n` + unless $?.exitstatus == 0 + puts "Cannot install latest gi18n tool to verify strings:\n#{output}" + exit 1 + end +end + +def verify_strings(english_reference_file, locale_to_verify) + language_to_verify, _ = split_locale(locale_to_verify) + en_path = "/en/".gsub("/", File::SEPARATOR) + lang_path = "/#{language_to_verify}/".gsub("/", File::SEPARATOR) + file_to_verify = english_reference_file.gsub("en_US", locale_to_verify).gsub(en_path, lang_path) + + if @verbose + puts "Verifying: \n\t #{english_reference_file} \n\t #{file_to_verify}" + end + + result = system("gi18n -c verify-strings -source-language en_US -f #{english_reference_file} -languages #{locale_to_verify} -language-files #{file_to_verify}") + unless result + puts "failed verification:" + unless File.exist?(file_to_verify) + puts "#{file_to_verify} does not exist." + exit 1 + end + + `find #{file_to_verify}.* -type f`.split.each do |output_info| + puts output_info + puts File.read(output_info) + end + exit 1 + end +end + +def run + path = "#{CLI_ROOT}/cf/i18n/resources/en/*".gsub("/", File::SEPARATOR) + english_json_files = `find #{path} -type f`.split + supported_locales = Dir.glob("#{CLI_ROOT}/cf/i18n/resources/**/*.all.json".gsub("/", File::SEPARATOR)).map do |filepath| + filepath.split(File::SEPARATOR).last.gsub(".all.json", "") + end.uniq + + english_json_files.each do |english_reference_file| + supported_locales.each do |locale| + verify_strings(english_reference_file, locale) + end + end +end + +@verbose = ARGV.include?("-v") +install_gi18n +run diff --git a/cf/actors/actors_suite_test.go b/cf/actors/actors_suite_test.go new file mode 100644 index 00000000000..2fcb971fcfc --- /dev/null +++ b/cf/actors/actors_suite_test.go @@ -0,0 +1,13 @@ +package actors_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestActors(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Actors Suite") +} diff --git a/cf/actors/broker_builder/broker_builder.go b/cf/actors/broker_builder/broker_builder.go new file mode 100644 index 00000000000..d9f9267f445 --- /dev/null +++ b/cf/actors/broker_builder/broker_builder.go @@ -0,0 +1,118 @@ +package broker_builder + +import ( + "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" +) + +type BrokerBuilder interface { + AttachBrokersToServices([]models.ServiceOffering) ([]models.ServiceBroker, error) + AttachSpecificBrokerToServices(string, []models.ServiceOffering) (models.ServiceBroker, error) + GetAllServiceBrokers() ([]models.ServiceBroker, error) + GetBrokerWithAllServices(brokerName string) (models.ServiceBroker, error) + GetBrokerWithSpecifiedService(serviceName string) (models.ServiceBroker, error) +} + +type Builder struct { + brokerRepo api.ServiceBrokerRepository + serviceBuilder service_builder.ServiceBuilder +} + +func NewBuilder(broker api.ServiceBrokerRepository, serviceBuilder service_builder.ServiceBuilder) Builder { + return Builder{ + brokerRepo: broker, + serviceBuilder: serviceBuilder, + } +} + +func (builder Builder) AttachBrokersToServices(services []models.ServiceOffering) ([]models.ServiceBroker, error) { + var brokers []models.ServiceBroker + brokersMap := make(map[string]models.ServiceBroker) + + for _, service := range services { + if service.BrokerGuid == "" { + continue + } + + if broker, ok := brokersMap[service.BrokerGuid]; ok { + broker.Services = append(broker.Services, service) + brokersMap[broker.Guid] = broker + } else { + broker, err := builder.brokerRepo.FindByGuid(service.BrokerGuid) + if err != nil { + return nil, err + } + broker.Services = append(broker.Services, service) + brokersMap[service.BrokerGuid] = broker + } + } + + for _, broker := range brokersMap { + brokers = append(brokers, broker) + } + + return brokers, nil +} + +func (builder Builder) AttachSpecificBrokerToServices(brokerName string, services []models.ServiceOffering) (models.ServiceBroker, error) { + broker, err := builder.brokerRepo.FindByName(brokerName) + if err != nil { + return models.ServiceBroker{}, err + } + + for _, service := range services { + if service.BrokerGuid == broker.Guid { + broker.Services = append(broker.Services, service) + } + } + + return broker, nil +} + +func (builder Builder) GetAllServiceBrokers() ([]models.ServiceBroker, error) { + brokers := []models.ServiceBroker{} + var err error + var services models.ServiceOfferings + + err = builder.brokerRepo.ListServiceBrokers(func(broker models.ServiceBroker) bool { + brokers = append(brokers, broker) + return true + }) + + for index, broker := range brokers { + services, err = builder.serviceBuilder.GetServicesForBroker(broker.Guid) + if err != nil { + return nil, err + } + + brokers[index].Services = services + } + return brokers, err +} + +func (builder Builder) GetBrokerWithAllServices(brokerName string) (models.ServiceBroker, error) { + broker, err := builder.brokerRepo.FindByName(brokerName) + if err != nil { + return models.ServiceBroker{}, err + } + services, err := builder.serviceBuilder.GetServicesForBroker(broker.Guid) + if err != nil { + return models.ServiceBroker{}, err + } + broker.Services = services + + return broker, nil +} + +func (builder Builder) GetBrokerWithSpecifiedService(serviceName string) (models.ServiceBroker, error) { + service, err := builder.serviceBuilder.GetServiceByNameWithPlansWithOrgNames(serviceName) + if err != nil { + return models.ServiceBroker{}, err + } + brokers, err := builder.AttachBrokersToServices([]models.ServiceOffering{service}) + if err != nil || len(brokers) == 0 { + return models.ServiceBroker{}, err + } + return brokers[0], err +} diff --git a/cf/actors/broker_builder/broker_builder_suite_test.go b/cf/actors/broker_builder/broker_builder_suite_test.go new file mode 100644 index 00000000000..e4709b5f697 --- /dev/null +++ b/cf/actors/broker_builder/broker_builder_suite_test.go @@ -0,0 +1,13 @@ +package broker_builder_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestBrokerBuilder(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "BrokerBuilder Suite") +} diff --git a/cf/actors/broker_builder/broker_builder_test.go b/cf/actors/broker_builder/broker_builder_test.go new file mode 100644 index 00000000000..70320368f6a --- /dev/null +++ b/cf/actors/broker_builder/broker_builder_test.go @@ -0,0 +1,238 @@ +package broker_builder_test + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors/broker_builder" + "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + + fake_service_builder "github.com/cloudfoundry/cli/cf/actors/service_builder/fakes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Broker Builder", func() { + var ( + brokerBuilder broker_builder.BrokerBuilder + + serviceBuilder *fake_service_builder.FakeServiceBuilder + brokerRepo *fakes.FakeServiceBrokerRepo + + serviceBroker1 models.ServiceBroker + + services models.ServiceOfferings + service1 models.ServiceOffering + service2 models.ServiceOffering + service3 models.ServiceOffering + publicServicePlan models.ServicePlanFields + privateServicePlan models.ServicePlanFields + ) + + BeforeEach(func() { + brokerRepo = &fakes.FakeServiceBrokerRepo{} + serviceBuilder = &fake_service_builder.FakeServiceBuilder{} + brokerBuilder = broker_builder.NewBuilder(brokerRepo, serviceBuilder) + + serviceBroker1 = models.ServiceBroker{Guid: "my-service-broker-guid", Name: "my-service-broker"} + + publicServicePlan = models.ServicePlanFields{ + Name: "public-service-plan", + Guid: "public-service-plan-guid", + Public: true, + } + + privateServicePlan = models.ServicePlanFields{ + Name: "private-service-plan", + Guid: "private-service-plan-guid", + Public: false, + OrgNames: []string{ + "org-1", + "org-2", + }, + } + + service1 = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-public-service", + Guid: "my-public-service-guid", + BrokerGuid: "my-service-broker-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + }, + } + + service2 = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-other-public-service", + Guid: "my-other-public-service-guid", + BrokerGuid: "my-service-broker-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + }, + } + + service3 = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-other-public-service", + Guid: "my-other-public-service-guid", + BrokerGuid: "my-service-broker-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + }, + } + + services = models.ServiceOfferings{ + service1, + service2, + } + + brokerRepo.FindByGuidServiceBroker = serviceBroker1 + }) + + Describe(".AttachBrokersToServices", func() { + It("attaches brokers to an array of services", func() { + + brokers, err := brokerBuilder.AttachBrokersToServices(services) + Expect(err).NotTo(HaveOccurred()) + Expect(len(brokers)).To(Equal(1)) + Expect(brokers[0].Name).To(Equal("my-service-broker")) + Expect(brokers[0].Services[0].Label).To(Equal("my-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + Expect(brokers[0].Services[1].Label).To(Equal("my-other-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + }) + + It("skips services that have no associated broker, e.g. v1 services", func() { + brokerlessService := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "lonely-v1-service", + Guid: "i-am-sad-and-old", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + }, + } + services = models.ServiceOfferings{ + service1, + service2, + brokerlessService, + } + + brokers, err := brokerBuilder.AttachBrokersToServices(services) + Expect(err).NotTo(HaveOccurred()) + Expect(len(brokers)).To(Equal(1)) + Expect(brokers[0].Name).To(Equal("my-service-broker")) + Expect(brokers[0].Services[0].Label).To(Equal("my-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + Expect(brokers[0].Services[1].Label).To(Equal("my-other-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + }) + }) + + Describe(".AttachSpecificBrokerToServices", func() { + BeforeEach(func() { + service3 = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-other-public-service", + Guid: "my-other-public-service-guid", + BrokerGuid: "my-other-service-broker-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + }, + } + services = append(services, service3) + }) + + It("attaches a single broker to only services that match", func() { + serviceBroker1.Services = models.ServiceOfferings{} + brokerRepo.FindByNameServiceBroker = serviceBroker1 + broker, err := brokerBuilder.AttachSpecificBrokerToServices("my-service-broker", services) + + Expect(err).NotTo(HaveOccurred()) + Expect(broker.Name).To(Equal("my-service-broker")) + Expect(broker.Services[0].Label).To(Equal("my-public-service")) + Expect(len(broker.Services[0].Plans)).To(Equal(2)) + Expect(broker.Services[1].Label).To(Equal("my-other-public-service")) + Expect(len(broker.Services[0].Plans)).To(Equal(2)) + Expect(len(broker.Services)).To(Equal(2)) + }) + }) + + Describe(".GetAllServiceBrokers", func() { + It("returns an error if we cannot list all brokers", func() { + brokerRepo.ListErr = true + + _, err := brokerBuilder.GetAllServiceBrokers() + Expect(err).To(HaveOccurred()) + }) + + It("returns an error if we cannot list the services for a broker", func() { + brokerRepo.ServiceBrokers = []models.ServiceBroker{serviceBroker1} + serviceBuilder.GetServicesForBrokerReturns(nil, errors.New("Cannot find services")) + + _, err := brokerBuilder.GetAllServiceBrokers() + Expect(err).To(HaveOccurred()) + }) + + It("returns all service brokers populated with their services", func() { + brokerRepo.ServiceBrokers = []models.ServiceBroker{serviceBroker1} + serviceBuilder.GetServicesForBrokerReturns(services, nil) + + brokers, err := brokerBuilder.GetAllServiceBrokers() + Expect(err).NotTo(HaveOccurred()) + Expect(len(brokers)).To(Equal(1)) + Expect(brokers[0].Name).To(Equal("my-service-broker")) + Expect(brokers[0].Services[0].Label).To(Equal("my-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + Expect(brokers[0].Services[1].Label).To(Equal("my-other-public-service")) + Expect(len(brokers[0].Services[0].Plans)).To(Equal(2)) + }) + }) + + Describe(".GetBrokerWithAllServices", func() { + It("returns a service broker populated with their services", func() { + brokerRepo.FindByNameServiceBroker = serviceBroker1 + serviceBuilder.GetServicesForBrokerReturns(services, nil) + + broker, err := brokerBuilder.GetBrokerWithAllServices("my-service-broker") + Expect(err).NotTo(HaveOccurred()) + Expect(broker.Name).To(Equal("my-service-broker")) + Expect(broker.Services[0].Label).To(Equal("my-public-service")) + Expect(len(broker.Services[0].Plans)).To(Equal(2)) + Expect(broker.Services[1].Label).To(Equal("my-other-public-service")) + Expect(len(broker.Services[0].Plans)).To(Equal(2)) + }) + }) + + Describe(".GetBrokerWithSpecifiedService", func() { + It("returns an error if a broker containeing the specific service cannot be found", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(models.ServiceOffering{}, errors.New("Asplosions")) + _, err := brokerBuilder.GetBrokerWithSpecifiedService("totally-not-a-service") + + Expect(err).To(HaveOccurred()) + }) + + It("returns the service broker populated with the specific service", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(service1, nil) + brokerRepo.FindByGuidServiceBroker = serviceBroker1 + + broker, err := brokerBuilder.GetBrokerWithSpecifiedService("my-public-service") + Expect(err).NotTo(HaveOccurred()) + Expect(broker.Name).To(Equal("my-service-broker")) + Expect(len(broker.Services)).To(Equal(1)) + Expect(broker.Services[0].Label).To(Equal("my-public-service")) + Expect(len(broker.Services[0].Plans)).To(Equal(2)) + }) + }) +}) diff --git a/cf/actors/broker_builder/fakes/fake_broker_builder.go b/cf/actors/broker_builder/fakes/fake_broker_builder.go new file mode 100644 index 00000000000..54dca0c71d0 --- /dev/null +++ b/cf/actors/broker_builder/fakes/fake_broker_builder.go @@ -0,0 +1,212 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/actors/broker_builder" + + "sync" + + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeBrokerBuilder struct { + AttachBrokersToServicesStub func([]models.ServiceOffering) ([]models.ServiceBroker, error) + attachBrokersToServicesMutex sync.RWMutex + attachBrokersToServicesArgsForCall []struct { + arg1 []models.ServiceOffering + } + attachBrokersToServicesReturns struct { + result1 []models.ServiceBroker + result2 error + } + AttachSpecificBrokerToServicesStub func(string, []models.ServiceOffering) (models.ServiceBroker, error) + attachSpecificBrokerToServicesMutex sync.RWMutex + attachSpecificBrokerToServicesArgsForCall []struct { + arg1 string + arg2 []models.ServiceOffering + } + attachSpecificBrokerToServicesReturns struct { + result1 models.ServiceBroker + result2 error + } + GetAllServiceBrokersStub func() ([]models.ServiceBroker, error) + getAllServiceBrokersMutex sync.RWMutex + getAllServiceBrokersArgsForCall []struct{} + getAllServiceBrokersReturns struct { + result1 []models.ServiceBroker + result2 error + } + GetBrokerWithAllServicesStub func(brokerName string) (models.ServiceBroker, error) + getBrokerWithAllServicesMutex sync.RWMutex + getBrokerWithAllServicesArgsForCall []struct { + brokerName string + } + getBrokerWithAllServicesReturns struct { + result1 models.ServiceBroker + result2 error + } + GetBrokerWithSpecifiedServiceStub func(serviceName string) (models.ServiceBroker, error) + getBrokerWithSpecifiedServiceMutex sync.RWMutex + getBrokerWithSpecifiedServiceArgsForCall []struct { + serviceName string + } + getBrokerWithSpecifiedServiceReturns struct { + result1 models.ServiceBroker + result2 error + } +} + +func (fake *FakeBrokerBuilder) AttachBrokersToServices(arg1 []models.ServiceOffering) ([]models.ServiceBroker, error) { + fake.attachBrokersToServicesMutex.Lock() + defer fake.attachBrokersToServicesMutex.Unlock() + fake.attachBrokersToServicesArgsForCall = append(fake.attachBrokersToServicesArgsForCall, struct { + arg1 []models.ServiceOffering + }{arg1}) + if fake.AttachBrokersToServicesStub != nil { + return fake.AttachBrokersToServicesStub(arg1) + } else { + return fake.attachBrokersToServicesReturns.result1, fake.attachBrokersToServicesReturns.result2 + } +} + +func (fake *FakeBrokerBuilder) AttachBrokersToServicesCallCount() int { + fake.attachBrokersToServicesMutex.RLock() + defer fake.attachBrokersToServicesMutex.RUnlock() + return len(fake.attachBrokersToServicesArgsForCall) +} + +func (fake *FakeBrokerBuilder) AttachBrokersToServicesArgsForCall(i int) []models.ServiceOffering { + fake.attachBrokersToServicesMutex.RLock() + defer fake.attachBrokersToServicesMutex.RUnlock() + return fake.attachBrokersToServicesArgsForCall[i].arg1 +} + +func (fake *FakeBrokerBuilder) AttachBrokersToServicesReturns(result1 []models.ServiceBroker, result2 error) { + fake.attachBrokersToServicesReturns = struct { + result1 []models.ServiceBroker + result2 error + }{result1, result2} +} + +func (fake *FakeBrokerBuilder) AttachSpecificBrokerToServices(arg1 string, arg2 []models.ServiceOffering) (models.ServiceBroker, error) { + fake.attachSpecificBrokerToServicesMutex.Lock() + defer fake.attachSpecificBrokerToServicesMutex.Unlock() + fake.attachSpecificBrokerToServicesArgsForCall = append(fake.attachSpecificBrokerToServicesArgsForCall, struct { + arg1 string + arg2 []models.ServiceOffering + }{arg1, arg2}) + if fake.AttachSpecificBrokerToServicesStub != nil { + return fake.AttachSpecificBrokerToServicesStub(arg1, arg2) + } else { + return fake.attachSpecificBrokerToServicesReturns.result1, fake.attachSpecificBrokerToServicesReturns.result2 + } +} + +func (fake *FakeBrokerBuilder) AttachSpecificBrokerToServicesCallCount() int { + fake.attachSpecificBrokerToServicesMutex.RLock() + defer fake.attachSpecificBrokerToServicesMutex.RUnlock() + return len(fake.attachSpecificBrokerToServicesArgsForCall) +} + +func (fake *FakeBrokerBuilder) AttachSpecificBrokerToServicesArgsForCall(i int) (string, []models.ServiceOffering) { + fake.attachSpecificBrokerToServicesMutex.RLock() + defer fake.attachSpecificBrokerToServicesMutex.RUnlock() + return fake.attachSpecificBrokerToServicesArgsForCall[i].arg1, fake.attachSpecificBrokerToServicesArgsForCall[i].arg2 +} + +func (fake *FakeBrokerBuilder) AttachSpecificBrokerToServicesReturns(result1 models.ServiceBroker, result2 error) { + fake.attachSpecificBrokerToServicesReturns = struct { + result1 models.ServiceBroker + result2 error + }{result1, result2} +} + +func (fake *FakeBrokerBuilder) GetAllServiceBrokers() ([]models.ServiceBroker, error) { + fake.getAllServiceBrokersMutex.Lock() + defer fake.getAllServiceBrokersMutex.Unlock() + fake.getAllServiceBrokersArgsForCall = append(fake.getAllServiceBrokersArgsForCall, struct{}{}) + if fake.GetAllServiceBrokersStub != nil { + return fake.GetAllServiceBrokersStub() + } else { + return fake.getAllServiceBrokersReturns.result1, fake.getAllServiceBrokersReturns.result2 + } +} + +func (fake *FakeBrokerBuilder) GetAllServiceBrokersCallCount() int { + fake.getAllServiceBrokersMutex.RLock() + defer fake.getAllServiceBrokersMutex.RUnlock() + return len(fake.getAllServiceBrokersArgsForCall) +} + +func (fake *FakeBrokerBuilder) GetAllServiceBrokersReturns(result1 []models.ServiceBroker, result2 error) { + fake.getAllServiceBrokersReturns = struct { + result1 []models.ServiceBroker + result2 error + }{result1, result2} +} + +func (fake *FakeBrokerBuilder) GetBrokerWithAllServices(brokerName string) (models.ServiceBroker, error) { + fake.getBrokerWithAllServicesMutex.Lock() + defer fake.getBrokerWithAllServicesMutex.Unlock() + fake.getBrokerWithAllServicesArgsForCall = append(fake.getBrokerWithAllServicesArgsForCall, struct { + brokerName string + }{brokerName}) + if fake.GetBrokerWithAllServicesStub != nil { + return fake.GetBrokerWithAllServicesStub(brokerName) + } else { + return fake.getBrokerWithAllServicesReturns.result1, fake.getBrokerWithAllServicesReturns.result2 + } +} + +func (fake *FakeBrokerBuilder) GetBrokerWithAllServicesCallCount() int { + fake.getBrokerWithAllServicesMutex.RLock() + defer fake.getBrokerWithAllServicesMutex.RUnlock() + return len(fake.getBrokerWithAllServicesArgsForCall) +} + +func (fake *FakeBrokerBuilder) GetBrokerWithAllServicesArgsForCall(i int) string { + fake.getBrokerWithAllServicesMutex.RLock() + defer fake.getBrokerWithAllServicesMutex.RUnlock() + return fake.getBrokerWithAllServicesArgsForCall[i].brokerName +} + +func (fake *FakeBrokerBuilder) GetBrokerWithAllServicesReturns(result1 models.ServiceBroker, result2 error) { + fake.getBrokerWithAllServicesReturns = struct { + result1 models.ServiceBroker + result2 error + }{result1, result2} +} + +func (fake *FakeBrokerBuilder) GetBrokerWithSpecifiedService(serviceName string) (models.ServiceBroker, error) { + fake.getBrokerWithSpecifiedServiceMutex.Lock() + defer fake.getBrokerWithSpecifiedServiceMutex.Unlock() + fake.getBrokerWithSpecifiedServiceArgsForCall = append(fake.getBrokerWithSpecifiedServiceArgsForCall, struct { + serviceName string + }{serviceName}) + if fake.GetBrokerWithSpecifiedServiceStub != nil { + return fake.GetBrokerWithSpecifiedServiceStub(serviceName) + } else { + return fake.getBrokerWithSpecifiedServiceReturns.result1, fake.getBrokerWithSpecifiedServiceReturns.result2 + } +} + +func (fake *FakeBrokerBuilder) GetBrokerWithSpecifiedServiceCallCount() int { + fake.getBrokerWithSpecifiedServiceMutex.RLock() + defer fake.getBrokerWithSpecifiedServiceMutex.RUnlock() + return len(fake.getBrokerWithSpecifiedServiceArgsForCall) +} + +func (fake *FakeBrokerBuilder) GetBrokerWithSpecifiedServiceArgsForCall(i int) string { + fake.getBrokerWithSpecifiedServiceMutex.RLock() + defer fake.getBrokerWithSpecifiedServiceMutex.RUnlock() + return fake.getBrokerWithSpecifiedServiceArgsForCall[i].serviceName +} + +func (fake *FakeBrokerBuilder) GetBrokerWithSpecifiedServiceReturns(result1 models.ServiceBroker, result2 error) { + fake.getBrokerWithSpecifiedServiceReturns = struct { + result1 models.ServiceBroker + result2 error + }{result1, result2} +} + +var _ BrokerBuilder = new(FakeBrokerBuilder) diff --git a/cf/actors/fakes/fake_push_actor.go b/cf/actors/fakes/fake_push_actor.go new file mode 100644 index 00000000000..ec656295c12 --- /dev/null +++ b/cf/actors/fakes/fake_push_actor.go @@ -0,0 +1,101 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/api/resources" + + "os" + "sync" +) + +type FakePushActor struct { + UploadAppStub func(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) error + uploadAppMutex sync.RWMutex + uploadAppArgsForCall []struct { + appGuid string + zipFile *os.File + presentFiles []resources.AppFileResource + } + uploadAppReturns struct { + result1 error + } + GatherFilesStub func(appDir string, uploadDir string) ([]resources.AppFileResource, error) + gatherFilesMutex sync.RWMutex + gatherFilesArgsForCall []struct { + appDir string + uploadDir string + } + gatherFilesReturns struct { + result1 []resources.AppFileResource + result2 error + } +} + +func (fake *FakePushActor) UploadApp(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) error { + fake.uploadAppMutex.Lock() + defer fake.uploadAppMutex.Unlock() + fake.uploadAppArgsForCall = append(fake.uploadAppArgsForCall, struct { + appGuid string + zipFile *os.File + presentFiles []resources.AppFileResource + }{appGuid, zipFile, presentFiles}) + if fake.UploadAppStub != nil { + return fake.UploadAppStub(appGuid, zipFile, presentFiles) + } else { + return fake.uploadAppReturns.result1 + } +} + +func (fake *FakePushActor) UploadAppCallCount() int { + fake.uploadAppMutex.RLock() + defer fake.uploadAppMutex.RUnlock() + return len(fake.uploadAppArgsForCall) +} + +func (fake *FakePushActor) UploadAppArgsForCall(i int) (string, *os.File, []resources.AppFileResource) { + fake.uploadAppMutex.RLock() + defer fake.uploadAppMutex.RUnlock() + return fake.uploadAppArgsForCall[i].appGuid, fake.uploadAppArgsForCall[i].zipFile, fake.uploadAppArgsForCall[i].presentFiles +} + +func (fake *FakePushActor) UploadAppReturns(result1 error) { + fake.uploadAppReturns = struct { + result1 error + }{result1} +} + +func (fake *FakePushActor) GatherFiles(appDir string, uploadDir string) ([]resources.AppFileResource, error) { + fake.gatherFilesMutex.Lock() + defer fake.gatherFilesMutex.Unlock() + fake.gatherFilesArgsForCall = append(fake.gatherFilesArgsForCall, struct { + appDir string + uploadDir string + }{appDir, uploadDir}) + if fake.GatherFilesStub != nil { + return fake.GatherFilesStub(appDir, uploadDir) + } else { + return fake.gatherFilesReturns.result1, fake.gatherFilesReturns.result2 + } +} + +func (fake *FakePushActor) GatherFilesCallCount() int { + fake.gatherFilesMutex.RLock() + defer fake.gatherFilesMutex.RUnlock() + return len(fake.gatherFilesArgsForCall) +} + +func (fake *FakePushActor) GatherFilesArgsForCall(i int) (string, string) { + fake.gatherFilesMutex.RLock() + defer fake.gatherFilesMutex.RUnlock() + return fake.gatherFilesArgsForCall[i].appDir, fake.gatherFilesArgsForCall[i].uploadDir +} + +func (fake *FakePushActor) GatherFilesReturns(result1 []resources.AppFileResource, result2 error) { + fake.gatherFilesReturns = struct { + result1 []resources.AppFileResource + result2 error + }{result1, result2} +} + +var _ PushActor = new(FakePushActor) diff --git a/cf/actors/fakes/fake_service_actor.go b/cf/actors/fakes/fake_service_actor.go new file mode 100644 index 00000000000..32d84623e8f --- /dev/null +++ b/cf/actors/fakes/fake_service_actor.go @@ -0,0 +1,140 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeServiceActor struct { + FilterBrokersStub func(brokerFlag string, serviceFlag string, orgFlag string) ([]models.ServiceBroker, error) + filterBrokersMutex sync.RWMutex + filterBrokersArgsForCall []struct { + arg1 string + arg2 string + arg3 string + } + filterBrokersReturns struct { + result1 []models.ServiceBroker + result2 error + } + AttachPlansToServiceStub func(models.ServiceOffering) (models.ServiceOffering, error) + attachPlansToServiceMutex sync.RWMutex + attachPlansToServiceArgsForCall []struct { + arg1 models.ServiceOffering + } + attachPlansToServiceReturns struct { + result1 models.ServiceOffering + result2 error + } + AttachOrgsToPlansStub func([]models.ServicePlanFields) ([]models.ServicePlanFields, error) + attachOrgsToPlansMutex sync.RWMutex + attachOrgsToPlansArgsForCall []struct { + arg1 []models.ServicePlanFields + } + attachOrgsToPlansReturns struct { + result1 []models.ServicePlanFields + result2 error + } +} + +func (fake *FakeServiceActor) FilterBrokers(arg1 string, arg2 string, arg3 string) ([]models.ServiceBroker, error) { + fake.filterBrokersMutex.Lock() + defer fake.filterBrokersMutex.Unlock() + fake.filterBrokersArgsForCall = append(fake.filterBrokersArgsForCall, struct { + arg1 string + arg2 string + arg3 string + }{arg1, arg2, arg3}) + if fake.FilterBrokersStub != nil { + return fake.FilterBrokersStub(arg1, arg2, arg3) + } else { + return fake.filterBrokersReturns.result1, fake.filterBrokersReturns.result2 + } +} + +func (fake *FakeServiceActor) FilterBrokersCallCount() int { + fake.filterBrokersMutex.RLock() + defer fake.filterBrokersMutex.RUnlock() + return len(fake.filterBrokersArgsForCall) +} + +func (fake *FakeServiceActor) FilterBrokersArgsForCall(i int) (string, string, string) { + fake.filterBrokersMutex.RLock() + defer fake.filterBrokersMutex.RUnlock() + return fake.filterBrokersArgsForCall[i].arg1, fake.filterBrokersArgsForCall[i].arg2, fake.filterBrokersArgsForCall[i].arg3 +} + +func (fake *FakeServiceActor) FilterBrokersReturns(result1 []models.ServiceBroker, result2 error) { + fake.filterBrokersReturns = struct { + result1 []models.ServiceBroker + result2 error + }{result1, result2} +} + +func (fake *FakeServiceActor) AttachPlansToService(arg1 models.ServiceOffering) (models.ServiceOffering, error) { + fake.attachPlansToServiceMutex.Lock() + defer fake.attachPlansToServiceMutex.Unlock() + fake.attachPlansToServiceArgsForCall = append(fake.attachPlansToServiceArgsForCall, struct { + arg1 models.ServiceOffering + }{arg1}) + if fake.AttachPlansToServiceStub != nil { + return fake.AttachPlansToServiceStub(arg1) + } else { + return fake.attachPlansToServiceReturns.result1, fake.attachPlansToServiceReturns.result2 + } +} + +func (fake *FakeServiceActor) AttachPlansToServiceCallCount() int { + fake.attachPlansToServiceMutex.RLock() + defer fake.attachPlansToServiceMutex.RUnlock() + return len(fake.attachPlansToServiceArgsForCall) +} + +func (fake *FakeServiceActor) AttachPlansToServiceArgsForCall(i int) models.ServiceOffering { + fake.attachPlansToServiceMutex.RLock() + defer fake.attachPlansToServiceMutex.RUnlock() + return fake.attachPlansToServiceArgsForCall[i].arg1 +} + +func (fake *FakeServiceActor) AttachPlansToServiceReturns(result1 models.ServiceOffering, result2 error) { + fake.attachPlansToServiceReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceActor) AttachOrgsToPlans(arg1 []models.ServicePlanFields) ([]models.ServicePlanFields, error) { + fake.attachOrgsToPlansMutex.Lock() + defer fake.attachOrgsToPlansMutex.Unlock() + fake.attachOrgsToPlansArgsForCall = append(fake.attachOrgsToPlansArgsForCall, struct { + arg1 []models.ServicePlanFields + }{arg1}) + if fake.AttachOrgsToPlansStub != nil { + return fake.AttachOrgsToPlansStub(arg1) + } else { + return fake.attachOrgsToPlansReturns.result1, fake.attachOrgsToPlansReturns.result2 + } +} + +func (fake *FakeServiceActor) AttachOrgsToPlansCallCount() int { + fake.attachOrgsToPlansMutex.RLock() + defer fake.attachOrgsToPlansMutex.RUnlock() + return len(fake.attachOrgsToPlansArgsForCall) +} + +func (fake *FakeServiceActor) AttachOrgsToPlansArgsForCall(i int) []models.ServicePlanFields { + fake.attachOrgsToPlansMutex.RLock() + defer fake.attachOrgsToPlansMutex.RUnlock() + return fake.attachOrgsToPlansArgsForCall[i].arg1 +} + +func (fake *FakeServiceActor) AttachOrgsToPlansReturns(result1 []models.ServicePlanFields, result2 error) { + fake.attachOrgsToPlansReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +var _ ServiceActor = new(FakeServiceActor) diff --git a/cf/actors/fakes/fake_service_plan_actor.go b/cf/actors/fakes/fake_service_plan_actor.go new file mode 100644 index 00000000000..5c6dc20fecc --- /dev/null +++ b/cf/actors/fakes/fake_service_plan_actor.go @@ -0,0 +1,240 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/actors" + "sync" +) + +type FakeServicePlanActor struct { + FindServiceAccessStub func(string, string) (actors.ServiceAccess, error) + findServiceAccessMutex sync.RWMutex + findServiceAccessArgsForCall []struct { + arg1 string + arg2 string + } + findServiceAccessReturns struct { + result1 actors.ServiceAccess + result2 error + } + UpdateAllPlansForServiceStub func(string, bool) (bool, error) + updateAllPlansForServiceMutex sync.RWMutex + updateAllPlansForServiceArgsForCall []struct { + arg1 string + arg2 bool + } + updateAllPlansForServiceReturns struct { + result1 bool + result2 error + } + UpdateOrgForServiceStub func(string, string, bool) (bool, error) + updateOrgForServiceMutex sync.RWMutex + updateOrgForServiceArgsForCall []struct { + arg1 string + arg2 string + arg3 bool + } + updateOrgForServiceReturns struct { + result1 bool + result2 error + } + UpdateSinglePlanForServiceStub func(string, string, bool) (actors.PlanAccess, error) + updateSinglePlanForServiceMutex sync.RWMutex + updateSinglePlanForServiceArgsForCall []struct { + arg1 string + arg2 string + arg3 bool + } + updateSinglePlanForServiceReturns struct { + result1 actors.PlanAccess + result2 error + } + UpdatePlanAndOrgForServiceStub func(string, string, string, bool) (actors.PlanAccess, error) + updatePlanAndOrgForServiceMutex sync.RWMutex + updatePlanAndOrgForServiceArgsForCall []struct { + arg1 string + arg2 string + arg3 string + arg4 bool + } + updatePlanAndOrgForServiceReturns struct { + result1 actors.PlanAccess + result2 error + } +} + +func (fake *FakeServicePlanActor) FindServiceAccess(arg1 string, arg2 string) (actors.ServiceAccess, error) { + fake.findServiceAccessMutex.Lock() + defer fake.findServiceAccessMutex.Unlock() + fake.findServiceAccessArgsForCall = append(fake.findServiceAccessArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.FindServiceAccessStub != nil { + return fake.FindServiceAccessStub(arg1, arg2) + } else { + return fake.findServiceAccessReturns.result1, fake.findServiceAccessReturns.result2 + } +} + +func (fake *FakeServicePlanActor) FindServiceAccessCallCount() int { + fake.findServiceAccessMutex.RLock() + defer fake.findServiceAccessMutex.RUnlock() + return len(fake.findServiceAccessArgsForCall) +} + +func (fake *FakeServicePlanActor) FindServiceAccessArgsForCall(i int) (string, string) { + fake.findServiceAccessMutex.RLock() + defer fake.findServiceAccessMutex.RUnlock() + return fake.findServiceAccessArgsForCall[i].arg1, fake.findServiceAccessArgsForCall[i].arg2 +} + +func (fake *FakeServicePlanActor) FindServiceAccessReturns(result1 actors.ServiceAccess, result2 error) { + fake.FindServiceAccessStub = nil + fake.findServiceAccessReturns = struct { + result1 actors.ServiceAccess + result2 error + }{result1, result2} +} + +func (fake *FakeServicePlanActor) UpdateAllPlansForService(arg1 string, arg2 bool) (bool, error) { + fake.updateAllPlansForServiceMutex.Lock() + defer fake.updateAllPlansForServiceMutex.Unlock() + fake.updateAllPlansForServiceArgsForCall = append(fake.updateAllPlansForServiceArgsForCall, struct { + arg1 string + arg2 bool + }{arg1, arg2}) + if fake.UpdateAllPlansForServiceStub != nil { + return fake.UpdateAllPlansForServiceStub(arg1, arg2) + } else { + return fake.updateAllPlansForServiceReturns.result1, fake.updateAllPlansForServiceReturns.result2 + } +} + +func (fake *FakeServicePlanActor) UpdateAllPlansForServiceCallCount() int { + fake.updateAllPlansForServiceMutex.RLock() + defer fake.updateAllPlansForServiceMutex.RUnlock() + return len(fake.updateAllPlansForServiceArgsForCall) +} + +func (fake *FakeServicePlanActor) UpdateAllPlansForServiceArgsForCall(i int) (string, bool) { + fake.updateAllPlansForServiceMutex.RLock() + defer fake.updateAllPlansForServiceMutex.RUnlock() + return fake.updateAllPlansForServiceArgsForCall[i].arg1, fake.updateAllPlansForServiceArgsForCall[i].arg2 +} + +func (fake *FakeServicePlanActor) UpdateAllPlansForServiceReturns(result1 bool, result2 error) { + fake.UpdateAllPlansForServiceStub = nil + fake.updateAllPlansForServiceReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeServicePlanActor) UpdateOrgForService(arg1 string, arg2 string, arg3 bool) (bool, error) { + fake.updateOrgForServiceMutex.Lock() + defer fake.updateOrgForServiceMutex.Unlock() + fake.updateOrgForServiceArgsForCall = append(fake.updateOrgForServiceArgsForCall, struct { + arg1 string + arg2 string + arg3 bool + }{arg1, arg2, arg3}) + if fake.UpdateOrgForServiceStub != nil { + return fake.UpdateOrgForServiceStub(arg1, arg2, arg3) + } else { + return fake.updateOrgForServiceReturns.result1, fake.updateOrgForServiceReturns.result2 + } +} + +func (fake *FakeServicePlanActor) UpdateOrgForServiceCallCount() int { + fake.updateOrgForServiceMutex.RLock() + defer fake.updateOrgForServiceMutex.RUnlock() + return len(fake.updateOrgForServiceArgsForCall) +} + +func (fake *FakeServicePlanActor) UpdateOrgForServiceArgsForCall(i int) (string, string, bool) { + fake.updateOrgForServiceMutex.RLock() + defer fake.updateOrgForServiceMutex.RUnlock() + return fake.updateOrgForServiceArgsForCall[i].arg1, fake.updateOrgForServiceArgsForCall[i].arg2, fake.updateOrgForServiceArgsForCall[i].arg3 +} + +func (fake *FakeServicePlanActor) UpdateOrgForServiceReturns(result1 bool, result2 error) { + fake.UpdateOrgForServiceStub = nil + fake.updateOrgForServiceReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeServicePlanActor) UpdateSinglePlanForService(arg1 string, arg2 string, arg3 bool) (actors.PlanAccess, error) { + fake.updateSinglePlanForServiceMutex.Lock() + defer fake.updateSinglePlanForServiceMutex.Unlock() + fake.updateSinglePlanForServiceArgsForCall = append(fake.updateSinglePlanForServiceArgsForCall, struct { + arg1 string + arg2 string + arg3 bool + }{arg1, arg2, arg3}) + if fake.UpdateSinglePlanForServiceStub != nil { + return fake.UpdateSinglePlanForServiceStub(arg1, arg2, arg3) + } else { + return fake.updateSinglePlanForServiceReturns.result1, fake.updateSinglePlanForServiceReturns.result2 + } +} + +func (fake *FakeServicePlanActor) UpdateSinglePlanForServiceCallCount() int { + fake.updateSinglePlanForServiceMutex.RLock() + defer fake.updateSinglePlanForServiceMutex.RUnlock() + return len(fake.updateSinglePlanForServiceArgsForCall) +} + +func (fake *FakeServicePlanActor) UpdateSinglePlanForServiceArgsForCall(i int) (string, string, bool) { + fake.updateSinglePlanForServiceMutex.RLock() + defer fake.updateSinglePlanForServiceMutex.RUnlock() + return fake.updateSinglePlanForServiceArgsForCall[i].arg1, fake.updateSinglePlanForServiceArgsForCall[i].arg2, fake.updateSinglePlanForServiceArgsForCall[i].arg3 +} + +func (fake *FakeServicePlanActor) UpdateSinglePlanForServiceReturns(result1 actors.PlanAccess, result2 error) { + fake.UpdateSinglePlanForServiceStub = nil + fake.updateSinglePlanForServiceReturns = struct { + result1 actors.PlanAccess + result2 error + }{result1, result2} +} + +func (fake *FakeServicePlanActor) UpdatePlanAndOrgForService(arg1 string, arg2 string, arg3 string, arg4 bool) (actors.PlanAccess, error) { + fake.updatePlanAndOrgForServiceMutex.Lock() + defer fake.updatePlanAndOrgForServiceMutex.Unlock() + fake.updatePlanAndOrgForServiceArgsForCall = append(fake.updatePlanAndOrgForServiceArgsForCall, struct { + arg1 string + arg2 string + arg3 string + arg4 bool + }{arg1, arg2, arg3, arg4}) + if fake.UpdatePlanAndOrgForServiceStub != nil { + return fake.UpdatePlanAndOrgForServiceStub(arg1, arg2, arg3, arg4) + } else { + return fake.updatePlanAndOrgForServiceReturns.result1, fake.updatePlanAndOrgForServiceReturns.result2 + } +} + +func (fake *FakeServicePlanActor) UpdatePlanAndOrgForServiceCallCount() int { + fake.updatePlanAndOrgForServiceMutex.RLock() + defer fake.updatePlanAndOrgForServiceMutex.RUnlock() + return len(fake.updatePlanAndOrgForServiceArgsForCall) +} + +func (fake *FakeServicePlanActor) UpdatePlanAndOrgForServiceArgsForCall(i int) (string, string, string, bool) { + fake.updatePlanAndOrgForServiceMutex.RLock() + defer fake.updatePlanAndOrgForServiceMutex.RUnlock() + return fake.updatePlanAndOrgForServiceArgsForCall[i].arg1, fake.updatePlanAndOrgForServiceArgsForCall[i].arg2, fake.updatePlanAndOrgForServiceArgsForCall[i].arg3, fake.updatePlanAndOrgForServiceArgsForCall[i].arg4 +} + +func (fake *FakeServicePlanActor) UpdatePlanAndOrgForServiceReturns(result1 actors.PlanAccess, result2 error) { + fake.UpdatePlanAndOrgForServiceStub = nil + fake.updatePlanAndOrgForServiceReturns = struct { + result1 actors.PlanAccess + result2 error + }{result1, result2} +} + +var _ actors.ServicePlanActor = new(FakeServicePlanActor) diff --git a/cf/actors/plan_builder/fakes/fake_plan_builder.go b/cf/actors/plan_builder/fakes/fake_plan_builder.go new file mode 100644 index 00000000000..fa3015f7913 --- /dev/null +++ b/cf/actors/plan_builder/fakes/fake_plan_builder.go @@ -0,0 +1,263 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakePlanBuilder struct { + AttachOrgsToPlansStub func([]models.ServicePlanFields) ([]models.ServicePlanFields, error) + attachOrgsToPlansMutex sync.RWMutex + attachOrgsToPlansArgsForCall []struct { + arg1 []models.ServicePlanFields + } + attachOrgsToPlansReturns struct { + result1 []models.ServicePlanFields + result2 error + } + AttachOrgToPlansStub func([]models.ServicePlanFields, string) ([]models.ServicePlanFields, error) + attachOrgToPlansMutex sync.RWMutex + attachOrgToPlansArgsForCall []struct { + arg1 []models.ServicePlanFields + arg2 string + } + attachOrgToPlansReturns struct { + result1 []models.ServicePlanFields + result2 error + } + GetPlansForServiceForOrgStub func(string, string) ([]models.ServicePlanFields, error) + getPlansForServiceForOrgMutex sync.RWMutex + getPlansForServiceForOrgArgsForCall []struct { + arg1 string + arg2 string + } + getPlansForServiceForOrgReturns struct { + result1 []models.ServicePlanFields + result2 error + } + GetPlansForServiceWithOrgsStub func(string) ([]models.ServicePlanFields, error) + getPlansForServiceWithOrgsMutex sync.RWMutex + getPlansForServiceWithOrgsArgsForCall []struct { + arg1 string + } + getPlansForServiceWithOrgsReturns struct { + result1 []models.ServicePlanFields + result2 error + } + GetPlansForServiceStub func(string) ([]models.ServicePlanFields, error) + getPlansForServiceMutex sync.RWMutex + getPlansForServiceArgsForCall []struct { + arg1 string + } + getPlansForServiceReturns struct { + result1 []models.ServicePlanFields + result2 error + } + GetPlansVisibleToOrgStub func(string) ([]models.ServicePlanFields, error) + getPlansVisibleToOrgMutex sync.RWMutex + getPlansVisibleToOrgArgsForCall []struct { + arg1 string + } + getPlansVisibleToOrgReturns struct { + result1 []models.ServicePlanFields + result2 error + } +} + +func (fake *FakePlanBuilder) AttachOrgsToPlans(arg1 []models.ServicePlanFields) ([]models.ServicePlanFields, error) { + fake.attachOrgsToPlansMutex.Lock() + defer fake.attachOrgsToPlansMutex.Unlock() + fake.attachOrgsToPlansArgsForCall = append(fake.attachOrgsToPlansArgsForCall, struct { + arg1 []models.ServicePlanFields + }{arg1}) + if fake.AttachOrgsToPlansStub != nil { + return fake.AttachOrgsToPlansStub(arg1) + } else { + return fake.attachOrgsToPlansReturns.result1, fake.attachOrgsToPlansReturns.result2 + } +} + +func (fake *FakePlanBuilder) AttachOrgsToPlansCallCount() int { + fake.attachOrgsToPlansMutex.RLock() + defer fake.attachOrgsToPlansMutex.RUnlock() + return len(fake.attachOrgsToPlansArgsForCall) +} + +func (fake *FakePlanBuilder) AttachOrgsToPlansArgsForCall(i int) []models.ServicePlanFields { + fake.attachOrgsToPlansMutex.RLock() + defer fake.attachOrgsToPlansMutex.RUnlock() + return fake.attachOrgsToPlansArgsForCall[i].arg1 +} + +func (fake *FakePlanBuilder) AttachOrgsToPlansReturns(result1 []models.ServicePlanFields, result2 error) { + fake.attachOrgsToPlansReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +func (fake *FakePlanBuilder) AttachOrgToPlans(arg1 []models.ServicePlanFields, arg2 string) ([]models.ServicePlanFields, error) { + fake.attachOrgToPlansMutex.Lock() + defer fake.attachOrgToPlansMutex.Unlock() + fake.attachOrgToPlansArgsForCall = append(fake.attachOrgToPlansArgsForCall, struct { + arg1 []models.ServicePlanFields + arg2 string + }{arg1, arg2}) + if fake.AttachOrgToPlansStub != nil { + return fake.AttachOrgToPlansStub(arg1, arg2) + } else { + return fake.attachOrgToPlansReturns.result1, fake.attachOrgToPlansReturns.result2 + } +} + +func (fake *FakePlanBuilder) AttachOrgToPlansCallCount() int { + fake.attachOrgToPlansMutex.RLock() + defer fake.attachOrgToPlansMutex.RUnlock() + return len(fake.attachOrgToPlansArgsForCall) +} + +func (fake *FakePlanBuilder) AttachOrgToPlansArgsForCall(i int) ([]models.ServicePlanFields, string) { + fake.attachOrgToPlansMutex.RLock() + defer fake.attachOrgToPlansMutex.RUnlock() + return fake.attachOrgToPlansArgsForCall[i].arg1, fake.attachOrgToPlansArgsForCall[i].arg2 +} + +func (fake *FakePlanBuilder) AttachOrgToPlansReturns(result1 []models.ServicePlanFields, result2 error) { + fake.attachOrgToPlansReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +func (fake *FakePlanBuilder) GetPlansForServiceForOrg(arg1 string, arg2 string) ([]models.ServicePlanFields, error) { + fake.getPlansForServiceForOrgMutex.Lock() + defer fake.getPlansForServiceForOrgMutex.Unlock() + fake.getPlansForServiceForOrgArgsForCall = append(fake.getPlansForServiceForOrgArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetPlansForServiceForOrgStub != nil { + return fake.GetPlansForServiceForOrgStub(arg1, arg2) + } else { + return fake.getPlansForServiceForOrgReturns.result1, fake.getPlansForServiceForOrgReturns.result2 + } +} + +func (fake *FakePlanBuilder) GetPlansForServiceForOrgCallCount() int { + fake.getPlansForServiceForOrgMutex.RLock() + defer fake.getPlansForServiceForOrgMutex.RUnlock() + return len(fake.getPlansForServiceForOrgArgsForCall) +} + +func (fake *FakePlanBuilder) GetPlansForServiceForOrgArgsForCall(i int) (string, string) { + fake.getPlansForServiceForOrgMutex.RLock() + defer fake.getPlansForServiceForOrgMutex.RUnlock() + return fake.getPlansForServiceForOrgArgsForCall[i].arg1, fake.getPlansForServiceForOrgArgsForCall[i].arg2 +} + +func (fake *FakePlanBuilder) GetPlansForServiceForOrgReturns(result1 []models.ServicePlanFields, result2 error) { + fake.getPlansForServiceForOrgReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +func (fake *FakePlanBuilder) GetPlansForServiceWithOrgs(arg1 string) ([]models.ServicePlanFields, error) { + fake.getPlansForServiceWithOrgsMutex.Lock() + defer fake.getPlansForServiceWithOrgsMutex.Unlock() + fake.getPlansForServiceWithOrgsArgsForCall = append(fake.getPlansForServiceWithOrgsArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetPlansForServiceWithOrgsStub != nil { + return fake.GetPlansForServiceWithOrgsStub(arg1) + } else { + return fake.getPlansForServiceWithOrgsReturns.result1, fake.getPlansForServiceWithOrgsReturns.result2 + } +} + +func (fake *FakePlanBuilder) GetPlansForServiceWithOrgsCallCount() int { + fake.getPlansForServiceWithOrgsMutex.RLock() + defer fake.getPlansForServiceWithOrgsMutex.RUnlock() + return len(fake.getPlansForServiceWithOrgsArgsForCall) +} + +func (fake *FakePlanBuilder) GetPlansForServiceWithOrgsArgsForCall(i int) string { + fake.getPlansForServiceWithOrgsMutex.RLock() + defer fake.getPlansForServiceWithOrgsMutex.RUnlock() + return fake.getPlansForServiceWithOrgsArgsForCall[i].arg1 +} + +func (fake *FakePlanBuilder) GetPlansForServiceWithOrgsReturns(result1 []models.ServicePlanFields, result2 error) { + fake.getPlansForServiceWithOrgsReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +func (fake *FakePlanBuilder) GetPlansForService(arg1 string) ([]models.ServicePlanFields, error) { + fake.getPlansForServiceMutex.Lock() + defer fake.getPlansForServiceMutex.Unlock() + fake.getPlansForServiceArgsForCall = append(fake.getPlansForServiceArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetPlansForServiceStub != nil { + return fake.GetPlansForServiceStub(arg1) + } else { + return fake.getPlansForServiceReturns.result1, fake.getPlansForServiceReturns.result2 + } +} + +func (fake *FakePlanBuilder) GetPlansForServiceCallCount() int { + fake.getPlansForServiceMutex.RLock() + defer fake.getPlansForServiceMutex.RUnlock() + return len(fake.getPlansForServiceArgsForCall) +} + +func (fake *FakePlanBuilder) GetPlansForServiceArgsForCall(i int) string { + fake.getPlansForServiceMutex.RLock() + defer fake.getPlansForServiceMutex.RUnlock() + return fake.getPlansForServiceArgsForCall[i].arg1 +} + +func (fake *FakePlanBuilder) GetPlansForServiceReturns(result1 []models.ServicePlanFields, result2 error) { + fake.getPlansForServiceReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +func (fake *FakePlanBuilder) GetPlansVisibleToOrg(arg1 string) ([]models.ServicePlanFields, error) { + fake.getPlansVisibleToOrgMutex.Lock() + defer fake.getPlansVisibleToOrgMutex.Unlock() + fake.getPlansVisibleToOrgArgsForCall = append(fake.getPlansVisibleToOrgArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetPlansVisibleToOrgStub != nil { + return fake.GetPlansVisibleToOrgStub(arg1) + } else { + return fake.getPlansVisibleToOrgReturns.result1, fake.getPlansVisibleToOrgReturns.result2 + } +} + +func (fake *FakePlanBuilder) GetPlansVisibleToOrgCallCount() int { + fake.getPlansVisibleToOrgMutex.RLock() + defer fake.getPlansVisibleToOrgMutex.RUnlock() + return len(fake.getPlansVisibleToOrgArgsForCall) +} + +func (fake *FakePlanBuilder) GetPlansVisibleToOrgArgsForCall(i int) string { + fake.getPlansVisibleToOrgMutex.RLock() + defer fake.getPlansVisibleToOrgMutex.RUnlock() + return fake.getPlansVisibleToOrgArgsForCall[i].arg1 +} + +func (fake *FakePlanBuilder) GetPlansVisibleToOrgReturns(result1 []models.ServicePlanFields, result2 error) { + fake.getPlansVisibleToOrgReturns = struct { + result1 []models.ServicePlanFields + result2 error + }{result1, result2} +} + +var _ PlanBuilder = new(FakePlanBuilder) diff --git a/cf/actors/plan_builder/plan_builder.go b/cf/actors/plan_builder/plan_builder.go new file mode 100644 index 00000000000..ed3a3e1d39e --- /dev/null +++ b/cf/actors/plan_builder/plan_builder.go @@ -0,0 +1,196 @@ +package plan_builder + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/models" +) + +type PlanBuilder interface { + AttachOrgsToPlans([]models.ServicePlanFields) ([]models.ServicePlanFields, error) + AttachOrgToPlans([]models.ServicePlanFields, string) ([]models.ServicePlanFields, error) + GetPlansForServiceForOrg(string, string) ([]models.ServicePlanFields, error) + GetPlansForServiceWithOrgs(string) ([]models.ServicePlanFields, error) + GetPlansForService(string) ([]models.ServicePlanFields, error) + GetPlansVisibleToOrg(string) ([]models.ServicePlanFields, error) +} + +var ( + OrgToPlansVisibilityMap *map[string][]string + PlanToOrgsVisibilityMap *map[string][]string +) + +type Builder struct { + servicePlanRepo api.ServicePlanRepository + servicePlanVisibilityRepo api.ServicePlanVisibilityRepository + orgRepo organizations.OrganizationRepository +} + +func NewBuilder(plan api.ServicePlanRepository, vis api.ServicePlanVisibilityRepository, org organizations.OrganizationRepository) Builder { + return Builder{ + servicePlanRepo: plan, + servicePlanVisibilityRepo: vis, + orgRepo: org, + } +} + +func (builder Builder) AttachOrgToPlans(plans []models.ServicePlanFields, orgName string) ([]models.ServicePlanFields, error) { + visMap, err := builder.buildPlanToOrgVisibilityMap(orgName) + if err != nil { + return nil, err + } + for planIndex, _ := range plans { + plan := &plans[planIndex] + plan.OrgNames = visMap[plan.Guid] + } + + return plans, nil +} + +func (builder Builder) AttachOrgsToPlans(plans []models.ServicePlanFields) ([]models.ServicePlanFields, error) { + visMap, err := builder.buildPlanToOrgsVisibilityMap() + if err != nil { + return nil, err + } + for planIndex, _ := range plans { + plan := &plans[planIndex] + plan.OrgNames = visMap[plan.Guid] + } + + return plans, nil +} + +func (builder Builder) GetPlansForServiceForOrg(serviceGuid string, orgName string) ([]models.ServicePlanFields, error) { + plans, err := builder.servicePlanRepo.Search(map[string]string{"service_guid": serviceGuid}) + if err != nil { + return nil, err + } + + plans, err = builder.AttachOrgToPlans(plans, orgName) + if err != nil { + return nil, err + } + return plans, nil +} + +func (builder Builder) GetPlansForService(serviceGuid string) ([]models.ServicePlanFields, error) { + plans, err := builder.servicePlanRepo.Search(map[string]string{"service_guid": serviceGuid}) + if err != nil { + return nil, err + } + return plans, nil +} + +func (builder Builder) GetPlansForServiceWithOrgs(serviceGuid string) ([]models.ServicePlanFields, error) { + plans, err := builder.servicePlanRepo.Search(map[string]string{"service_guid": serviceGuid}) + if err != nil { + return nil, err + } + + plans, err = builder.AttachOrgsToPlans(plans) + if err != nil { + return nil, err + } + return plans, nil +} + +func (builder Builder) GetPlansVisibleToOrg(orgName string) ([]models.ServicePlanFields, error) { + var plansToReturn []models.ServicePlanFields + allPlans, err := builder.servicePlanRepo.Search(nil) + + planToOrgsVisMap, err := builder.buildPlanToOrgsVisibilityMap() + if err != nil { + return nil, err + } + + orgToPlansVisMap := builder.buildOrgToPlansVisibilityMap(planToOrgsVisMap) + + filterOrgPlans := orgToPlansVisMap[orgName] + + for _, plan := range allPlans { + if builder.containsGuid(filterOrgPlans, plan.Guid) { + plan.OrgNames = planToOrgsVisMap[plan.Guid] + plansToReturn = append(plansToReturn, plan) + } else if plan.Public { + plansToReturn = append(plansToReturn, plan) + } + } + + return plansToReturn, nil +} + +func (builder Builder) containsGuid(guidSlice []string, guid string) bool { + for _, g := range guidSlice { + if g == guid { + return true + } + } + return false +} + +func (builder Builder) buildPlanToOrgVisibilityMap(orgName string) (map[string][]string, error) { + // Since this map doesn't ever change, we memoize it for performance + orgLookup := make(map[string]string) + + org, err := builder.orgRepo.FindByName(orgName) + if err != nil { + return nil, err + } + orgLookup[org.Guid] = org.Name + + visibilities, err := builder.servicePlanVisibilityRepo.List() + if err != nil { + return nil, err + } + + visMap := make(map[string][]string) + for _, vis := range visibilities { + if _, exists := orgLookup[vis.OrganizationGuid]; exists { + visMap[vis.ServicePlanGuid] = append(visMap[vis.ServicePlanGuid], orgLookup[vis.OrganizationGuid]) + } + } + + return visMap, nil +} + +func (builder Builder) buildPlanToOrgsVisibilityMap() (map[string][]string, error) { + // Since this map doesn't ever change, we memoize it for performance + if PlanToOrgsVisibilityMap == nil { + orgLookup := make(map[string]string) + + orgs, err := builder.orgRepo.ListOrgs() + if err != nil { + return nil, err + } + for _, org := range orgs { + orgLookup[org.Guid] = org.Name + } + + visibilities, err := builder.servicePlanVisibilityRepo.List() + if err != nil { + return nil, err + } + + visMap := make(map[string][]string) + for _, vis := range visibilities { + visMap[vis.ServicePlanGuid] = append(visMap[vis.ServicePlanGuid], orgLookup[vis.OrganizationGuid]) + } + PlanToOrgsVisibilityMap = &visMap + } + + return *PlanToOrgsVisibilityMap, nil +} + +func (builder Builder) buildOrgToPlansVisibilityMap(planToOrgsMap map[string][]string) map[string][]string { + if OrgToPlansVisibilityMap == nil { + visMap := make(map[string][]string) + for planGuid, orgNames := range planToOrgsMap { + for _, orgName := range orgNames { + visMap[orgName] = append(visMap[orgName], planGuid) + } + } + OrgToPlansVisibilityMap = &visMap + } + + return *OrgToPlansVisibilityMap +} diff --git a/cf/actors/plan_builder/plan_builder_suite_test.go b/cf/actors/plan_builder/plan_builder_suite_test.go new file mode 100644 index 00000000000..cc6f5a31ecb --- /dev/null +++ b/cf/actors/plan_builder/plan_builder_suite_test.go @@ -0,0 +1,13 @@ +package plan_builder_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlanBuilder(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "PlanBuilder Suite") +} diff --git a/cf/actors/plan_builder/plan_builder_test.go b/cf/actors/plan_builder/plan_builder_test.go new file mode 100644 index 00000000000..496f98cb2d6 --- /dev/null +++ b/cf/actors/plan_builder/plan_builder_test.go @@ -0,0 +1,131 @@ +package plan_builder_test + +import ( + "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/api/fakes" + testorg "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Plan builder", func() { + var ( + builder plan_builder.PlanBuilder + + planRepo *fakes.FakeServicePlanRepo + visibilityRepo *fakes.FakeServicePlanVisibilityRepository + orgRepo *testorg.FakeOrganizationRepository + + plan1 models.ServicePlanFields + plan2 models.ServicePlanFields + + org1 models.Organization + org2 models.Organization + ) + + BeforeEach(func() { + planRepo = &fakes.FakeServicePlanRepo{} + visibilityRepo = &fakes.FakeServicePlanVisibilityRepository{} + orgRepo = &testorg.FakeOrganizationRepository{} + builder = plan_builder.NewBuilder(planRepo, visibilityRepo, orgRepo) + + plan1 = models.ServicePlanFields{ + Name: "service-plan1", + Guid: "service-plan1-guid", + ServiceOfferingGuid: "service-guid1", + } + + plan2 = models.ServicePlanFields{ + Name: "service-plan2", + Guid: "service-plan2-guid", + ServiceOfferingGuid: "service-guid1", + } + planRepo.SearchReturns = map[string][]models.ServicePlanFields{ + "service-guid1": []models.ServicePlanFields{plan1, plan2}, + } + org1 = models.Organization{} + org1.Name = "org1" + org1.Guid = "org1-guid" + + org2 = models.Organization{} + org2.Name = "org2" + org2.Guid = "org2-guid" + visibilityRepo.ListReturns([]models.ServicePlanVisibilityFields{ + {ServicePlanGuid: "service-plan1-guid", OrganizationGuid: "org1-guid"}, + {ServicePlanGuid: "service-plan1-guid", OrganizationGuid: "org2-guid"}, + }, nil) + orgRepo.ListOrgsReturns([]models.Organization{org1, org2}, nil) + }) + + Describe(".AttachOrgsToPlans", func() { + It("returns plans fully populated with the orgnames that have visibility", func() { + barePlans := []models.ServicePlanFields{plan1, plan2} + + plans, err := builder.AttachOrgsToPlans(barePlans) + Expect(err).ToNot(HaveOccurred()) + + Expect(plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + }) + + Describe(".AttachOrgToPlans", func() { + It("returns plans fully populated with the orgnames that have visibility", func() { + orgRepo.FindByNameReturns(org1, nil) + barePlans := []models.ServicePlanFields{plan1, plan2} + + plans, err := builder.AttachOrgToPlans(barePlans, "org1") + Expect(err).ToNot(HaveOccurred()) + + Expect(plans[0].OrgNames).To(Equal([]string{"org1"})) + }) + }) + + Describe(".GetPlansForServiceWithOrgs", func() { + It("returns all the plans for the service with the provided guid", func() { + plans, err := builder.GetPlansForServiceWithOrgs("service-guid1") + Expect(err).ToNot(HaveOccurred()) + + Expect(len(plans)).To(Equal(2)) + Expect(plans[0].Name).To(Equal("service-plan1")) + Expect(plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + Expect(plans[1].Name).To(Equal("service-plan2")) + }) + }) + + Describe(".GetPlansForService", func() { + It("returns all the plans for the service with the provided guid", func() { + plans, err := builder.GetPlansForService("service-guid1") + Expect(err).ToNot(HaveOccurred()) + + Expect(len(plans)).To(Equal(2)) + Expect(plans[0].Name).To(Equal("service-plan1")) + Expect(plans[0].OrgNames).To(BeNil()) + Expect(plans[1].Name).To(Equal("service-plan2")) + }) + }) + + Describe(".GetPlansForServiceForOrg", func() { + It("returns all the plans for the service with the provided guid", func() { + orgRepo.FindByNameReturns(org1, nil) + plans, err := builder.GetPlansForServiceForOrg("service-guid1", "org1") + Expect(err).ToNot(HaveOccurred()) + + Expect(len(plans)).To(Equal(2)) + Expect(plans[0].Name).To(Equal("service-plan1")) + Expect(plans[0].OrgNames).To(Equal([]string{"org1"})) + Expect(plans[1].Name).To(Equal("service-plan2")) + }) + }) + + Describe(".GetPlansVisibleToOrg", func() { + It("returns all the plans visible to the named org", func() { + plans, err := builder.GetPlansVisibleToOrg("org1") + Expect(err).ToNot(HaveOccurred()) + + Expect(len(plans)).To(Equal(1)) + Expect(plans[0].Name).To(Equal("service-plan1")) + Expect(plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + }) +}) diff --git a/cf/actors/push.go b/cf/actors/push.go new file mode 100644 index 00000000000..f81eee498b2 --- /dev/null +++ b/cf/actors/push.go @@ -0,0 +1,117 @@ +package actors + +import ( + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/api/application_bits" + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/gofileutils/fileutils" +) + +type PushActor interface { + UploadApp(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) error + GatherFiles(appDir string, uploadDir string) ([]resources.AppFileResource, error) +} + +type PushActorImpl struct { + appBitsRepo application_bits.ApplicationBitsRepository + appfiles app_files.AppFiles + zipper app_files.Zipper +} + +func NewPushActor(appBitsRepo application_bits.ApplicationBitsRepository, zipper app_files.Zipper, appfiles app_files.AppFiles) PushActor { + return PushActorImpl{ + appBitsRepo: appBitsRepo, + appfiles: appfiles, + zipper: zipper, + } +} + +func (actor PushActorImpl) GatherFiles(appDir string, uploadDir string) (presentFiles []resources.AppFileResource, apiErr error) { + if actor.zipper.IsZipFile(appDir) { + fileutils.TempDir("unzipped-app", func(tmpDir string, err error) { + err = actor.zipper.Unzip(appDir, tmpDir) + if err != nil { + presentFiles = nil + apiErr = err + return + } + presentFiles, apiErr = actor.copyUploadableFiles(tmpDir, uploadDir) + }) + } else { + presentFiles, apiErr = actor.copyUploadableFiles(appDir, uploadDir) + } + return presentFiles, apiErr +} + +func (actor PushActorImpl) UploadApp(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) error { + return actor.appBitsRepo.UploadBits(appGuid, zipFile, presentFiles) +} + +func (actor PushActorImpl) copyUploadableFiles(appDir string, uploadDir string) (presentFiles []resources.AppFileResource, err error) { + // Find which files need to be uploaded + allAppFiles, err := actor.appfiles.AppFilesInDir(appDir) + if err != nil { + return + } + + appFilesToUpload, presentFiles, apiErr := actor.getFilesToUpload(allAppFiles) + if apiErr != nil { + err = errors.New(apiErr.Error()) + return + } + + // Copy files into a temporary directory and return it + err = actor.appfiles.CopyFiles(appFilesToUpload, appDir, uploadDir) + if err != nil { + return + } + + // copy cfignore if present + fileutils.CopyPathToPath(filepath.Join(appDir, ".cfignore"), filepath.Join(uploadDir, ".cfignore")) //error handling? + + return +} + +func (actor PushActorImpl) getFilesToUpload(allAppFiles []models.AppFileFields) (appFilesToUpload []models.AppFileFields, presentFiles []resources.AppFileResource, apiErr error) { + appFilesRequest := []resources.AppFileResource{} + for _, file := range allAppFiles { + appFilesRequest = append(appFilesRequest, resources.AppFileResource{ + Path: file.Path, + Sha1: file.Sha1, + Size: file.Size, + }) + } + + presentFiles, apiErr = actor.appBitsRepo.GetApplicationFiles(appFilesRequest) + if apiErr != nil { + return nil, nil, apiErr + } + + appFilesToUpload = make([]models.AppFileFields, len(allAppFiles)) + copy(appFilesToUpload, allAppFiles) + for _, file := range presentFiles { + appFile := models.AppFileFields{ + Path: file.Path, + Sha1: file.Sha1, + Size: file.Size, + } + appFilesToUpload = actor.deleteAppFile(appFilesToUpload, appFile) + } + + return +} + +func (actor PushActorImpl) deleteAppFile(appFiles []models.AppFileFields, targetFile models.AppFileFields) []models.AppFileFields { + for i, file := range appFiles { + if file.Path == targetFile.Path { + appFiles[i] = appFiles[len(appFiles)-1] + return appFiles[:len(appFiles)-1] + } + } + return appFiles +} diff --git a/cf/actors/push_test.go b/cf/actors/push_test.go new file mode 100644 index 00000000000..7e845df40c9 --- /dev/null +++ b/cf/actors/push_test.go @@ -0,0 +1,142 @@ +package actors_test + +import ( + "errors" + "github.com/cloudfoundry/cli/cf/actors" + fakeBits "github.com/cloudfoundry/cli/cf/api/application_bits/fakes" + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/app_files/fakes" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/gofileutils/fileutils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "os" + "path/filepath" +) + +var _ = Describe("Push Actor", func() { + var ( + appBitsRepo *fakeBits.FakeApplicationBitsRepository + appFiles *fakes.FakeAppFiles + zipper *fakes.FakeZipper + actor actors.PushActor + fixturesDir string + appDir string + allFiles []models.AppFileFields + presentFiles []resources.AppFileResource + ) + + BeforeEach(func() { + appBitsRepo = &fakeBits.FakeApplicationBitsRepository{} + appFiles = &fakes.FakeAppFiles{} + zipper = &fakes.FakeZipper{} + actor = actors.NewPushActor(appBitsRepo, zipper, appFiles) + fixturesDir = filepath.Join("..", "..", "fixtures", "applications") + }) + + Describe("GatherFiles", func() { + BeforeEach(func() { + allFiles = []models.AppFileFields{ + models.AppFileFields{Path: "example-app/.cfignore"}, + models.AppFileFields{Path: "example-app/app.rb"}, + models.AppFileFields{Path: "example-app/config.ru"}, + models.AppFileFields{Path: "example-app/Gemfile"}, + models.AppFileFields{Path: "example-app/Gemfile.lock"}, + models.AppFileFields{Path: "example-app/ignore-me"}, + models.AppFileFields{Path: "example-app/manifest.yml"}, + } + + presentFiles = []resources.AppFileResource{ + resources.AppFileResource{Path: "example-app/ignore-me"}, + } + + appDir = filepath.Join(fixturesDir, "example-app.zip") + zipper.UnzipReturns(nil) + appFiles.AppFilesInDirReturns(allFiles, nil) + appBitsRepo.GetApplicationFilesReturns(presentFiles, nil) + }) + + AfterEach(func() { + }) + + Context("when the input is a zipfile", func() { + BeforeEach(func() { + zipper.IsZipFileReturns(true) + }) + + It("extracts the zip", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + files, err := actor.GatherFiles(appDir, tmpDir) + Expect(zipper.UnzipCallCount()).To(Equal(1)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(Equal(presentFiles)) + }) + }) + + }) + + Context("when the input is a directory full of files", func() { + BeforeEach(func() { + zipper.IsZipFileReturns(false) + }) + + It("does not try to unzip the directory", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + files, err := actor.GatherFiles(appDir, tmpDir) + Expect(zipper.UnzipCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(Equal(presentFiles)) + }) + }) + }) + + Context("when errors occur", func() { + It("returns an error if it cannot unzip the files", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + zipper.IsZipFileReturns(true) + zipper.UnzipReturns(errors.New("error")) + _, err = actor.GatherFiles(appDir, tmpDir) + Expect(err).To(HaveOccurred()) + }) + }) + + It("returns an error if it cannot walk the files", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + appFiles.AppFilesInDirReturns(nil, errors.New("error")) + _, err = actor.GatherFiles(appDir, tmpDir) + Expect(err).To(HaveOccurred()) + }) + }) + + It("returns an error if we cannot reach the cc", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + appBitsRepo.GetApplicationFilesReturns(nil, errors.New("error")) + _, err = actor.GatherFiles(appDir, tmpDir) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("when using .cfignore", func() { + BeforeEach(func() { + appBitsRepo.GetApplicationFilesReturns(nil, nil) + appDir = filepath.Join(fixturesDir, "exclude-a-default-cfignore") + }) + + It("includes the .cfignore file in the upload directory", func() { + fileutils.TempDir("gather-files", func(tmpDir string, err error) { + files, err := actor.GatherFiles(appDir, tmpDir) + Expect(err).NotTo(HaveOccurred()) + + _, err = os.Stat(filepath.Join(tmpDir, ".cfignore")) + Expect(os.IsNotExist(err)).To(BeFalse()) + Expect(len(files)).To(Equal(0)) + }) + }) + }) + }) + + Describe(".UploadApp", func() { + It("Simply delegates to the UploadApp function on the app bits repo, which is not worth testing", func() {}) + }) +}) diff --git a/cf/actors/routes.go b/cf/actors/routes.go new file mode 100644 index 00000000000..4f75d67edaa --- /dev/null +++ b/cf/actors/routes.go @@ -0,0 +1,67 @@ +package actors + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type RouteActor struct { + ui terminal.UI + routeRepo api.RouteRepository +} + +func NewRouteActor(ui terminal.UI, routeRepo api.RouteRepository) RouteActor { + return RouteActor{ui: ui, routeRepo: routeRepo} +} + +func (routeActor RouteActor) FindOrCreateRoute(hostname string, domain models.DomainFields) (route models.Route) { + route, apiErr := routeActor.routeRepo.FindByHostAndDomain(hostname, domain) + + switch apiErr.(type) { + case nil: + routeActor.ui.Say(T("Using route {{.RouteURL}}", map[string]interface{}{"RouteURL": terminal.EntityNameColor(route.URL())})) + case *errors.ModelNotFoundError: + routeActor.ui.Say(T("Creating route {{.Hostname}}...", map[string]interface{}{"Hostname": terminal.EntityNameColor(domain.UrlForHost(hostname))})) + + route, apiErr = routeActor.routeRepo.Create(hostname, domain) + if apiErr != nil { + routeActor.ui.Failed(apiErr.Error()) + } + + routeActor.ui.Ok() + routeActor.ui.Say("") + default: + routeActor.ui.Failed(apiErr.Error()) + } + + return +} + +func (routeActor RouteActor) BindRoute(app models.Application, route models.Route) { + if !app.HasRoute(route) { + routeActor.ui.Say(T("Binding {{.URL}} to {{.AppName}}...", map[string]interface{}{"URL": terminal.EntityNameColor(route.URL()), "AppName": terminal.EntityNameColor(app.Name)})) + + apiErr := routeActor.routeRepo.Bind(route.Guid, app.Guid) + switch apiErr := apiErr.(type) { + case nil: + routeActor.ui.Ok() + routeActor.ui.Say("") + return + case errors.HttpError: + if apiErr.ErrorCode() == errors.INVALID_RELATION { + routeActor.ui.Failed(T("The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", map[string]interface{}{"URL": route.URL()})) + } + } + routeActor.ui.Failed(apiErr.Error()) + } +} + +func (routeActor RouteActor) UnbindAll(app models.Application) { + for _, route := range app.Routes { + routeActor.ui.Say(T("Removing route {{.URL}}...", map[string]interface{}{"URL": terminal.EntityNameColor(route.URL())})) + routeActor.routeRepo.Unbind(route.Guid, app.Guid) + } +} diff --git a/cf/actors/service_builder/fakes/fake_service_builder.go b/cf/actors/service_builder/fakes/fake_service_builder.go new file mode 100644 index 00000000000..95fb789a8f7 --- /dev/null +++ b/cf/actors/service_builder/fakes/fake_service_builder.go @@ -0,0 +1,536 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeServiceBuilder struct { + GetAllServicesStub func() ([]models.ServiceOffering, error) + getAllServicesMutex sync.RWMutex + getAllServicesArgsForCall []struct{} + getAllServicesReturns struct { + result1 []models.ServiceOffering + result2 error + } + GetAllServicesWithPlansStub func() ([]models.ServiceOffering, error) + getAllServicesWithPlansMutex sync.RWMutex + getAllServicesWithPlansArgsForCall []struct{} + getAllServicesWithPlansReturns struct { + result1 []models.ServiceOffering + result2 error + } + GetServiceByNameWithPlansStub func(string) (models.ServiceOffering, error) + getServiceByNameWithPlansMutex sync.RWMutex + getServiceByNameWithPlansArgsForCall []struct { + arg1 string + } + getServiceByNameWithPlansReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServiceByNameWithPlansWithOrgNamesStub func(string) (models.ServiceOffering, error) + getServiceByNameWithPlansWithOrgNamesMutex sync.RWMutex + getServiceByNameWithPlansWithOrgNamesArgsForCall []struct { + arg1 string + } + getServiceByNameWithPlansWithOrgNamesReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServiceByNameForSpaceStub func(string, string) (models.ServiceOffering, error) + getServiceByNameForSpaceMutex sync.RWMutex + getServiceByNameForSpaceArgsForCall []struct { + arg1 string + arg2 string + } + getServiceByNameForSpaceReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServiceByNameForSpaceWithPlansStub func(string, string) (models.ServiceOffering, error) + getServiceByNameForSpaceWithPlansMutex sync.RWMutex + getServiceByNameForSpaceWithPlansArgsForCall []struct { + arg1 string + arg2 string + } + getServiceByNameForSpaceWithPlansReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServicesByNameForSpaceWithPlansStub func(string, string) (models.ServiceOfferings, error) + getServicesByNameForSpaceWithPlansMutex sync.RWMutex + getServicesByNameForSpaceWithPlansArgsForCall []struct { + arg1 string + arg2 string + } + getServicesByNameForSpaceWithPlansReturns struct { + result1 models.ServiceOfferings + result2 error + } + GetServiceByNameForOrgStub func(string, string) (models.ServiceOffering, error) + getServiceByNameForOrgMutex sync.RWMutex + getServiceByNameForOrgArgsForCall []struct { + arg1 string + arg2 string + } + getServiceByNameForOrgReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServicesForBrokerStub func(string) ([]models.ServiceOffering, error) + getServicesForBrokerMutex sync.RWMutex + getServicesForBrokerArgsForCall []struct { + arg1 string + } + getServicesForBrokerReturns struct { + result1 []models.ServiceOffering + result2 error + } + GetServicesForSpaceStub func(string) ([]models.ServiceOffering, error) + getServicesForSpaceMutex sync.RWMutex + getServicesForSpaceArgsForCall []struct { + arg1 string + } + getServicesForSpaceReturns struct { + result1 []models.ServiceOffering + result2 error + } + GetServicesForSpaceWithPlansStub func(string) ([]models.ServiceOffering, error) + getServicesForSpaceWithPlansMutex sync.RWMutex + getServicesForSpaceWithPlansArgsForCall []struct { + arg1 string + } + getServicesForSpaceWithPlansReturns struct { + result1 []models.ServiceOffering + result2 error + } + GetServiceVisibleToOrgStub func(string, string) (models.ServiceOffering, error) + getServiceVisibleToOrgMutex sync.RWMutex + getServiceVisibleToOrgArgsForCall []struct { + arg1 string + arg2 string + } + getServiceVisibleToOrgReturns struct { + result1 models.ServiceOffering + result2 error + } + GetServicesVisibleToOrgStub func(string) ([]models.ServiceOffering, error) + getServicesVisibleToOrgMutex sync.RWMutex + getServicesVisibleToOrgArgsForCall []struct { + arg1 string + } + getServicesVisibleToOrgReturns struct { + result1 []models.ServiceOffering + result2 error + } +} + +func (fake *FakeServiceBuilder) GetAllServices() ([]models.ServiceOffering, error) { + fake.getAllServicesMutex.Lock() + defer fake.getAllServicesMutex.Unlock() + fake.getAllServicesArgsForCall = append(fake.getAllServicesArgsForCall, struct{}{}) + if fake.GetAllServicesStub != nil { + return fake.GetAllServicesStub() + } else { + return fake.getAllServicesReturns.result1, fake.getAllServicesReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetAllServicesCallCount() int { + fake.getAllServicesMutex.RLock() + defer fake.getAllServicesMutex.RUnlock() + return len(fake.getAllServicesArgsForCall) +} + +func (fake *FakeServiceBuilder) GetAllServicesReturns(result1 []models.ServiceOffering, result2 error) { + fake.getAllServicesReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetAllServicesWithPlans() ([]models.ServiceOffering, error) { + fake.getAllServicesWithPlansMutex.Lock() + defer fake.getAllServicesWithPlansMutex.Unlock() + fake.getAllServicesWithPlansArgsForCall = append(fake.getAllServicesWithPlansArgsForCall, struct{}{}) + if fake.GetAllServicesWithPlansStub != nil { + return fake.GetAllServicesWithPlansStub() + } else { + return fake.getAllServicesWithPlansReturns.result1, fake.getAllServicesWithPlansReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetAllServicesWithPlansCallCount() int { + fake.getAllServicesWithPlansMutex.RLock() + defer fake.getAllServicesWithPlansMutex.RUnlock() + return len(fake.getAllServicesWithPlansArgsForCall) +} + +func (fake *FakeServiceBuilder) GetAllServicesWithPlansReturns(result1 []models.ServiceOffering, result2 error) { + fake.getAllServicesWithPlansReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlans(arg1 string) (models.ServiceOffering, error) { + fake.getServiceByNameWithPlansMutex.Lock() + defer fake.getServiceByNameWithPlansMutex.Unlock() + fake.getServiceByNameWithPlansArgsForCall = append(fake.getServiceByNameWithPlansArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServiceByNameWithPlansStub != nil { + return fake.GetServiceByNameWithPlansStub(arg1) + } else { + return fake.getServiceByNameWithPlansReturns.result1, fake.getServiceByNameWithPlansReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansCallCount() int { + fake.getServiceByNameWithPlansMutex.RLock() + defer fake.getServiceByNameWithPlansMutex.RUnlock() + return len(fake.getServiceByNameWithPlansArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansArgsForCall(i int) string { + fake.getServiceByNameWithPlansMutex.RLock() + defer fake.getServiceByNameWithPlansMutex.RUnlock() + return fake.getServiceByNameWithPlansArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceByNameWithPlansReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansWithOrgNames(arg1 string) (models.ServiceOffering, error) { + fake.getServiceByNameWithPlansWithOrgNamesMutex.Lock() + defer fake.getServiceByNameWithPlansWithOrgNamesMutex.Unlock() + fake.getServiceByNameWithPlansWithOrgNamesArgsForCall = append(fake.getServiceByNameWithPlansWithOrgNamesArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServiceByNameWithPlansWithOrgNamesStub != nil { + return fake.GetServiceByNameWithPlansWithOrgNamesStub(arg1) + } else { + return fake.getServiceByNameWithPlansWithOrgNamesReturns.result1, fake.getServiceByNameWithPlansWithOrgNamesReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansWithOrgNamesCallCount() int { + fake.getServiceByNameWithPlansWithOrgNamesMutex.RLock() + defer fake.getServiceByNameWithPlansWithOrgNamesMutex.RUnlock() + return len(fake.getServiceByNameWithPlansWithOrgNamesArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansWithOrgNamesArgsForCall(i int) string { + fake.getServiceByNameWithPlansWithOrgNamesMutex.RLock() + defer fake.getServiceByNameWithPlansWithOrgNamesMutex.RUnlock() + return fake.getServiceByNameWithPlansWithOrgNamesArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServiceByNameWithPlansWithOrgNamesReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceByNameWithPlansWithOrgNamesReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpace(arg1 string, arg2 string) (models.ServiceOffering, error) { + fake.getServiceByNameForSpaceMutex.Lock() + defer fake.getServiceByNameForSpaceMutex.Unlock() + fake.getServiceByNameForSpaceArgsForCall = append(fake.getServiceByNameForSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetServiceByNameForSpaceStub != nil { + return fake.GetServiceByNameForSpaceStub(arg1, arg2) + } else { + return fake.getServiceByNameForSpaceReturns.result1, fake.getServiceByNameForSpaceReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceCallCount() int { + fake.getServiceByNameForSpaceMutex.RLock() + defer fake.getServiceByNameForSpaceMutex.RUnlock() + return len(fake.getServiceByNameForSpaceArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceArgsForCall(i int) (string, string) { + fake.getServiceByNameForSpaceMutex.RLock() + defer fake.getServiceByNameForSpaceMutex.RUnlock() + return fake.getServiceByNameForSpaceArgsForCall[i].arg1, fake.getServiceByNameForSpaceArgsForCall[i].arg2 +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceByNameForSpaceReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceWithPlans(arg1 string, arg2 string) (models.ServiceOffering, error) { + fake.getServiceByNameForSpaceWithPlansMutex.Lock() + defer fake.getServiceByNameForSpaceWithPlansMutex.Unlock() + fake.getServiceByNameForSpaceWithPlansArgsForCall = append(fake.getServiceByNameForSpaceWithPlansArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetServiceByNameForSpaceWithPlansStub != nil { + return fake.GetServiceByNameForSpaceWithPlansStub(arg1, arg2) + } else { + return fake.getServiceByNameForSpaceWithPlansReturns.result1, fake.getServiceByNameForSpaceWithPlansReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceWithPlansCallCount() int { + fake.getServiceByNameForSpaceWithPlansMutex.RLock() + defer fake.getServiceByNameForSpaceWithPlansMutex.RUnlock() + return len(fake.getServiceByNameForSpaceWithPlansArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceWithPlansArgsForCall(i int) (string, string) { + fake.getServiceByNameForSpaceWithPlansMutex.RLock() + defer fake.getServiceByNameForSpaceWithPlansMutex.RUnlock() + return fake.getServiceByNameForSpaceWithPlansArgsForCall[i].arg1, fake.getServiceByNameForSpaceWithPlansArgsForCall[i].arg2 +} + +func (fake *FakeServiceBuilder) GetServiceByNameForSpaceWithPlansReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceByNameForSpaceWithPlansReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServicesByNameForSpaceWithPlans(arg1 string, arg2 string) (models.ServiceOfferings, error) { + fake.getServicesByNameForSpaceWithPlansMutex.Lock() + defer fake.getServicesByNameForSpaceWithPlansMutex.Unlock() + fake.getServicesByNameForSpaceWithPlansArgsForCall = append(fake.getServicesByNameForSpaceWithPlansArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetServicesByNameForSpaceWithPlansStub != nil { + return fake.GetServicesByNameForSpaceWithPlansStub(arg1, arg2) + } else { + return fake.getServicesByNameForSpaceWithPlansReturns.result1, fake.getServicesByNameForSpaceWithPlansReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServicesByNameForSpaceWithPlansCallCount() int { + fake.getServicesByNameForSpaceWithPlansMutex.RLock() + defer fake.getServicesByNameForSpaceWithPlansMutex.RUnlock() + return len(fake.getServicesByNameForSpaceWithPlansArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServicesByNameForSpaceWithPlansArgsForCall(i int) (string, string) { + fake.getServicesByNameForSpaceWithPlansMutex.RLock() + defer fake.getServicesByNameForSpaceWithPlansMutex.RUnlock() + return fake.getServicesByNameForSpaceWithPlansArgsForCall[i].arg1, fake.getServicesByNameForSpaceWithPlansArgsForCall[i].arg2 +} + +func (fake *FakeServiceBuilder) GetServicesByNameForSpaceWithPlansReturns(result1 models.ServiceOfferings, result2 error) { + fake.getServicesByNameForSpaceWithPlansReturns = struct { + result1 models.ServiceOfferings + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceByNameForOrg(arg1 string, arg2 string) (models.ServiceOffering, error) { + fake.getServiceByNameForOrgMutex.Lock() + defer fake.getServiceByNameForOrgMutex.Unlock() + fake.getServiceByNameForOrgArgsForCall = append(fake.getServiceByNameForOrgArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetServiceByNameForOrgStub != nil { + return fake.GetServiceByNameForOrgStub(arg1, arg2) + } else { + return fake.getServiceByNameForOrgReturns.result1, fake.getServiceByNameForOrgReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceByNameForOrgCallCount() int { + fake.getServiceByNameForOrgMutex.RLock() + defer fake.getServiceByNameForOrgMutex.RUnlock() + return len(fake.getServiceByNameForOrgArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceByNameForOrgArgsForCall(i int) (string, string) { + fake.getServiceByNameForOrgMutex.RLock() + defer fake.getServiceByNameForOrgMutex.RUnlock() + return fake.getServiceByNameForOrgArgsForCall[i].arg1, fake.getServiceByNameForOrgArgsForCall[i].arg2 +} + +func (fake *FakeServiceBuilder) GetServiceByNameForOrgReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceByNameForOrgReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServicesForBroker(arg1 string) ([]models.ServiceOffering, error) { + fake.getServicesForBrokerMutex.Lock() + defer fake.getServicesForBrokerMutex.Unlock() + fake.getServicesForBrokerArgsForCall = append(fake.getServicesForBrokerArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServicesForBrokerStub != nil { + return fake.GetServicesForBrokerStub(arg1) + } else { + return fake.getServicesForBrokerReturns.result1, fake.getServicesForBrokerReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServicesForBrokerCallCount() int { + fake.getServicesForBrokerMutex.RLock() + defer fake.getServicesForBrokerMutex.RUnlock() + return len(fake.getServicesForBrokerArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServicesForBrokerArgsForCall(i int) string { + fake.getServicesForBrokerMutex.RLock() + defer fake.getServicesForBrokerMutex.RUnlock() + return fake.getServicesForBrokerArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServicesForBrokerReturns(result1 []models.ServiceOffering, result2 error) { + fake.getServicesForBrokerReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServicesForSpace(arg1 string) ([]models.ServiceOffering, error) { + fake.getServicesForSpaceMutex.Lock() + defer fake.getServicesForSpaceMutex.Unlock() + fake.getServicesForSpaceArgsForCall = append(fake.getServicesForSpaceArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServicesForSpaceStub != nil { + return fake.GetServicesForSpaceStub(arg1) + } else { + return fake.getServicesForSpaceReturns.result1, fake.getServicesForSpaceReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceCallCount() int { + fake.getServicesForSpaceMutex.RLock() + defer fake.getServicesForSpaceMutex.RUnlock() + return len(fake.getServicesForSpaceArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceArgsForCall(i int) string { + fake.getServicesForSpaceMutex.RLock() + defer fake.getServicesForSpaceMutex.RUnlock() + return fake.getServicesForSpaceArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceReturns(result1 []models.ServiceOffering, result2 error) { + fake.getServicesForSpaceReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceWithPlans(arg1 string) ([]models.ServiceOffering, error) { + fake.getServicesForSpaceWithPlansMutex.Lock() + defer fake.getServicesForSpaceWithPlansMutex.Unlock() + fake.getServicesForSpaceWithPlansArgsForCall = append(fake.getServicesForSpaceWithPlansArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServicesForSpaceWithPlansStub != nil { + return fake.GetServicesForSpaceWithPlansStub(arg1) + } else { + return fake.getServicesForSpaceWithPlansReturns.result1, fake.getServicesForSpaceWithPlansReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceWithPlansCallCount() int { + fake.getServicesForSpaceWithPlansMutex.RLock() + defer fake.getServicesForSpaceWithPlansMutex.RUnlock() + return len(fake.getServicesForSpaceWithPlansArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceWithPlansArgsForCall(i int) string { + fake.getServicesForSpaceWithPlansMutex.RLock() + defer fake.getServicesForSpaceWithPlansMutex.RUnlock() + return fake.getServicesForSpaceWithPlansArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServicesForSpaceWithPlansReturns(result1 []models.ServiceOffering, result2 error) { + fake.getServicesForSpaceWithPlansReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServiceVisibleToOrg(arg1 string, arg2 string) (models.ServiceOffering, error) { + fake.getServiceVisibleToOrgMutex.Lock() + defer fake.getServiceVisibleToOrgMutex.Unlock() + fake.getServiceVisibleToOrgArgsForCall = append(fake.getServiceVisibleToOrgArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.GetServiceVisibleToOrgStub != nil { + return fake.GetServiceVisibleToOrgStub(arg1, arg2) + } else { + return fake.getServiceVisibleToOrgReturns.result1, fake.getServiceVisibleToOrgReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServiceVisibleToOrgCallCount() int { + fake.getServiceVisibleToOrgMutex.RLock() + defer fake.getServiceVisibleToOrgMutex.RUnlock() + return len(fake.getServiceVisibleToOrgArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServiceVisibleToOrgArgsForCall(i int) (string, string) { + fake.getServiceVisibleToOrgMutex.RLock() + defer fake.getServiceVisibleToOrgMutex.RUnlock() + return fake.getServiceVisibleToOrgArgsForCall[i].arg1, fake.getServiceVisibleToOrgArgsForCall[i].arg2 +} + +func (fake *FakeServiceBuilder) GetServiceVisibleToOrgReturns(result1 models.ServiceOffering, result2 error) { + fake.getServiceVisibleToOrgReturns = struct { + result1 models.ServiceOffering + result2 error + }{result1, result2} +} + +func (fake *FakeServiceBuilder) GetServicesVisibleToOrg(arg1 string) ([]models.ServiceOffering, error) { + fake.getServicesVisibleToOrgMutex.Lock() + defer fake.getServicesVisibleToOrgMutex.Unlock() + fake.getServicesVisibleToOrgArgsForCall = append(fake.getServicesVisibleToOrgArgsForCall, struct { + arg1 string + }{arg1}) + if fake.GetServicesVisibleToOrgStub != nil { + return fake.GetServicesVisibleToOrgStub(arg1) + } else { + return fake.getServicesVisibleToOrgReturns.result1, fake.getServicesVisibleToOrgReturns.result2 + } +} + +func (fake *FakeServiceBuilder) GetServicesVisibleToOrgCallCount() int { + fake.getServicesVisibleToOrgMutex.RLock() + defer fake.getServicesVisibleToOrgMutex.RUnlock() + return len(fake.getServicesVisibleToOrgArgsForCall) +} + +func (fake *FakeServiceBuilder) GetServicesVisibleToOrgArgsForCall(i int) string { + fake.getServicesVisibleToOrgMutex.RLock() + defer fake.getServicesVisibleToOrgMutex.RUnlock() + return fake.getServicesVisibleToOrgArgsForCall[i].arg1 +} + +func (fake *FakeServiceBuilder) GetServicesVisibleToOrgReturns(result1 []models.ServiceOffering, result2 error) { + fake.getServicesVisibleToOrgReturns = struct { + result1 []models.ServiceOffering + result2 error + }{result1, result2} +} + +var _ ServiceBuilder = new(FakeServiceBuilder) diff --git a/cf/actors/service_builder/service_builder.go b/cf/actors/service_builder/service_builder.go new file mode 100644 index 00000000000..417d7e5f0cd --- /dev/null +++ b/cf/actors/service_builder/service_builder.go @@ -0,0 +1,284 @@ +package service_builder + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" +) + +type ServiceBuilder interface { + GetAllServices() ([]models.ServiceOffering, error) + GetAllServicesWithPlans() ([]models.ServiceOffering, error) + + GetServiceByNameWithPlans(string) (models.ServiceOffering, error) + GetServiceByNameWithPlansWithOrgNames(string) (models.ServiceOffering, error) + GetServiceByNameForSpace(string, string) (models.ServiceOffering, error) + GetServiceByNameForSpaceWithPlans(string, string) (models.ServiceOffering, error) + GetServicesByNameForSpaceWithPlans(string, string) (models.ServiceOfferings, error) + GetServiceByNameForOrg(string, string) (models.ServiceOffering, error) + + GetServicesForBroker(string) ([]models.ServiceOffering, error) + + GetServicesForSpace(string) ([]models.ServiceOffering, error) + GetServicesForSpaceWithPlans(string) ([]models.ServiceOffering, error) + + GetServiceVisibleToOrg(string, string) (models.ServiceOffering, error) + GetServicesVisibleToOrg(string) ([]models.ServiceOffering, error) +} + +type Builder struct { + serviceRepo api.ServiceRepository + planBuilder plan_builder.PlanBuilder +} + +func NewBuilder(service api.ServiceRepository, planBuilder plan_builder.PlanBuilder) Builder { + return Builder{ + serviceRepo: service, + planBuilder: planBuilder, + } +} + +func (builder Builder) GetAllServices() ([]models.ServiceOffering, error) { + return builder.serviceRepo.GetAllServiceOfferings() +} + +func (builder Builder) GetAllServicesWithPlans() ([]models.ServiceOffering, error) { + services, err := builder.GetAllServices() + if err != nil { + return []models.ServiceOffering{}, err + } + + var plans []models.ServicePlanFields + for index, service := range services { + plans, err = builder.planBuilder.GetPlansForService(service.Guid) + if err != nil { + return []models.ServiceOffering{}, err + } + services[index].Plans = plans + } + + return services, err +} + +func (builder Builder) GetServicesForSpace(spaceGuid string) ([]models.ServiceOffering, error) { + return builder.serviceRepo.GetServiceOfferingsForSpace(spaceGuid) +} + +func (builder Builder) GetServicesForSpaceWithPlans(spaceGuid string) ([]models.ServiceOffering, error) { + services, err := builder.GetServicesForSpace(spaceGuid) + if err != nil { + return []models.ServiceOffering{}, err + } + + for index, service := range services { + services[index].Plans, err = builder.planBuilder.GetPlansForService(service.Guid) + if err != nil { + return []models.ServiceOffering{}, err + } + } + + return services, nil +} + +func (builder Builder) GetServiceByNameWithPlans(serviceLabel string) (models.ServiceOffering, error) { + services, err := builder.serviceRepo.FindServiceOfferingsByLabel(serviceLabel) + if err != nil { + return models.ServiceOffering{}, err + } + service := returnV2Service(services) + + service.Plans, err = builder.planBuilder.GetPlansForService(service.Guid) + if err != nil { + return models.ServiceOffering{}, err + } + + return service, nil +} + +func (builder Builder) GetServiceByNameForOrg(serviceLabel, orgName string) (models.ServiceOffering, error) { + services, err := builder.serviceRepo.FindServiceOfferingsByLabel(serviceLabel) + if err != nil { + return models.ServiceOffering{}, err + } + + service, err := builder.attachPlansToServiceForOrg(services[0], orgName) + if err != nil { + return models.ServiceOffering{}, err + } + return service, nil +} + +func (builder Builder) GetServiceByNameForSpace(serviceLabel, spaceGuid string) (models.ServiceOffering, error) { + offerings, err := builder.serviceRepo.FindServiceOfferingsForSpaceByLabel(spaceGuid, serviceLabel) + if err != nil { + return models.ServiceOffering{}, err + } + + for _, offering := range offerings { + if offering.Provider == "" { + return offering, nil + } + } + + return models.ServiceOffering{}, errors.New("Could not find service") +} + +func (builder Builder) GetServiceByNameForSpaceWithPlans(serviceLabel, spaceGuid string) (models.ServiceOffering, error) { + offering, err := builder.GetServiceByNameForSpace(serviceLabel, spaceGuid) + if err != nil { + return models.ServiceOffering{}, err + } + + offering.Plans, err = builder.planBuilder.GetPlansForService(offering.Guid) + if err != nil { + return models.ServiceOffering{}, err + } + + return offering, nil +} + +func (builder Builder) GetServicesByNameForSpaceWithPlans(serviceLabel, spaceGuid string) (models.ServiceOfferings, error) { + offerings, err := builder.serviceRepo.FindServiceOfferingsForSpaceByLabel(serviceLabel, spaceGuid) + if err != nil { + return models.ServiceOfferings{}, err + } + + for index, offering := range offerings { + offerings[index].Plans, err = builder.planBuilder.GetPlansForService(offering.Guid) + if err != nil { + return models.ServiceOfferings{}, err + } + } + + return offerings, nil +} + +func (builder Builder) GetServiceByNameWithPlansWithOrgNames(serviceLabel string) (models.ServiceOffering, error) { + services, err := builder.serviceRepo.FindServiceOfferingsByLabel(serviceLabel) + if err != nil { + return models.ServiceOffering{}, err + } + + service, err := builder.attachPlansToService(services[0]) + if err != nil { + return models.ServiceOffering{}, err + } + return service, nil +} + +func (builder Builder) GetServicesForBroker(brokerGuid string) ([]models.ServiceOffering, error) { + services, err := builder.serviceRepo.ListServicesFromBroker(brokerGuid) + if err != nil { + return nil, err + } + for index, service := range services { + services[index], err = builder.attachPlansToService(service) + if err != nil { + return nil, err + } + } + return services, nil +} + +func (builder Builder) GetServiceVisibleToOrg(serviceName string, orgName string) (models.ServiceOffering, error) { + visiblePlans, err := builder.planBuilder.GetPlansVisibleToOrg(orgName) + if err != nil { + return models.ServiceOffering{}, err + } + + if len(visiblePlans) == 0 { + return models.ServiceOffering{}, nil + } + + return builder.attachSpecificServiceToPlans(serviceName, visiblePlans) +} + +func (builder Builder) GetServicesVisibleToOrg(orgName string) ([]models.ServiceOffering, error) { + visiblePlans, err := builder.planBuilder.GetPlansVisibleToOrg(orgName) + if err != nil { + return nil, err + } + + if len(visiblePlans) == 0 { + return nil, nil + } + + return builder.attachServicesToPlans(visiblePlans) +} + +func (builder Builder) attachPlansToServiceForOrg(service models.ServiceOffering, orgName string) (models.ServiceOffering, error) { + plans, err := builder.planBuilder.GetPlansForServiceForOrg(service.Guid, orgName) + if err != nil { + return models.ServiceOffering{}, err + } + + service.Plans = plans + return service, nil +} + +func (builder Builder) attachPlansToService(service models.ServiceOffering) (models.ServiceOffering, error) { + plans, err := builder.planBuilder.GetPlansForServiceWithOrgs(service.Guid) + if err != nil { + return models.ServiceOffering{}, err + } + + service.Plans = plans + return service, nil +} + +func (builder Builder) attachServicesToPlans(plans []models.ServicePlanFields) ([]models.ServiceOffering, error) { + var services []models.ServiceOffering + servicesMap := make(map[string]models.ServiceOffering) + + for _, plan := range plans { + if plan.ServiceOfferingGuid == "" { + continue + } + + if service, ok := servicesMap[plan.ServiceOfferingGuid]; ok { + service.Plans = append(service.Plans, plan) + servicesMap[service.Guid] = service + } else { + service, err := builder.serviceRepo.GetServiceOfferingByGuid(plan.ServiceOfferingGuid) + if err != nil { + return nil, err + } + service.Plans = append(service.Plans, plan) + servicesMap[service.Guid] = service + } + } + + for _, service := range servicesMap { + services = append(services, service) + } + + return services, nil +} + +func (builder Builder) attachSpecificServiceToPlans(serviceName string, plans []models.ServicePlanFields) (models.ServiceOffering, error) { + services, err := builder.serviceRepo.FindServiceOfferingsByLabel(serviceName) + if err != nil { + return models.ServiceOffering{}, err + } + + service := services[0] + for _, plan := range plans { + if plan.ServiceOfferingGuid == service.Guid { + service.Plans = append(service.Plans, plan) + } + } + + return service, nil +} + +func returnV2Service(services models.ServiceOfferings) models.ServiceOffering { + for _, service := range services { + if service.Provider == "" { + return service + } + } + + return models.ServiceOffering{} +} diff --git a/cf/actors/service_builder/service_builder_suite_test.go b/cf/actors/service_builder/service_builder_suite_test.go new file mode 100644 index 00000000000..3fa79637f8b --- /dev/null +++ b/cf/actors/service_builder/service_builder_suite_test.go @@ -0,0 +1,13 @@ +package service_builder_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestServiceBuilder(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ServiceBuilder Suite") +} diff --git a/cf/actors/service_builder/service_builder_test.go b/cf/actors/service_builder/service_builder_test.go new file mode 100644 index 00000000000..534cd0f5420 --- /dev/null +++ b/cf/actors/service_builder/service_builder_test.go @@ -0,0 +1,419 @@ +package service_builder_test + +import ( + plan_builder_fakes "github.com/cloudfoundry/cli/cf/actors/plan_builder/fakes" + "github.com/cloudfoundry/cli/cf/actors/service_builder" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + + "github.com/cloudfoundry/cli/cf/models" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service Builder", func() { + var ( + planBuilder *plan_builder_fakes.FakePlanBuilder + serviceBuilder service_builder.ServiceBuilder + serviceRepo *testapi.FakeServiceRepo + service1 models.ServiceOffering + v1Service models.ServiceOffering + planWithoutOrgs models.ServicePlanFields + plan1 models.ServicePlanFields + plan2 models.ServicePlanFields + ) + + BeforeEach(func() { + serviceRepo = &testapi.FakeServiceRepo{} + planBuilder = &plan_builder_fakes.FakePlanBuilder{} + + serviceBuilder = service_builder.NewBuilder(serviceRepo, planBuilder) + service1 = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-service1", + Guid: "service-guid1", + BrokerGuid: "my-service-broker-guid1", + }, + } + + v1Service = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "v1Service", + Guid: "v1Service-guid", + BrokerGuid: "my-service-broker-guid1", + Provider: "IAmV1", + }, + } + + serviceRepo.FindServiceOfferingsByLabelName = "my-service1" + serviceRepo.FindServiceOfferingsByLabelServiceOfferings = models.ServiceOfferings{service1, v1Service} + + serviceRepo.GetServiceOfferingByGuidReturns = struct { + ServiceOffering models.ServiceOffering + Error error + }{ + service1, + nil, + } + + serviceRepo.ListServicesFromBrokerReturns = map[string][]models.ServiceOffering{ + "my-service-broker-guid1": []models.ServiceOffering{service1}, + } + + plan1 = models.ServicePlanFields{ + Name: "service-plan1", + Guid: "service-plan1-guid", + ServiceOfferingGuid: "service-guid1", + OrgNames: []string{"org1", "org2"}, + } + + plan2 = models.ServicePlanFields{ + Name: "service-plan2", + Guid: "service-plan2-guid", + ServiceOfferingGuid: "service-guid1", + } + + planWithoutOrgs = models.ServicePlanFields{ + Name: "service-plan-without-orgs", + Guid: "service-plan-without-orgs-guid", + ServiceOfferingGuid: "service-guid1", + } + + planBuilder.GetPlansVisibleToOrgReturns([]models.ServicePlanFields{plan1, plan2}, nil) + planBuilder.GetPlansForServiceWithOrgsReturns([]models.ServicePlanFields{plan1, plan2}, nil) + planBuilder.GetPlansForServiceForOrgReturns([]models.ServicePlanFields{plan1, plan2}, nil) + }) + + Describe(".GetServicesForSpace", func() { + BeforeEach(func() { + serviceRepo.GetServiceOfferingsForSpaceReturns = struct { + ServiceOfferings []models.ServiceOffering + Error error + }{ + []models.ServiceOffering{service1, service1}, + nil, + } + }) + + It("returns the services for the space", func() { + services, err := serviceBuilder.GetServicesForSpace("spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(services)).To(Equal(2)) + }) + }) + + Describe(".GetServicesForSpaceWithPlans", func() { + BeforeEach(func() { + serviceRepo.GetServiceOfferingsForSpaceReturns = struct { + ServiceOfferings []models.ServiceOffering + Error error + }{ + []models.ServiceOffering{service1, service1}, + nil, + } + + planBuilder.GetPlansForServiceReturns([]models.ServicePlanFields{planWithoutOrgs}, nil) + }) + + It("returns the services for the space, populated with plans", func() { + services, err := serviceBuilder.GetServicesForSpaceWithPlans("spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(services)).To(Equal(2)) + Expect(services[0].Plans[0]).To(Equal(planWithoutOrgs)) + Expect(services[1].Plans[0]).To(Equal(planWithoutOrgs)) + }) + }) + + Describe(".GetAllServices", func() { + BeforeEach(func() { + serviceRepo.GetAllServiceOfferingsReturns = struct { + ServiceOfferings []models.ServiceOffering + Error error + }{ + []models.ServiceOffering{service1, service1}, + nil, + } + }) + + It("returns the named service, populated with plans", func() { + services, err := serviceBuilder.GetAllServices() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(services)).To(Equal(2)) + }) + }) + + Describe(".GetAllServicesWithPlans", func() { + BeforeEach(func() { + serviceRepo.GetAllServiceOfferingsReturns = struct { + ServiceOfferings []models.ServiceOffering + Error error + }{ + []models.ServiceOffering{service1, service1}, + nil, + } + + planBuilder.GetPlansForServiceReturns([]models.ServicePlanFields{plan1}, nil) + }) + + It("returns the named service, populated with plans", func() { + services, err := serviceBuilder.GetAllServicesWithPlans() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(services)).To(Equal(2)) + Expect(services[0].Plans[0]).To(Equal(plan1)) + }) + }) + + Describe(".GetServiceByNameWithPlans", func() { + BeforeEach(func() { + planBuilder.GetPlansForServiceReturns([]models.ServicePlanFields{plan2}, nil) + }) + + It("returns the named service, populated with plans", func() { + service, err := serviceBuilder.GetServiceByNameWithPlans("my-service1") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(service.Plans)).To(Equal(1)) + Expect(service.Plans[0].Name).To(Equal("service-plan2")) + Expect(service.Plans[0].OrgNames).To(BeNil()) + }) + }) + + Describe(".GetServiceByNameWithPlansWithOrgNames", func() { + It("returns the named service, populated with plans", func() { + service, err := serviceBuilder.GetServiceByNameWithPlansWithOrgNames("my-service1") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(service.Plans)).To(Equal(2)) + Expect(service.Plans[0].Name).To(Equal("service-plan1")) + Expect(service.Plans[1].Name).To(Equal("service-plan2")) + Expect(service.Plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + }) + + Describe(".GetServiceByNameForSpace", func() { + Context("mixed v2 and v1 services", func() { + BeforeEach(func() { + service2 := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "service", + Guid: "service-guid-v2", + }, + } + + service1 := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "service", + Guid: "service-guid", + Provider: "a provider", + }, + } + + serviceRepo.FindServiceOfferingsForSpaceByLabelReturns = struct { + ServiceOfferings models.ServiceOfferings + Error error + }{ + models.ServiceOfferings{service1, service2}, + nil, + } + }) + + It("returns the nv2 service", func() { + service, err := serviceBuilder.GetServiceByNameForSpace("service", "spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(service.Plans)).To(Equal(0)) + Expect(service.Guid).To(Equal("service-guid-v2")) + }) + }) + + Context("v2 services", func() { + BeforeEach(func() { + service := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "service", + Guid: "service-guid", + }, + } + + serviceRepo.FindServiceOfferingsForSpaceByLabelReturns = struct { + ServiceOfferings models.ServiceOfferings + Error error + }{ + models.ServiceOfferings{service}, + nil, + } + }) + + It("returns the named service", func() { + service, err := serviceBuilder.GetServiceByNameForSpace("service", "spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(service.Plans)).To(Equal(0)) + Expect(service.Guid).To(Equal("service-guid")) + }) + }) + + Context("v1 services", func() { + BeforeEach(func() { + service := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "service", + Guid: "service-guid", + Provider: "a provider", + }, + } + + serviceRepo.FindServiceOfferingsForSpaceByLabelReturns = struct { + ServiceOfferings models.ServiceOfferings + Error error + }{ + models.ServiceOfferings{service}, + nil, + } + }) + + It("returns the an error", func() { + service, err := serviceBuilder.GetServiceByNameForSpace("service", "spaceGuid") + Expect(service).To(Equal(models.ServiceOffering{})) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe(".GetServiceByNameForSpaceWithPlans", func() { + BeforeEach(func() { + service := models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "serviceWithPlans", + }, + } + + serviceRepo.FindServiceOfferingsForSpaceByLabelReturns = struct { + ServiceOfferings models.ServiceOfferings + Error error + }{ + models.ServiceOfferings{service}, + nil, + } + + planBuilder.GetPlansForServiceReturns([]models.ServicePlanFields{planWithoutOrgs}, nil) + }) + + It("returns the named service", func() { + service, err := serviceBuilder.GetServiceByNameForSpaceWithPlans("serviceWithPlans", "spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(service.Plans)).To(Equal(1)) + Expect(service.Plans[0].Name).To(Equal("service-plan-without-orgs")) + Expect(service.Plans[0].OrgNames).To(BeNil()) + }) + }) + + Describe(".GetServicesByNameForSpaceWithPlans", func() { + BeforeEach(func() { + serviceRepo.FindServiceOfferingsForSpaceByLabelReturns = struct { + ServiceOfferings models.ServiceOfferings + Error error + }{ + models.ServiceOfferings{service1, v1Service}, + nil, + } + + planBuilder.GetPlansForServiceReturns([]models.ServicePlanFields{planWithoutOrgs}, nil) + }) + + It("returns the named service", func() { + services, err := serviceBuilder.GetServicesByNameForSpaceWithPlans("serviceWithPlans", "spaceGuid") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(services)).To(Equal(2)) + Expect(services[0].Label).To(Equal("my-service1")) + Expect(services[0].Plans[0].Name).To(Equal("service-plan-without-orgs")) + Expect(services[0].Plans[0].OrgNames).To(BeNil()) + Expect(services[1].Label).To(Equal("v1Service")) + Expect(services[1].Plans[0].Name).To(Equal("service-plan-without-orgs")) + Expect(services[1].Plans[0].OrgNames).To(BeNil()) + }) + }) + + Describe(".GetServiceByNameForOrg", func() { + It("returns the named service, populated with plans", func() { + service, err := serviceBuilder.GetServiceByNameForOrg("my-service1", "org1") + Expect(err).NotTo(HaveOccurred()) + + Expect(planBuilder.GetPlansForServiceForOrgCallCount()).To(Equal(1)) + servName, orgName := planBuilder.GetPlansForServiceForOrgArgsForCall(0) + Expect(servName).To(Equal("service-guid1")) + Expect(orgName).To(Equal("org1")) + + Expect(len(service.Plans)).To(Equal(2)) + Expect(service.Plans[0].Name).To(Equal("service-plan1")) + Expect(service.Plans[1].Name).To(Equal("service-plan2")) + Expect(service.Plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + }) + + Describe(".GetServicesForBroker", func() { + It("returns all the services for a broker, fully populated", func() { + services, err := serviceBuilder.GetServicesForBroker("my-service-broker-guid1") + Expect(err).NotTo(HaveOccurred()) + + service := services[0] + Expect(service.Label).To(Equal("my-service1")) + Expect(len(service.Plans)).To(Equal(2)) + Expect(service.Plans[0].Name).To(Equal("service-plan1")) + Expect(service.Plans[1].Name).To(Equal("service-plan2")) + Expect(service.Plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + }) + + Describe(".GetServiceVisibleToOrg", func() { + It("Returns a service populated with plans visible to the provided org", func() { + service, err := serviceBuilder.GetServiceVisibleToOrg("my-service1", "org1") + Expect(err).NotTo(HaveOccurred()) + + Expect(service.Label).To(Equal("my-service1")) + Expect(len(service.Plans)).To(Equal(2)) + Expect(service.Plans[0].Name).To(Equal("service-plan1")) + Expect(service.Plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + + Context("When no plans are visible to the provided org", func() { + It("Returns nil", func() { + planBuilder.GetPlansVisibleToOrgReturns(nil, nil) + service, err := serviceBuilder.GetServiceVisibleToOrg("my-service1", "org3") + Expect(err).NotTo(HaveOccurred()) + + Expect(service).To(Equal(models.ServiceOffering{})) + }) + }) + }) + + Describe(".GetServicesVisibleToOrg", func() { + It("Returns services with plans visible to the provided org", func() { + planBuilder.GetPlansVisibleToOrgReturns([]models.ServicePlanFields{plan1, plan2}, nil) + services, err := serviceBuilder.GetServicesVisibleToOrg("org1") + Expect(err).NotTo(HaveOccurred()) + + service := services[0] + Expect(service.Label).To(Equal("my-service1")) + Expect(len(service.Plans)).To(Equal(2)) + Expect(service.Plans[0].Name).To(Equal("service-plan1")) + Expect(service.Plans[0].OrgNames).To(Equal([]string{"org1", "org2"})) + }) + + Context("When no plans are visible to the provided org", func() { + It("Returns nil", func() { + planBuilder.GetPlansVisibleToOrgReturns(nil, nil) + services, err := serviceBuilder.GetServicesVisibleToOrg("org3") + Expect(err).NotTo(HaveOccurred()) + + Expect(services).To(BeNil()) + }) + }) + }) +}) diff --git a/cf/actors/services.go b/cf/actors/services.go new file mode 100644 index 00000000000..9332202adc3 --- /dev/null +++ b/cf/actors/services.go @@ -0,0 +1,113 @@ +package actors + +import ( + "github.com/cloudfoundry/cli/cf/actors/broker_builder" + "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/models" +) + +type ServiceActor interface { + FilterBrokers(brokerFlag string, serviceFlag string, orgFlag string) ([]models.ServiceBroker, error) +} + +type ServiceHandler struct { + orgRepo organizations.OrganizationRepository + brokerBuilder broker_builder.BrokerBuilder + serviceBuilder service_builder.ServiceBuilder +} + +func NewServiceHandler(org organizations.OrganizationRepository, brokerBuilder broker_builder.BrokerBuilder, serviceBuilder service_builder.ServiceBuilder) ServiceHandler { + return ServiceHandler{ + orgRepo: org, + brokerBuilder: brokerBuilder, + serviceBuilder: serviceBuilder, + } +} + +func (actor ServiceHandler) FilterBrokers(brokerFlag string, serviceFlag string, orgFlag string) ([]models.ServiceBroker, error) { + if orgFlag == "" { + return actor.getServiceBrokers(brokerFlag, serviceFlag) + } else { + err := actor.checkForOrgExistence(orgFlag) + if err != nil { + return nil, err + } + return actor.buildBrokersVisibleFromOrg(brokerFlag, serviceFlag, orgFlag) + } +} + +func (actor ServiceHandler) checkForOrgExistence(orgName string) error { + _, err := actor.orgRepo.FindByName(orgName) + return err +} + +func (actor ServiceHandler) getServiceBrokers(brokerName string, serviceName string) ([]models.ServiceBroker, error) { + if serviceName != "" { + broker, err := actor.brokerBuilder.GetBrokerWithSpecifiedService(serviceName) + if err != nil { + return nil, err + } + + if brokerName != "" { + if broker.Name != brokerName { + return nil, nil + } + } + return []models.ServiceBroker{broker}, nil + } + + if brokerName != "" && serviceName == "" { + broker, err := actor.brokerBuilder.GetBrokerWithAllServices(brokerName) + if err != nil { + return nil, err + } + return []models.ServiceBroker{broker}, nil + } + + return actor.brokerBuilder.GetAllServiceBrokers() +} + +func (actor ServiceHandler) buildBrokersVisibleFromOrg(brokerFlag string, serviceFlag string, orgFlag string) ([]models.ServiceBroker, error) { + if serviceFlag != "" && brokerFlag != "" { + service, err := actor.serviceBuilder.GetServiceVisibleToOrg(serviceFlag, orgFlag) + if err != nil { + return nil, err + } + broker, err := actor.brokerBuilder.AttachSpecificBrokerToServices(brokerFlag, []models.ServiceOffering{service}) + if err != nil { + return nil, err + } + return []models.ServiceBroker{broker}, nil + } + + if serviceFlag != "" && brokerFlag == "" { + service, err := actor.serviceBuilder.GetServiceVisibleToOrg(serviceFlag, orgFlag) + if err != nil { + return nil, err + } + return actor.brokerBuilder.AttachBrokersToServices([]models.ServiceOffering{service}) + } + + if serviceFlag == "" && brokerFlag != "" { + services, err := actor.serviceBuilder.GetServicesVisibleToOrg(orgFlag) + if err != nil { + return nil, err + } + broker, err := actor.brokerBuilder.AttachSpecificBrokerToServices(brokerFlag, services) + if err != nil { + return nil, err + } + return []models.ServiceBroker{broker}, nil + } + + if serviceFlag == "" && brokerFlag == "" { + services, err := actor.serviceBuilder.GetServicesVisibleToOrg(orgFlag) + if err != nil { + return nil, err + } + return actor.brokerBuilder.AttachBrokersToServices(services) + } + + return nil, nil +} diff --git a/cf/actors/services_plans.go b/cf/actors/services_plans.go new file mode 100644 index 00000000000..a23c4e3e172 --- /dev/null +++ b/cf/actors/services_plans.go @@ -0,0 +1,272 @@ +package actors + +import ( + "errors" + "fmt" + + "github.com/cloudfoundry/cli/cf/api/organizations" + + "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" +) + +type ServicePlanActor interface { + FindServiceAccess(string, string) (ServiceAccess, error) + UpdateAllPlansForService(string, bool) (bool, error) + UpdateOrgForService(string, string, bool) (bool, error) + UpdateSinglePlanForService(string, string, bool) (PlanAccess, error) + UpdatePlanAndOrgForService(string, string, string, bool) (PlanAccess, error) +} + +type PlanAccess int + +const ( + PlanAccessError PlanAccess = iota + All + Limited + None +) + +type ServiceAccess int + +const ( + ServiceAccessError ServiceAccess = iota + AllPlansArePublic + AllPlansArePrivate + AllPlansAreLimited + SomePlansArePublicSomeAreLimited + SomePlansArePublicSomeArePrivate + SomePlansAreLimitedSomeArePrivate + SomePlansArePublicSomeAreLimitedSomeArePrivate +) + +type ServicePlanHandler struct { + servicePlanRepo api.ServicePlanRepository + servicePlanVisibilityRepo api.ServicePlanVisibilityRepository + orgRepo organizations.OrganizationRepository + serviceBuilder service_builder.ServiceBuilder + planBuilder plan_builder.PlanBuilder +} + +func NewServicePlanHandler(plan api.ServicePlanRepository, vis api.ServicePlanVisibilityRepository, org organizations.OrganizationRepository, planBuilder plan_builder.PlanBuilder, serviceBuilder service_builder.ServiceBuilder) ServicePlanHandler { + return ServicePlanHandler{ + servicePlanRepo: plan, + servicePlanVisibilityRepo: vis, + orgRepo: org, + serviceBuilder: serviceBuilder, + planBuilder: planBuilder, + } +} + +func (actor ServicePlanHandler) UpdateAllPlansForService(serviceName string, setPlanVisibility bool) (bool, error) { + service, err := actor.serviceBuilder.GetServiceByNameWithPlansWithOrgNames(serviceName) + if err != nil { + return false, err + } + + allPlansWereSet := true + for _, plan := range service.Plans { + planAccess, err := actor.updateSinglePlan(service, plan.Name, setPlanVisibility) + if err != nil { + return false, err + } + // If any plan is Limited we know that we have to change the visibility. + planAlreadySet := ((planAccess == All) == setPlanVisibility) && planAccess != Limited + allPlansWereSet = allPlansWereSet && planAlreadySet + } + return allPlansWereSet, nil +} + +func (actor ServicePlanHandler) UpdateOrgForService(serviceName string, orgName string, setPlanVisibility bool) (bool, error) { + var err error + var service models.ServiceOffering + + service, err = actor.serviceBuilder.GetServiceByNameForOrg(serviceName, orgName) + if err != nil { + return false, err + } + + org, err := actor.orgRepo.FindByName(orgName) + if err != nil { + return false, err + } + + allPlansWereSet := true + for _, plan := range service.Plans { + visibilityExists := plan.OrgHasVisibility(org.Name) + if plan.Public || visibilityExists == setPlanVisibility { + continue + } else if visibilityExists && !setPlanVisibility { + actor.deleteServicePlanVisibilities(map[string]string{"organization_guid": org.Guid, "service_plan_guid": plan.Guid}) + } else if !visibilityExists && setPlanVisibility { + err = actor.servicePlanVisibilityRepo.Create(plan.Guid, org.Guid) + if err != nil { + return false, err + } + } + // We only get here once we have already updated a plan. + allPlansWereSet = false + } + return allPlansWereSet, nil +} + +func (actor ServicePlanHandler) UpdatePlanAndOrgForService(serviceName, planName, orgName string, setPlanVisibility bool) (PlanAccess, error) { + service, err := actor.serviceBuilder.GetServiceByNameForOrg(serviceName, orgName) + if err != nil { + return PlanAccessError, err + } + + org, err := actor.orgRepo.FindByName(orgName) + if err != nil { + return PlanAccessError, err + } + + found := false + var servicePlan models.ServicePlanFields + for i, val := range service.Plans { + if val.Name == planName { + found = true + servicePlan = service.Plans[i] + } + } + if !found { + return PlanAccessError, errors.New(fmt.Sprintf("Service plan %s not found", planName)) + } + + if !servicePlan.Public && setPlanVisibility { + if servicePlan.OrgHasVisibility(orgName) { + return Limited, nil + } + + // Enable service access + err = actor.servicePlanVisibilityRepo.Create(servicePlan.Guid, org.Guid) + if err != nil { + return PlanAccessError, err + } + } else if !servicePlan.Public && !setPlanVisibility { + // Disable service access + if servicePlan.OrgHasVisibility(org.Name) { + err = actor.deleteServicePlanVisibilities(map[string]string{"organization_guid": org.Guid, "service_plan_guid": servicePlan.Guid}) + if err != nil { + return PlanAccessError, err + } + } + } + + access := actor.findPlanAccess(servicePlan) + return access, nil +} + +func (actor ServicePlanHandler) UpdateSinglePlanForService(serviceName string, planName string, setPlanVisibility bool) (PlanAccess, error) { + serviceOffering, err := actor.serviceBuilder.GetServiceByNameWithPlansWithOrgNames(serviceName) + if err != nil { + return PlanAccessError, err + } + return actor.updateSinglePlan(serviceOffering, planName, setPlanVisibility) +} + +func (actor ServicePlanHandler) updateSinglePlan(serviceOffering models.ServiceOffering, planName string, setPlanVisibility bool) (PlanAccess, error) { + var planToUpdate *models.ServicePlanFields + + //find the service plan and set it as the only service plan for update + for _, servicePlan := range serviceOffering.Plans { + if servicePlan.Name == planName { + planToUpdate = &servicePlan //he has the orgs inside him!!! + break + } + } + + if planToUpdate == nil { + return PlanAccessError, errors.New(fmt.Sprintf("The plan %s could not be found for service %s", planName, serviceOffering.Label)) + } + + err := actor.updateServicePlanAvailability(serviceOffering.Guid, *planToUpdate, setPlanVisibility) + if err != nil { + return PlanAccessError, err + } + + access := actor.findPlanAccess(*planToUpdate) + return access, nil +} + +func (actor ServicePlanHandler) deleteServicePlanVisibilities(queryParams map[string]string) error { + visibilities, err := actor.servicePlanVisibilityRepo.Search(queryParams) + if err != nil { + return err + } + for _, visibility := range visibilities { + err = actor.servicePlanVisibilityRepo.Delete(visibility.Guid) + if err != nil { + return err + } + } + + return nil +} + +func (actor ServicePlanHandler) updateServicePlanAvailability(serviceGuid string, servicePlan models.ServicePlanFields, setPlanVisibility bool) error { + // We delete all service plan visibilities for the given Plan since the attribute public should function as a giant on/off + // switch for all orgs. Thus we need to clean up any visibilities laying around so that they don't carry over. + err := actor.deleteServicePlanVisibilities(map[string]string{"service_plan_guid": servicePlan.Guid}) + if err != nil { + return err + } + + if servicePlan.Public == setPlanVisibility { + return nil + } + + return actor.servicePlanRepo.Update(servicePlan, serviceGuid, setPlanVisibility) +} + +func (actor ServicePlanHandler) FindServiceAccess(serviceName string, orgName string) (ServiceAccess, error) { + service, err := actor.serviceBuilder.GetServiceByNameForOrg(serviceName, orgName) + if err != nil { + return ServiceAccessError, err + } + + publicBucket, limitedBucket, privateBucket := 0, 0, 0 + + for _, plan := range service.Plans { + if plan.Public { + publicBucket++ + } else if len(plan.OrgNames) > 0 { + limitedBucket++ + } else { + privateBucket++ + } + } + + if publicBucket > 0 && limitedBucket == 0 && privateBucket == 0 { + return AllPlansArePublic, nil + } + if publicBucket > 0 && limitedBucket > 0 && privateBucket == 0 { + return SomePlansArePublicSomeAreLimited, nil + } + if publicBucket > 0 && privateBucket > 0 && limitedBucket == 0 { + return SomePlansArePublicSomeArePrivate, nil + } + + if limitedBucket > 0 && publicBucket == 0 && privateBucket == 0 { + return AllPlansAreLimited, nil + } + if privateBucket > 0 && publicBucket == 0 && privateBucket == 0 { + return AllPlansArePrivate, nil + } + if limitedBucket > 0 && privateBucket > 0 && publicBucket == 0 { + return SomePlansAreLimitedSomeArePrivate, nil + } + return SomePlansArePublicSomeAreLimitedSomeArePrivate, nil +} + +func (actor ServicePlanHandler) findPlanAccess(plan models.ServicePlanFields) PlanAccess { + if plan.Public { + return All + } else if len(plan.OrgNames) > 0 { + return Limited + } else { + return None + } +} diff --git a/cf/actors/services_plans_test.go b/cf/actors/services_plans_test.go new file mode 100644 index 00000000000..12b0b94d0ff --- /dev/null +++ b/cf/actors/services_plans_test.go @@ -0,0 +1,621 @@ +package actors_test + +import ( + "github.com/cloudfoundry/cli/cf/errors" + + "github.com/cloudfoundry/cli/cf/actors" + fake_plan_builder "github.com/cloudfoundry/cli/cf/actors/plan_builder/fakes" + fake_service_builder "github.com/cloudfoundry/cli/cf/actors/service_builder/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + fake_orgs "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/models" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service Plans", func() { + var ( + actor actors.ServicePlanActor + + servicePlanRepo *testapi.FakeServicePlanRepo + servicePlanVisibilityRepo *testapi.FakeServicePlanVisibilityRepository + orgRepo *fake_orgs.FakeOrganizationRepository + + planBuilder *fake_plan_builder.FakePlanBuilder + serviceBuilder *fake_service_builder.FakeServiceBuilder + + privateServicePlanVisibilityFields models.ServicePlanVisibilityFields + publicServicePlanVisibilityFields models.ServicePlanVisibilityFields + limitedServicePlanVisibilityFields models.ServicePlanVisibilityFields + + publicServicePlan models.ServicePlanFields + privateServicePlan models.ServicePlanFields + limitedServicePlan models.ServicePlanFields + + publicService models.ServiceOffering + mixedService models.ServiceOffering + privateService models.ServiceOffering + publicAndLimitedService models.ServiceOffering + + org1 models.Organization + org2 models.Organization + + visibility1 models.ServicePlanVisibilityFields + ) + + BeforeEach(func() { + servicePlanRepo = &testapi.FakeServicePlanRepo{} + servicePlanVisibilityRepo = &testapi.FakeServicePlanVisibilityRepository{} + orgRepo = &fake_orgs.FakeOrganizationRepository{} + planBuilder = &fake_plan_builder.FakePlanBuilder{} + serviceBuilder = &fake_service_builder.FakeServiceBuilder{} + + actor = actors.NewServicePlanHandler(servicePlanRepo, servicePlanVisibilityRepo, orgRepo, planBuilder, serviceBuilder) + + org1 = models.Organization{} + org1.Name = "org-1" + org1.Guid = "org-1-guid" + + org2 = models.Organization{} + org2.Name = "org-2" + org2.Guid = "org-2-guid" + + orgRepo.FindByNameReturns(org1, nil) + + publicServicePlanVisibilityFields = models.ServicePlanVisibilityFields{ + Guid: "public-service-plan-visibility-guid", + ServicePlanGuid: "public-service-plan-guid", + } + + privateServicePlanVisibilityFields = models.ServicePlanVisibilityFields{ + Guid: "private-service-plan-visibility-guid", + ServicePlanGuid: "private-service-plan-guid", + } + + limitedServicePlanVisibilityFields = models.ServicePlanVisibilityFields{ + Guid: "limited-service-plan-visibility-guid", + ServicePlanGuid: "limited-service-plan-guid", + OrganizationGuid: "org-1-guid", + } + + publicServicePlan = models.ServicePlanFields{ + Name: "public-service-plan", + Guid: "public-service-plan-guid", + Public: true, + } + + privateServicePlan = models.ServicePlanFields{ + Name: "private-service-plan", + Guid: "private-service-plan-guid", + Public: false, + OrgNames: []string{}, + } + + limitedServicePlan = models.ServicePlanFields{ + Name: "limited-service-plan", + Guid: "limited-service-plan-guid", + Public: false, + OrgNames: []string{ + "org-1", + }, + } + + publicService = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-public-service", + Guid: "my-public-service-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + publicServicePlan, + }, + } + + mixedService = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-mixed-service", + Guid: "my-mixed-service-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + privateServicePlan, + limitedServicePlan, + }, + } + + privateService = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-private-service", + Guid: "my-private-service-guid", + }, + Plans: []models.ServicePlanFields{ + privateServicePlan, + privateServicePlan, + }, + } + publicAndLimitedService = models.ServiceOffering{ + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-public-and-limited-service", + Guid: "my-public-and-limited-service-guid", + }, + Plans: []models.ServicePlanFields{ + publicServicePlan, + publicServicePlan, + limitedServicePlan, + }, + } + + visibility1 = models.ServicePlanVisibilityFields{ + Guid: "visibility-guid-1", + OrganizationGuid: "org-1-guid", + ServicePlanGuid: "limited-service-plan-guid", + } + }) + + Describe(".UpdateAllPlansForService", func() { + BeforeEach(func() { + servicePlanVisibilityRepo.SearchReturns( + []models.ServicePlanVisibilityFields{privateServicePlanVisibilityFields}, nil) + + servicePlanRepo.SearchReturns = map[string][]models.ServicePlanFields{ + "my-mixed-service-guid": { + publicServicePlan, + privateServicePlan, + }, + } + }) + + It("Returns an error if the service cannot be found", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(models.ServiceOffering{}, errors.New("service was not found")) + _, err := actor.UpdateAllPlansForService("not-a-service", true) + Expect(err.Error()).To(Equal("service was not found")) + }) + + It("Removes the service plan visibilities for any non-public service plans", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateAllPlansForService("my-mixed-service", true) + Expect(err).ToNot(HaveOccurred()) + + servicePlanVisibilityGuid := servicePlanVisibilityRepo.DeleteArgsForCall(0) + Expect(servicePlanVisibilityGuid).To(Equal("private-service-plan-visibility-guid")) + }) + + Context("when setting all plans to public", func() { + It("Sets all non-public service plans to public", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateAllPlansForService("my-mixed-service", true) + Expect(err).ToNot(HaveOccurred()) + + servicePlan, serviceGuid, public := servicePlanRepo.UpdateArgsForCall(0) + Expect(servicePlan.Public).To(BeFalse()) + Expect(serviceGuid).To(Equal("my-mixed-service-guid")) + Expect(public).To(BeTrue()) + }) + + It("Returns true if all the plans were public", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(publicService, nil) + + servicesOriginallyPublic, err := actor.UpdateAllPlansForService("my-public-service", true) + Expect(err).NotTo(HaveOccurred()) + Expect(servicesOriginallyPublic).To(BeTrue()) + }) + + It("Returns false if any of the plans were not public", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + + servicesOriginallyPublic, err := actor.UpdateAllPlansForService("my-mixed-service", true) + Expect(err).NotTo(HaveOccurred()) + Expect(servicesOriginallyPublic).To(BeFalse()) + }) + + It("Does not try to update service plans if they are all already public", func() { + servicePlanRepo.SearchReturns = map[string][]models.ServicePlanFields{ + "my-public-service-guid": { + publicServicePlan, + publicServicePlan, + }, + } + + _, err := actor.UpdateAllPlansForService("my-public-service", true) + Expect(err).ToNot(HaveOccurred()) + + Expect(servicePlanRepo.UpdateCallCount()).To(Equal(0)) + }) + }) + + Context("when setting all plans to private", func() { + It("Sets all public service plans to private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + + _, err := actor.UpdateAllPlansForService("my-mixed-service", false) + Expect(err).ToNot(HaveOccurred()) + + servicePlan, serviceGuid, public := servicePlanRepo.UpdateArgsForCall(0) + Expect(servicePlan.Public).To(BeTrue()) + Expect(serviceGuid).To(Equal("my-mixed-service-guid")) + Expect(public).To(BeFalse()) + }) + + It("Returns true if all plans were already private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(privateService, nil) + + allPlansAlreadyPrivate, err := actor.UpdateAllPlansForService("my-private-service", false) + Expect(err).NotTo(HaveOccurred()) + Expect(allPlansAlreadyPrivate).To(BeTrue()) + }) + + It("Returns false if any of the plans were not private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + + allPlansAlreadyPrivate, err := actor.UpdateAllPlansForService("my-mixed-service", false) + Expect(err).NotTo(HaveOccurred()) + Expect(allPlansAlreadyPrivate).To(BeFalse()) + }) + + It("Does not try to update service plans if they are all already private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(privateService, nil) + + _, err := actor.UpdateAllPlansForService("my-private-service", false) + Expect(err).ToNot(HaveOccurred()) + + Expect(servicePlanRepo.UpdateCallCount()).To(Equal(0)) + }) + }) + }) + + Describe(".UpdateOrgForService", func() { + BeforeEach(func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + + orgRepo.FindByNameReturns(org1, nil) + }) + + It("Returns an error if the service cannot be found", func() { + serviceBuilder.GetServiceByNameForOrgReturns(models.ServiceOffering{}, errors.New("service was not found")) + + _, err := actor.UpdateOrgForService("not-a-service", "org-1", true) + Expect(err.Error()).To(Equal("service was not found")) + }) + + Context("when giving access to all plans for a single org", func() { + It("creates a service plan visibility for all private plans", func() { + _, err := actor.UpdateOrgForService("my-mixed-service", "org-1", true) + Expect(err).ToNot(HaveOccurred()) + + Expect(servicePlanVisibilityRepo.CreateCallCount()).To(Equal(1)) + + planGuid, orgGuid := servicePlanVisibilityRepo.CreateArgsForCall(0) + Expect(planGuid).To(Equal("private-service-plan-guid")) + Expect(orgGuid).To(Equal("org-1-guid")) + }) + + It("Returns true if all the plans were already public", func() { + serviceBuilder.GetServiceByNameForOrgReturns(publicService, nil) + allPlansSet, err := actor.UpdateOrgForService("my-public-service", "org-1", true) + Expect(err).NotTo(HaveOccurred()) + Expect(allPlansSet).To(BeTrue()) + }) + + It("Returns false if any of the plans were not public", func() { + serviceBuilder.GetServiceByNameForOrgReturns(privateService, nil) + allPlansSet, err := actor.UpdateOrgForService("my-private-service", "org-1", true) + Expect(err).NotTo(HaveOccurred()) + Expect(allPlansSet).To(BeFalse()) + }) + + It("Does not try to update service plans if they are all already public or the org already has access", func() { + serviceBuilder.GetServiceByNameForOrgReturns(publicAndLimitedService, nil) + + allPlansWereSet, err := actor.UpdateOrgForService("my-public-and-limited-service", "org-1", true) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanVisibilityRepo.CreateCallCount()).To(Equal(0)) + Expect(allPlansWereSet).To(BeTrue()) + }) + }) + + Context("when disabling access to all plans for a single org", func() { + It("deletes the associated visibilities for all limited plans", func() { + serviceBuilder.GetServiceByNameForOrgReturns(publicAndLimitedService, nil) + servicePlanVisibilityRepo.SearchReturns([]models.ServicePlanVisibilityFields{visibility1}, nil) + allPlansSet, err := actor.UpdateOrgForService("my-public-and-limited-service", "org-1", false) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(1)) + Expect(allPlansSet).To(BeFalse()) + + services := servicePlanVisibilityRepo.SearchArgsForCall(0) + Expect(services["organization_guid"]).To(Equal("org-1-guid")) + + visibilityGuid := servicePlanVisibilityRepo.DeleteArgsForCall(0) + Expect(visibilityGuid).To(Equal("visibility-guid-1")) + }) + + It("Does not try to update service plans if they are all public", func() { + serviceBuilder.GetServiceByNameForOrgReturns(publicService, nil) + + allPlansWereSet, err := actor.UpdateOrgForService("my-public-and-limited-service", "org-1", false) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(0)) + Expect(allPlansWereSet).To(BeTrue()) + }) + + It("Does not try to update service plans if the org already did not have visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(privateService, nil) + + allPlansWereSet, err := actor.UpdateOrgForService("my-private-service", "org-1", false) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(0)) + Expect(allPlansWereSet).To(BeTrue()) + }) + }) + }) + + Describe(".UpdateSinglePlanForService", func() { + It("Returns an error if the service cannot be found", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(models.ServiceOffering{}, errors.New("service was not found")) + _, err := actor.UpdateSinglePlanForService("not-a-service", "public-service-plan", true) + Expect(err.Error()).To(Equal("service was not found")) + }) + + It("Returns None if the original plan was private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(privateService, nil) + originalAccessValue, err := actor.UpdateSinglePlanForService("my-mixed-service", "private-service-plan", true) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.None)) + }) + + It("Returns All if the original plan was public", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + originalAccessValue, err := actor.UpdateSinglePlanForService("my-mixed-service", "public-service-plan", true) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.All)) + }) + + It("Returns an error if the plan cannot be found", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "not-a-service-plan", true) + Expect(err.Error()).To(Equal("The plan not-a-service-plan could not be found for service my-mixed-service")) + }) + + Context("when setting a public service plan to public", func() { + It("Does not try to update the service plan", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "public-service-plan", true) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanRepo.UpdateCallCount()).To(Equal(0)) + }) + }) + + Context("when setting private service plan to public", func() { + BeforeEach(func() { + servicePlanVisibilityRepo.SearchReturns( + []models.ServicePlanVisibilityFields{privateServicePlanVisibilityFields}, nil) + }) + + It("removes the service plan visibilities for the service plan", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "private-service-plan", true) + Expect(err).ToNot(HaveOccurred()) + + servicePlanVisibilityGuid := servicePlanVisibilityRepo.DeleteArgsForCall(0) + Expect(servicePlanVisibilityGuid).To(Equal("private-service-plan-visibility-guid")) + }) + + It("sets a service plan to public", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "private-service-plan", true) + Expect(err).ToNot(HaveOccurred()) + + servicePlan, serviceGuid, public := servicePlanRepo.UpdateArgsForCall(0) + Expect(servicePlan.Public).To(BeFalse()) + Expect(serviceGuid).To(Equal("my-mixed-service-guid")) + Expect(public).To(BeTrue()) + }) + }) + + Context("when setting a private service plan to private", func() { + It("Does not try to update the service plan", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "private-service-plan", false) + Expect(err).ToNot(HaveOccurred()) + Expect(servicePlanRepo.UpdateCallCount()).To(Equal(0)) + }) + }) + + Context("When setting public service plan to private", func() { + BeforeEach(func() { + servicePlanVisibilityRepo.SearchReturns( + []models.ServicePlanVisibilityFields{publicServicePlanVisibilityFields}, nil) + }) + + It("removes the service plan visibilities for the service plan", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "public-service-plan", false) + Expect(err).ToNot(HaveOccurred()) + + servicePlanVisibilityGuid := servicePlanVisibilityRepo.DeleteArgsForCall(0) + Expect(servicePlanVisibilityGuid).To(Equal("public-service-plan-visibility-guid")) + }) + + It("sets the plan to private", func() { + serviceBuilder.GetServiceByNameWithPlansWithOrgNamesReturns(mixedService, nil) + _, err := actor.UpdateSinglePlanForService("my-mixed-service", "public-service-plan", false) + Expect(err).ToNot(HaveOccurred()) + + servicePlan, serviceGuid, public := servicePlanRepo.UpdateArgsForCall(0) + Expect(servicePlan.Public).To(BeTrue()) + Expect(serviceGuid).To(Equal("my-mixed-service-guid")) + Expect(public).To(BeFalse()) + }) + }) + }) + + Describe(".UpdatePlanAndOrgForService", func() { + BeforeEach(func() { + orgRepo.FindByNameReturns(org1, nil) + }) + + It("returns an error if the service cannot be found", func() { + serviceBuilder.GetServiceByNameForOrgReturns(models.ServiceOffering{}, errors.New("service was not found")) + + _, err := actor.UpdatePlanAndOrgForService("not-a-service", "public-service-plan", "public-org", true) + Expect(err.Error()).To(Equal("service was not found")) + }) + + It("returns an error if the org cannot be found", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.NewModelNotFoundError("organization", "not-an-org")) + _, err := actor.UpdatePlanAndOrgForService("a-real-service", "public-service-plan", "not-an-org", true) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error if the plan cannot be found", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + + _, err := actor.UpdatePlanAndOrgForService("a-real-service", "not-a-plan", "org-1", true) + Expect(err).To(HaveOccurred()) + }) + + Context("when disabling access to a single plan for a single org", func() { + Context("for a public plan", func() { + It("returns All", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "public-service-plan", "org-1", false) + + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.All)) + }) + + It("does not try and delete the visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "public-service-plan", "org-1", false) + + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.All)) + }) + }) + + Context("for a private plan", func() { + Context("with no service plan visibilities", func() { + It("returns None", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "private-service-plan", "org-1", false) + + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.None)) + }) + It("does not try and delete the visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "private-service-plan", "org-1", false) + + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.None)) + }) + }) + + Context("with service plan visibilities", func() { + BeforeEach(func() { + servicePlanVisibilityRepo.SearchReturns( + []models.ServicePlanVisibilityFields{limitedServicePlanVisibilityFields}, nil) + + }) + It("deletes a service plan visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "limited-service-plan", "org-1", false) + + servicePlanVisGuid := servicePlanVisibilityRepo.DeleteArgsForCall(0) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.Limited)) + Expect(servicePlanVisGuid).To(Equal("limited-service-plan-visibility-guid")) + }) + + It("does not call delete if the specified service plan visibility does not exist", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + orgRepo.FindByNameReturns(org2, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "limited-service-plan", "org-2", false) + + Expect(servicePlanVisibilityRepo.DeleteCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.Limited)) + }) + }) + }) + }) + + Context("when enabling access", func() { + Context("for a public plan", func() { + It("returns All", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "public-service-plan", "org-1", true) + + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.All)) + }) + + It("does not try and create the visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "public-service-plan", "org-1", true) + + Expect(servicePlanVisibilityRepo.CreateCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.All)) + }) + }) + + Context("for a limited plan", func() { + BeforeEach(func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + }) + It("returns Limited", func() { + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "limited-service-plan", "org-1", true) + + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.Limited)) + }) + + Context("when the org already has access", func() { + It("does not try and create the visibility", func() { + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "limited-service-plan", "org-1", true) + + Expect(servicePlanVisibilityRepo.CreateCallCount()).To(Equal(0)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.Limited)) + }) + }) + Context("when the org does not have access", func() { + It("creates the visibility", func() { + orgRepo.FindByNameReturns(org2, nil) + + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "limited-service-plan", "org-2", true) + + Expect(servicePlanVisibilityRepo.CreateCallCount()).To(Equal(1)) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.Limited)) + }) + }) + }) + + Context("for a private plan", func() { + It("returns None", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "private-service-plan", "org-1", true) + + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.None)) + }) + + It("creates a service plan visibility", func() { + serviceBuilder.GetServiceByNameForOrgReturns(mixedService, nil) + originalAccessValue, err := actor.UpdatePlanAndOrgForService("my-mixed-service", "private-service-plan", "org-1", true) + + servicePlanGuid, orgGuid := servicePlanVisibilityRepo.CreateArgsForCall(0) + Expect(err).NotTo(HaveOccurred()) + Expect(originalAccessValue).To(Equal(actors.None)) + Expect(servicePlanGuid).To(Equal("private-service-plan-guid")) + Expect(orgGuid).To(Equal("org-1-guid")) + }) + }) + }) + }) +}) diff --git a/cf/actors/services_test.go b/cf/actors/services_test.go new file mode 100644 index 00000000000..e4ba4b907a7 --- /dev/null +++ b/cf/actors/services_test.go @@ -0,0 +1,203 @@ +package actors_test + +import ( + "github.com/cloudfoundry/cli/cf/actors" + broker_builder "github.com/cloudfoundry/cli/cf/actors/broker_builder/fakes" + service_builder "github.com/cloudfoundry/cli/cf/actors/service_builder/fakes" + organization_fakes "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Services", func() { + var ( + actor actors.ServiceActor + brokerBuilder *broker_builder.FakeBrokerBuilder + serviceBuilder *service_builder.FakeServiceBuilder + orgRepo *organization_fakes.FakeOrganizationRepository + serviceBroker1 models.ServiceBroker + serviceBroker2 models.ServiceBroker + service1 models.ServiceOffering + ) + + BeforeEach(func() { + orgRepo = &organization_fakes.FakeOrganizationRepository{} + brokerBuilder = &broker_builder.FakeBrokerBuilder{} + serviceBuilder = &service_builder.FakeServiceBuilder{} + + actor = actors.NewServiceHandler(orgRepo, brokerBuilder, serviceBuilder) + + serviceBroker1 = models.ServiceBroker{Guid: "my-service-broker-guid1", Name: "my-service-broker1"} + serviceBroker2 = models.ServiceBroker{Guid: "my-service-broker-guid2", Name: "my-service-broker2"} + + service1 = models.ServiceOffering{ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "my-service1", + Guid: "service-guid1", + BrokerGuid: "my-service-broker-guid1"}, + } + + org1 := models.Organization{} + org1.Name = "org1" + org1.Guid = "org-guid" + + org2 := models.Organization{} + org2.Name = "org2" + org2.Guid = "org2-guid" + }) + + Describe("FilterBrokers", func() { + Context("when no flags are passed", func() { + It("returns all brokers", func() { + returnedBrokers := []models.ServiceBroker{serviceBroker1} + brokerBuilder.GetAllServiceBrokersReturns(returnedBrokers, nil) + + brokers, err := actor.FilterBrokers("", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + }) + }) + + Context("when the -b flag is passed", func() { + It("returns a single broker contained in a slice with all dependencies populated", func() { + returnedBroker := serviceBroker1 + brokerBuilder.GetBrokerWithAllServicesReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("my-service-broker1", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + }) + }) + + Context("when the -e flag is passed", func() { + It("returns a single broker containing a single service", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBroker := serviceBroker1 + brokerBuilder.GetBrokerWithSpecifiedServiceReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("", "my-service1", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + }) + + Context("when the -o flag is passed", func() { + It("returns an error if the org does not actually exist", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.NewModelNotFoundError("organization", "org-that-shall-not-be-found")) + _, err := actor.FilterBrokers("", "", "org-that-shall-not-be-found") + + Expect(err).To(HaveOccurred()) + }) + + It("returns a slice of brokers containing Services/Service Plans visible to the org", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBroker := []models.ServiceBroker{serviceBroker1} + + serviceBuilder.GetServicesVisibleToOrgReturns([]models.ServiceOffering{service1}, nil) + brokerBuilder.AttachBrokersToServicesReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("", "", "org1") + Expect(err).NotTo(HaveOccurred()) + + orgName := serviceBuilder.GetServicesVisibleToOrgArgsForCall(0) + Expect(orgName).To(Equal("org1")) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + }) + + Context("when the -b AND the -e flags are passed", func() { + It("returns the intersection set", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBroker := serviceBroker1 + brokerBuilder.GetBrokerWithSpecifiedServiceReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("my-service-broker1", "my-service1", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + + Expect(brokers[0].Services[0].Label).To(Equal("my-service1")) + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + + Context("when the -b AND -e intersection is the empty set", func() { + It("returns an empty set", func() { + brokerBuilder.GetBrokerWithSpecifiedServiceReturns(models.ServiceBroker{}, nil) + brokers, err := actor.FilterBrokers("my-service-broker", "my-service2", "") + + Expect(len(brokers)).To(Equal(0)) + Expect(err).To(BeNil()) + }) + }) + }) + + Context("when the -b AND the -o flags are passed", func() { + It("returns the intersection set", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBroker := serviceBroker1 + + serviceBuilder.GetServiceVisibleToOrgReturns(service1, nil) + brokerBuilder.AttachSpecificBrokerToServicesReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("my-service-broker", "", "org1") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + + Expect(brokers[0].Services[0].Label).To(Equal("my-service1")) + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + }) + + Context("when the -e AND the -o flags are passed", func() { + It("returns the intersection set", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBrokers := []models.ServiceBroker{serviceBroker1} + + serviceBuilder.GetServicesVisibleToOrgReturns([]models.ServiceOffering{service1}, nil) + brokerBuilder.AttachBrokersToServicesReturns(returnedBrokers, nil) + + brokers, err := actor.FilterBrokers("", "my-service1", "org1") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + + Expect(brokers[0].Services[0].Label).To(Equal("my-service1")) + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + }) + + Context("when the -b AND -e AND the -o flags are passed", func() { + It("returns the intersection set", func() { + serviceBroker1.Services = []models.ServiceOffering{service1} + returnedBroker := serviceBroker1 + + serviceBuilder.GetServicesVisibleToOrgReturns([]models.ServiceOffering{service1}, nil) + brokerBuilder.AttachSpecificBrokerToServicesReturns(returnedBroker, nil) + + brokers, err := actor.FilterBrokers("my-service-broker1", "my-service1", "org1") + Expect(err).NotTo(HaveOccurred()) + + Expect(len(brokers)).To(Equal(1)) + Expect(len(brokers[0].Services)).To(Equal(1)) + + Expect(brokers[0].Services[0].Label).To(Equal("my-service1")) + Expect(brokers[0].Services[0].Guid).To(Equal("service-guid1")) + }) + }) + }) +}) diff --git a/cf/api/api_suite_test.go b/cf/api/api_suite_test.go new file mode 100644 index 00000000000..f37973978e8 --- /dev/null +++ b/cf/api/api_suite_test.go @@ -0,0 +1,19 @@ +package api_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestApi(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Api Suite") +} diff --git a/cf/api/app_events/app_events.go b/cf/api/app_events/app_events.go new file mode 100644 index 00000000000..20fa00b8130 --- /dev/null +++ b/cf/api/app_events/app_events.go @@ -0,0 +1,50 @@ +package app_events + +import ( + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/api/strategy" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type AppEventsRepository interface { + RecentEvents(appGuid string, limit int64) ([]models.EventFields, error) +} + +type CloudControllerAppEventsRepository struct { + config core_config.Reader + gateway net.Gateway + strategy strategy.EndpointStrategy +} + +func NewCloudControllerAppEventsRepository(config core_config.Reader, gateway net.Gateway, strategy strategy.EndpointStrategy) CloudControllerAppEventsRepository { + return CloudControllerAppEventsRepository{ + config: config, + gateway: gateway, + strategy: strategy, + } +} + +func (repo CloudControllerAppEventsRepository) RecentEvents(appGuid string, limit int64) ([]models.EventFields, error) { + count := int64(0) + events := make([]models.EventFields, 0, limit) + apiErr := repo.listEvents(appGuid, limit, func(eventField models.EventFields) bool { + count++ + events = append(events, eventField) + return count < limit + }) + + return events, apiErr +} + +func (repo CloudControllerAppEventsRepository) listEvents(appGuid string, limit int64, cb func(models.EventFields) bool) error { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + repo.strategy.EventsURL(appGuid, limit), + repo.strategy.EventsResource(), + + func(resource interface{}) bool { + return cb(resource.(resources.EventResource).ToFields()) + }) +} diff --git a/cf/api/app_events/app_events_suite_test.go b/cf/api/app_events/app_events_suite_test.go new file mode 100644 index 00000000000..4f78a36cebc --- /dev/null +++ b/cf/api/app_events/app_events_suite_test.go @@ -0,0 +1,19 @@ +package app_events_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAppEvents(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "AppEvents Suite") +} diff --git a/cf/api/app_events/app_events_test.go b/cf/api/app_events/app_events_test.go new file mode 100644 index 00000000000..5f4e2b16c50 --- /dev/null +++ b/cf/api/app_events/app_events_test.go @@ -0,0 +1,129 @@ +package app_events_test + +import ( + "net/http" + "net/http/httptest" + "time" + + . "github.com/cloudfoundry/cli/cf/api/app_events" + "github.com/cloudfoundry/cli/cf/api/strategy" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + testtime "github.com/cloudfoundry/cli/testhelpers/time" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("App Events Repo", func() { + var ( + server *httptest.Server + handler *testnet.TestHandler + config core_config.ReadWriter + repo AppEventsRepository + ) + + BeforeEach(func() { + config = testconfig.NewRepository() + config.SetAccessToken("BEARER my_access_token") + config.SetApiVersion("2.2.0") + }) + + JustBeforeEach(func() { + strategy := strategy.NewEndpointStrategy(config.ApiVersion()) + gateway := net.NewCloudControllerGateway(config, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerAppEventsRepository(config, gateway, strategy) + }) + + AfterEach(func() { + server.Close() + }) + + setupTestServer := func(requests ...testnet.TestRequest) { + server, handler = testnet.NewServer(requests) + config.SetApiEndpoint(server.URL) + } + + Describe("list recent events", func() { + It("returns the most recent events", func() { + setupTestServer(eventsRequest) + + list, err := repo.RecentEvents("my-app-guid", 2) + Expect(err).ToNot(HaveOccurred()) + + Expect(list).To(ConsistOf([]models.EventFields{ + models.EventFields{ + Guid: "event-1-guid", + Name: "audit.app.update", + Timestamp: testtime.MustParse(eventTimestampFormat, "2014-01-21T00:20:11+00:00"), + Description: "instances: 1, memory: 256, command: PRIVATE DATA HIDDEN, environment_json: PRIVATE DATA HIDDEN", + ActorName: "somebody@pivotallabs.com", + }, + models.EventFields{ + Guid: "event-2-guid", + Name: "audit.app.update", + Timestamp: testtime.MustParse(eventTimestampFormat, "2014-01-21T00:20:11+00:00"), + Description: "instances: 1, memory: 256, command: PRIVATE DATA HIDDEN, environment_json: PRIVATE DATA HIDDEN", + ActorName: "nobody@pivotallabs.com", + }, + })) + }) + }) +}) + +const eventTimestampFormat = "2006-01-02T15:04:05-07:00" + +var eventsRequest = testnet.TestRequest{ + Method: "GET", + Path: "/v2/events?q=actee%3Amy-app-guid&order-direction=desc&results-per-page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 1, + "total_pages": 1, + "prev_url": null, + "next_url": "/v2/events?q=actee%3Amy-app-guid&page=2", + "resources": [ + { + "metadata": { + "guid": "event-1-guid" + }, + "entity": { + "type": "audit.app.update", + "timestamp": "2014-01-21T00:20:11+00:00", + "actor_name": "somebody@pivotallabs.com", + "metadata": { + "request": { + "command": "PRIVATE DATA HIDDEN", + "instances": 1, + "memory": 256, + "name": "dora", + "environment_json": "PRIVATE DATA HIDDEN" + } + } + } + }, + { + "metadata": { + "guid": "event-2-guid" + }, + "entity": { + "type": "audit.app.update", + "actor_name": "nobody@pivotallabs.com", + "timestamp": "2014-01-21T00:20:11+00:00", + "metadata": { + "request": { + "command": "PRIVATE DATA HIDDEN", + "instances": 1, + "memory": 256, + "name": "dora", + "environment_json": "PRIVATE DATA HIDDEN" + } + } + } + } + ] + }`}} diff --git a/cf/api/app_events/fakes/fake_app_events_repository.go b/cf/api/app_events/fakes/fake_app_events_repository.go new file mode 100644 index 00000000000..b8730a5aa87 --- /dev/null +++ b/cf/api/app_events/fakes/fake_app_events_repository.go @@ -0,0 +1,57 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/app_events" + + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeAppEventsRepository struct { + RecentEventsStub func(appGuid string, limit int64) ([]models.EventFields, error) + recentEventsMutex sync.RWMutex + recentEventsArgsForCall []struct { + appGuid string + limit int64 + } + recentEventsReturns struct { + result1 []models.EventFields + result2 error + } +} + +func (fake *FakeAppEventsRepository) RecentEvents(appGuid string, limit int64) ([]models.EventFields, error) { + fake.recentEventsMutex.Lock() + defer fake.recentEventsMutex.Unlock() + fake.recentEventsArgsForCall = append(fake.recentEventsArgsForCall, struct { + appGuid string + limit int64 + }{appGuid, limit}) + if fake.RecentEventsStub != nil { + return fake.RecentEventsStub(appGuid, limit) + } else { + return fake.recentEventsReturns.result1, fake.recentEventsReturns.result2 + } +} + +func (fake *FakeAppEventsRepository) RecentEventsCallCount() int { + fake.recentEventsMutex.RLock() + defer fake.recentEventsMutex.RUnlock() + return len(fake.recentEventsArgsForCall) +} + +func (fake *FakeAppEventsRepository) RecentEventsArgsForCall(i int) (string, int64) { + fake.recentEventsMutex.RLock() + defer fake.recentEventsMutex.RUnlock() + return fake.recentEventsArgsForCall[i].appGuid, fake.recentEventsArgsForCall[i].limit +} + +func (fake *FakeAppEventsRepository) RecentEventsReturns(result1 []models.EventFields, result2 error) { + fake.recentEventsReturns = struct { + result1 []models.EventFields + result2 error + }{result1, result2} +} + +var _ AppEventsRepository = new(FakeAppEventsRepository) diff --git a/cf/api/app_files/app_files.go b/cf/api/app_files/app_files.go new file mode 100644 index 00000000000..459c3655175 --- /dev/null +++ b/cf/api/app_files/app_files.go @@ -0,0 +1,34 @@ +package app_files + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" +) + +type AppFilesRepository interface { + ListFiles(appGuid string, instance int, path string) (files string, apiErr error) +} + +type CloudControllerAppFilesRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerAppFilesRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerAppFilesRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerAppFilesRepository) ListFiles(appGuid string, instance int, path string) (files string, apiErr error) { + url := fmt.Sprintf("%s/v2/apps/%s/instances/%d/files/%s", repo.config.ApiEndpoint(), appGuid, instance, path) + request, apiErr := repo.gateway.NewRequest("GET", url, repo.config.AccessToken(), nil) + if apiErr != nil { + return + } + + files, _, apiErr = repo.gateway.PerformRequestForTextResponse(request) + return +} diff --git a/cf/api/app_files/app_files_suite_test.go b/cf/api/app_files/app_files_suite_test.go new file mode 100644 index 00000000000..bfdc848a3ca --- /dev/null +++ b/cf/api/app_files/app_files_suite_test.go @@ -0,0 +1,19 @@ +package app_files_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAppFiles(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "AppFiles Suite") +} diff --git a/cf/api/app_files/app_files_test.go b/cf/api/app_files/app_files_test.go new file mode 100644 index 00000000000..f8add1e5ee1 --- /dev/null +++ b/cf/api/app_files/app_files_test.go @@ -0,0 +1,69 @@ +package app_files_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/app_files" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppFilesRepository", func() { + It("lists files", func() { + expectedResponse := "file 1\n file 2\n file 3" + + listFilesEndpoint := func(writer http.ResponseWriter, request *http.Request) { + methodMatches := request.Method == "GET" + pathMatches := request.URL.Path == "/some/path" + + if !methodMatches || !pathMatches { + fmt.Printf("One of the matchers did not match. Method [%t] Path [%t]", + methodMatches, pathMatches) + + writer.WriteHeader(http.StatusInternalServerError) + return + } + + writer.WriteHeader(http.StatusOK) + fmt.Fprint(writer, expectedResponse) + } + + listFilesServer := httptest.NewServer(http.HandlerFunc(listFilesEndpoint)) + defer listFilesServer.Close() + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/apps/my-app-guid/instances/1/files/some/path", + Response: testnet.TestResponse{ + Status: http.StatusTemporaryRedirect, + Header: http.Header{ + "Location": {fmt.Sprintf("%s/some/path", listFilesServer.URL)}, + }, + }, + }) + + listFilesRedirectServer, handler := testnet.NewServer([]testnet.TestRequest{req}) + defer listFilesRedirectServer.Close() + + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(listFilesRedirectServer.URL) + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo := NewCloudControllerAppFilesRepository(configRepo, gateway) + list, err := repo.ListFiles("my-app-guid", 1, "some/path") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).ToNot(HaveOccurred()) + Expect(list).To(Equal(expectedResponse)) + }) +}) diff --git a/cf/api/app_files/fakes/fake_app_files_repository.go b/cf/api/app_files/fakes/fake_app_files_repository.go new file mode 100644 index 00000000000..809f31f7eea --- /dev/null +++ b/cf/api/app_files/fakes/fake_app_files_repository.go @@ -0,0 +1,58 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/app_files" + + "sync" +) + +type FakeAppFilesRepository struct { + ListFilesStub func(appGuid string, instance int, path string) (files string, apiErr error) + listFilesMutex sync.RWMutex + listFilesArgsForCall []struct { + appGuid string + instance int + path string + } + listFilesReturns struct { + result1 string + result2 error + } +} + +func (fake *FakeAppFilesRepository) ListFiles(appGuid string, instance int, path string) (files string, apiErr error) { + fake.listFilesMutex.Lock() + defer fake.listFilesMutex.Unlock() + fake.listFilesArgsForCall = append(fake.listFilesArgsForCall, struct { + appGuid string + instance int + path string + }{appGuid, instance, path}) + if fake.ListFilesStub != nil { + return fake.ListFilesStub(appGuid, instance, path) + } else { + return fake.listFilesReturns.result1, fake.listFilesReturns.result2 + } +} + +func (fake *FakeAppFilesRepository) ListFilesCallCount() int { + fake.listFilesMutex.RLock() + defer fake.listFilesMutex.RUnlock() + return len(fake.listFilesArgsForCall) +} + +func (fake *FakeAppFilesRepository) ListFilesArgsForCall(i int) (string, int, string) { + fake.listFilesMutex.RLock() + defer fake.listFilesMutex.RUnlock() + return fake.listFilesArgsForCall[i].appGuid, fake.listFilesArgsForCall[i].instance, fake.listFilesArgsForCall[i].path +} + +func (fake *FakeAppFilesRepository) ListFilesReturns(result1 string, result2 error) { + fake.listFilesReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +var _ AppFilesRepository = new(FakeAppFilesRepository) diff --git a/cf/api/app_instances/app_instances.go b/cf/api/app_instances/app_instances.go new file mode 100644 index 00000000000..f34b0df53f8 --- /dev/null +++ b/cf/api/app_instances/app_instances.go @@ -0,0 +1,100 @@ +package app_instances + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type InstancesApiResponse map[string]InstanceApiResponse + +type InstanceApiResponse struct { + State string + Since float64 +} + +type StatsApiResponse map[string]InstanceStatsApiResponse + +type InstanceStatsApiResponse struct { + Stats struct { + DiskQuota int64 `json:"disk_quota"` + MemQuota int64 `json:"mem_quota"` + Usage struct { + Cpu float64 + Disk int64 + Mem int64 + } + } +} + +type AppInstancesRepository interface { + GetInstances(appGuid string) (instances []models.AppInstanceFields, apiErr error) +} + +type CloudControllerAppInstancesRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerAppInstancesRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerAppInstancesRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerAppInstancesRepository) GetInstances(appGuid string) (instances []models.AppInstanceFields, err error) { + instancesResponse := InstancesApiResponse{} + err = repo.gateway.GetResource( + fmt.Sprintf("%s/v2/apps/%s/instances", repo.config.ApiEndpoint(), appGuid), + &instancesResponse) + if err != nil { + return + } + + instances = make([]models.AppInstanceFields, len(instancesResponse), len(instancesResponse)) + for k, v := range instancesResponse { + index, err := strconv.Atoi(k) + if err != nil { + continue + } + + instances[index] = models.AppInstanceFields{ + State: models.InstanceState(strings.ToLower(v.State)), + Since: time.Unix(int64(v.Since), 0), + } + } + + return repo.updateInstancesWithStats(appGuid, instances) +} + +func (repo CloudControllerAppInstancesRepository) updateInstancesWithStats(guid string, instances []models.AppInstanceFields) (updatedInst []models.AppInstanceFields, apiErr error) { + path := fmt.Sprintf("%s/v2/apps/%s/stats", repo.config.ApiEndpoint(), guid) + statsResponse := StatsApiResponse{} + apiErr = repo.gateway.GetResource(path, &statsResponse) + if apiErr != nil { + return + } + + updatedInst = make([]models.AppInstanceFields, len(statsResponse), len(statsResponse)) + for k, v := range statsResponse { + index, err := strconv.Atoi(k) + if err != nil { + continue + } + + instance := instances[index] + instance.CpuUsage = v.Stats.Usage.Cpu + instance.DiskQuota = v.Stats.DiskQuota + instance.DiskUsage = v.Stats.Usage.Disk + instance.MemQuota = v.Stats.MemQuota + instance.MemUsage = v.Stats.Usage.Mem + + updatedInst[index] = instance + } + return +} diff --git a/cf/api/app_instances/app_instances_suite_test.go b/cf/api/app_instances/app_instances_suite_test.go new file mode 100644 index 00000000000..d72786312f6 --- /dev/null +++ b/cf/api/app_instances/app_instances_suite_test.go @@ -0,0 +1,19 @@ +package app_instances_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAppInstances(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "AppInstances Suite") +} diff --git a/cf/api/app_instances/app_instances_test.go b/cf/api/app_instances/app_instances_test.go new file mode 100644 index 00000000000..6cbf7c55c92 --- /dev/null +++ b/cf/api/app_instances/app_instances_test.go @@ -0,0 +1,102 @@ +package app_instances_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/app_instances" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppInstancesRepo", func() { + It("returns instances of the app, given a guid", func() { + ts, handler, repo := createAppInstancesRepo([]testnet.TestRequest{ + appInstancesRequest, + appStatsRequest, + }) + defer ts.Close() + appGuid := "my-cool-app-guid" + + instances, err := repo.GetInstances(appGuid) + Expect(err).NotTo(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + + Expect(len(instances)).To(Equal(2)) + + Expect(instances[0].State).To(Equal(models.InstanceRunning)) + Expect(instances[1].State).To(Equal(models.InstanceStarting)) + + instance0 := instances[0] + Expect(instance0.Since).To(Equal(time.Unix(1379522342, 0))) + Expect(instance0.DiskQuota).To(Equal(int64(1073741824))) + Expect(instance0.DiskUsage).To(Equal(int64(56037376))) + Expect(instance0.MemQuota).To(Equal(int64(67108864))) + Expect(instance0.MemUsage).To(Equal(int64(19218432))) + Expect(instance0.CpuUsage).To(Equal(3.659571249238058e-05)) + }) +}) + +var appStatsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/apps/my-cool-app-guid/stats", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "1":{ + "stats": { + "disk_quota": 10000, + "mem_quota": 1024, + "usage": { + "cpu": 0.3, + "disk": 10000, + "mem": 1024 + } + } + }, + "0":{ + "stats": { + "disk_quota": 1073741824, + "mem_quota": 67108864, + "usage": { + "cpu": 3.659571249238058e-05, + "disk": 56037376, + "mem": 19218432 + } + } + } +}`}}) + +var appInstancesRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/apps/my-cool-app-guid/instances", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "1": { + "state": "STARTING", + "since": 1379522342.6783738 + }, + "0": { + "state": "RUNNING", + "since": 1379522342.6783738 + } +}`}}) + +func createAppInstancesRepo(requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo AppInstancesRepository) { + ts, handler = testnet.NewServer(requests) + space := models.SpaceFields{} + space.Guid = "my-space-guid" + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerAppInstancesRepository(configRepo, gateway) + return +} diff --git a/cf/api/app_instances/fakes/fake_app_instances_repository.go b/cf/api/app_instances/fakes/fake_app_instances_repository.go new file mode 100644 index 00000000000..e584b019d05 --- /dev/null +++ b/cf/api/app_instances/fakes/fake_app_instances_repository.go @@ -0,0 +1,56 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/api/app_instances" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeAppInstancesRepository struct { + GetInstancesStub func(appGuid string) (instances []models.AppInstanceFields, apiErr error) + getInstancesMutex sync.RWMutex + getInstancesArgsForCall []struct { + appGuid string + } + getInstancesReturns struct { + result1 []models.AppInstanceFields + result2 error + } +} + +func (fake *FakeAppInstancesRepository) GetInstances(appGuid string) (instances []models.AppInstanceFields, apiErr error) { + fake.getInstancesMutex.Lock() + fake.getInstancesArgsForCall = append(fake.getInstancesArgsForCall, struct { + appGuid string + }{appGuid}) + fake.getInstancesMutex.Unlock() + if fake.GetInstancesStub != nil { + return fake.GetInstancesStub(appGuid) + } else { + return fake.getInstancesReturns.result1, fake.getInstancesReturns.result2 + } +} + +func (fake *FakeAppInstancesRepository) GetInstancesCallCount() int { + fake.getInstancesMutex.RLock() + defer fake.getInstancesMutex.RUnlock() + return len(fake.getInstancesArgsForCall) +} + +func (fake *FakeAppInstancesRepository) GetInstancesArgsForCall(i int) string { + fake.getInstancesMutex.RLock() + defer fake.getInstancesMutex.RUnlock() + return fake.getInstancesArgsForCall[i].appGuid +} + +func (fake *FakeAppInstancesRepository) GetInstancesReturns(result1 []models.AppInstanceFields, result2 error) { + fake.GetInstancesStub = nil + fake.getInstancesReturns = struct { + result1 []models.AppInstanceFields + result2 error + }{result1, result2} +} + +var _ app_instances.AppInstancesRepository = new(FakeAppInstancesRepository) diff --git a/cf/api/app_summary.go b/cf/api/app_summary.go new file mode 100644 index 00000000000..16377f97511 --- /dev/null +++ b/cf/api/app_summary.go @@ -0,0 +1,129 @@ +package api + +import ( + "fmt" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ApplicationSummaries struct { + Apps []ApplicationFromSummary +} + +func (resource ApplicationSummaries) ToModels() (apps []models.ApplicationFields) { + for _, application := range resource.Apps { + apps = append(apps, application.ToFields()) + } + return +} + +type ApplicationFromSummary struct { + Guid string + Name string + Routes []RouteSummary + RunningInstances int `json:"running_instances"` + Memory int64 + Instances int + DiskQuota int64 `json:"disk_quota"` + Urls []string + State string + SpaceGuid string `json:"space_guid"` + PackageUpdatedAt *time.Time `json:"package_updated_at"` +} + +func (resource ApplicationFromSummary) ToFields() (app models.ApplicationFields) { + app = models.ApplicationFields{} + app.Guid = resource.Guid + app.Name = resource.Name + app.State = strings.ToLower(resource.State) + app.InstanceCount = resource.Instances + app.DiskQuota = resource.DiskQuota + app.RunningInstances = resource.RunningInstances + app.Memory = resource.Memory + app.SpaceGuid = resource.SpaceGuid + app.PackageUpdatedAt = resource.PackageUpdatedAt + + return +} + +func (resource ApplicationFromSummary) ToModel() (app models.Application) { + app.ApplicationFields = resource.ToFields() + routes := []models.RouteSummary{} + for _, route := range resource.Routes { + routes = append(routes, route.ToModel()) + } + app.Routes = routes + + return +} + +type RouteSummary struct { + Guid string + Host string + Domain DomainSummary +} + +func (resource RouteSummary) ToModel() (route models.RouteSummary) { + domain := models.DomainFields{} + domain.Guid = resource.Domain.Guid + domain.Name = resource.Domain.Name + domain.Shared = resource.Domain.OwningOrganizationGuid != "" + + route.Guid = resource.Guid + route.Host = resource.Host + route.Domain = domain + return +} + +type DomainSummary struct { + Guid string + Name string + OwningOrganizationGuid string +} + +type AppSummaryRepository interface { + GetSummariesInCurrentSpace() (apps []models.Application, apiErr error) + GetSummary(appGuid string) (summary models.Application, apiErr error) +} + +type CloudControllerAppSummaryRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerAppSummaryRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerAppSummaryRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerAppSummaryRepository) GetSummariesInCurrentSpace() (apps []models.Application, apiErr error) { + resources := new(ApplicationSummaries) + + path := fmt.Sprintf("%s/v2/spaces/%s/summary", repo.config.ApiEndpoint(), repo.config.SpaceFields().Guid) + apiErr = repo.gateway.GetResource(path, resources) + if apiErr != nil { + return + } + + for _, resource := range resources.Apps { + apps = append(apps, resource.ToModel()) + } + return +} + +func (repo CloudControllerAppSummaryRepository) GetSummary(appGuid string) (summary models.Application, apiErr error) { + path := fmt.Sprintf("%s/v2/apps/%s/summary", repo.config.ApiEndpoint(), appGuid) + summaryResponse := new(ApplicationFromSummary) + apiErr = repo.gateway.GetResource(path, summaryResponse) + if apiErr != nil { + return + } + + summary = summaryResponse.ToModel() + return +} diff --git a/cf/api/app_summary_test.go b/cf/api/app_summary_test.go new file mode 100644 index 00000000000..5913eb8bce6 --- /dev/null +++ b/cf/api/app_summary_test.go @@ -0,0 +1,161 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppSummaryRepository", func() { + var ( + testServer *httptest.Server + handler *testnet.TestHandler + repo AppSummaryRepository + ) + + BeforeEach(func() { + getAppSummariesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/summary", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: getAppSummariesResponseBody, + }, + }) + + testServer, handler = testnet.NewServer([]testnet.TestRequest{getAppSummariesRequest}) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(testServer.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerAppSummaryRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + It("returns a slice of app summaries for each instance", func() { + apps, apiErr := repo.GetSummariesInCurrentSpace() + Expect(handler).To(HaveAllRequestsCalled()) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(3).To(Equal(len(apps))) + + app1 := apps[0] + Expect(app1.Name).To(Equal("app1")) + Expect(app1.Guid).To(Equal("app-1-guid")) + Expect(len(app1.Routes)).To(Equal(1)) + Expect(app1.Routes[0].URL()).To(Equal("app1.cfapps.io")) + + Expect(app1.State).To(Equal("started")) + Expect(app1.InstanceCount).To(Equal(1)) + Expect(app1.RunningInstances).To(Equal(1)) + Expect(app1.Memory).To(Equal(int64(128))) + Expect(app1.PackageUpdatedAt.Format("2006-01-02T15:04:05Z07:00")).To(Equal("2014-10-24T19:54:00Z")) + + app2 := apps[1] + Expect(app2.Name).To(Equal("app2")) + Expect(app2.Guid).To(Equal("app-2-guid")) + Expect(len(app2.Routes)).To(Equal(2)) + Expect(app2.Routes[0].URL()).To(Equal("app2.cfapps.io")) + Expect(app2.Routes[1].URL()).To(Equal("foo.cfapps.io")) + + Expect(app2.State).To(Equal("started")) + Expect(app2.InstanceCount).To(Equal(3)) + Expect(app2.RunningInstances).To(Equal(1)) + Expect(app2.Memory).To(Equal(int64(512))) + Expect(app2.PackageUpdatedAt.Format("2006-01-02T15:04:05Z07:00")).To(Equal("2012-10-24T19:55:00Z")) + + nullUpdateAtApp := apps[2] + Expect(nullUpdateAtApp.PackageUpdatedAt).To(BeNil()) + }) +}) + +const getAppSummariesResponseBody string = ` +{ + "apps":[ + { + "guid":"app-1-guid", + "routes":[ + { + "guid":"route-1-guid", + "host":"app1", + "domain":{ + "guid":"domain-1-guid", + "name":"cfapps.io" + } + } + ], + "running_instances":1, + "name":"app1", + "memory":128, + "instances":1, + "state":"STARTED", + "service_names":[ + "my-service-instance" + ], + "package_updated_at":"2014-10-24T19:54:00+00:00" + },{ + "guid":"app-2-guid", + "routes":[ + { + "guid":"route-2-guid", + "host":"app2", + "domain":{ + "guid":"domain-1-guid", + "name":"cfapps.io" + } + }, + { + "guid":"route-2-guid", + "host":"foo", + "domain":{ + "guid":"domain-1-guid", + "name":"cfapps.io" + } + } + ], + "running_instances":1, + "name":"app2", + "memory":512, + "instances":3, + "state":"STARTED", + "service_names":[ + "my-service-instance" + ], + "package_updated_at":"2012-10-24T19:55:00+00:00" + },{ + "guid":"app-with-null-updated-at-guid", + "routes":[ + { + "guid":"route-3-guid", + "host":"app3", + "domain":{ + "guid":"domain-3-guid", + "name":"cfapps.io" + } + } + ], + "running_instances":1, + "name":"app-with-null-updated-at", + "memory":512, + "instances":3, + "state":"STARTED", + "service_names":[ + "my-service-instance" + ], + "package_updated_at":null + } + ] +}` diff --git a/cf/api/application_bits/application_bits.go b/cf/api/application_bits/application_bits.go new file mode 100644 index 00000000000..ecd6f1b33aa --- /dev/null +++ b/cf/api/application_bits/application_bits.go @@ -0,0 +1,153 @@ +package application_bits + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/textproto" + "os" + "time" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/gofileutils/fileutils" +) + +const ( + DefaultAppUploadBitsTimeout = 15 * time.Minute +) + +type ApplicationBitsRepository interface { + GetApplicationFiles(appFilesRequest []resources.AppFileResource) ([]resources.AppFileResource, error) + UploadBits(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) (apiErr error) +} + +type CloudControllerApplicationBitsRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerApplicationBitsRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerApplicationBitsRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerApplicationBitsRepository) UploadBits(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) (apiErr error) { + apiUrl := fmt.Sprintf("/v2/apps/%s/bits", appGuid) + fileutils.TempFile("requests", func(requestFile *os.File, err error) { + if err != nil { + apiErr = errors.NewWithError(T("Error creating tmp file: {{.Err}}", map[string]interface{}{"Err": err}), err) + return + } + + // json.Marshal represents a nil value as "null" instead of an empty slice "[]" + if presentFiles == nil { + presentFiles = []resources.AppFileResource{} + } + + presentFilesJSON, err := json.Marshal(presentFiles) + if err != nil { + apiErr = errors.NewWithError(T("Error marshaling JSON"), err) + return + } + + boundary, err := repo.writeUploadBody(zipFile, requestFile, presentFilesJSON) + if err != nil { + apiErr = errors.NewWithError(T("Error writing to tmp file: {{.Err}}", map[string]interface{}{"Err": err}), err) + return + } + + var request *net.Request + request, apiErr = repo.gateway.NewRequestForFile("PUT", repo.config.ApiEndpoint()+apiUrl, repo.config.AccessToken(), requestFile) + if apiErr != nil { + return + } + + contentType := fmt.Sprintf("multipart/form-data; boundary=%s", boundary) + request.HttpReq.Header.Set("Content-Type", contentType) + + response := &resources.Resource{} + _, apiErr = repo.gateway.PerformPollingRequestForJSONResponse(repo.config.ApiEndpoint(), request, response, DefaultAppUploadBitsTimeout) + if apiErr != nil { + return + } + }) + + return +} + +func (repo CloudControllerApplicationBitsRepository) GetApplicationFiles(appFilesToCheck []resources.AppFileResource) ([]resources.AppFileResource, error) { + allAppFilesJson, err := json.Marshal(appFilesToCheck) + if err != nil { + apiErr := errors.NewWithError(T("Failed to create json for resource_match request"), err) + return nil, apiErr + } + + presentFiles := []resources.AppFileResource{} + apiErr := repo.gateway.UpdateResourceSync( + repo.config.ApiEndpoint(), + "/v2/resource_match", + bytes.NewReader(allAppFilesJson), + &presentFiles) + + if apiErr != nil { + return nil, apiErr + } + + return presentFiles, nil +} + +func (repo CloudControllerApplicationBitsRepository) writeUploadBody(zipFile *os.File, body *os.File, presentResourcesJson []byte) (boundary string, err error) { + writer := multipart.NewWriter(body) + defer writer.Close() + + boundary = writer.Boundary() + + part, err := writer.CreateFormField("resources") + if err != nil { + return + } + + _, err = io.Copy(part, bytes.NewBuffer(presentResourcesJson)) + if err != nil { + return + } + + if zipFile != nil { + zipStats, zipErr := zipFile.Stat() + if zipErr != nil { + return + } + + if zipStats.Size() == 0 { + return + } + + part, zipErr = createZipPartWriter(zipStats, writer) + if zipErr != nil { + return + } + + _, zipErr = io.Copy(part, zipFile) + if zipErr != nil { + return + } + } + + return +} + +func createZipPartWriter(zipStats os.FileInfo, writer *multipart.Writer) (io.Writer, error) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="application"; filename="application.zip"`) + h.Set("Content-Type", "application/zip") + h.Set("Content-Length", fmt.Sprintf("%d", zipStats.Size())) + h.Set("Content-Transfer-Encoding", "binary") + return writer.CreatePart(h) +} diff --git a/cf/api/application_bits/application_bits_suite_test.go b/cf/api/application_bits/application_bits_suite_test.go new file mode 100644 index 00000000000..217a21c175c --- /dev/null +++ b/cf/api/application_bits/application_bits_suite_test.go @@ -0,0 +1,19 @@ +package application_bits_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestApplicationBits(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "ApplicationBits Suite") +} diff --git a/cf/api/application_bits/application_bits_test.go b/cf/api/application_bits/application_bits_test.go new file mode 100644 index 00000000000..b02c5197165 --- /dev/null +++ b/cf/api/application_bits/application_bits_test.go @@ -0,0 +1,420 @@ +package application_bits_test + +import ( + "archive/zip" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/application_bits" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerApplicationBitsRepository", func() { + var ( + fixturesDir string + repo ApplicationBitsRepository + file1 resources.AppFileResource + file2 resources.AppFileResource + file3 resources.AppFileResource + file4 resources.AppFileResource + testHandler *testnet.TestHandler + testServer *httptest.Server + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + fixturesDir = filepath.Join(cwd, "../../../fixtures/applications") + + configRepo = testconfig.NewRepositoryWithDefaults() + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + gateway.PollingThrottle = time.Duration(0) + + repo = NewCloudControllerApplicationBitsRepository(configRepo, gateway) + + file1 = resources.AppFileResource{Path: "app.rb", Sha1: "2474735f5163ba7612ef641f438f4b5bee00127b", Size: 51} + file2 = resources.AppFileResource{Path: "config.ru", Sha1: "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", Size: 70} + file3 = resources.AppFileResource{Path: "Gemfile", Sha1: "d9c3a51de5c89c11331d3b90b972789f1a14699a", Size: 59} + file4 = resources.AppFileResource{Path: "Gemfile.lock", Sha1: "345f999aef9070fb9a608e65cf221b7038156b6d", Size: 229} + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".UploadBits", func() { + var uploadFile *os.File + var err error + + BeforeEach(func() { + uploadFile, err = os.Open(filepath.Join(fixturesDir, "ignored_and_resource_matched_example_app.zip")) + if err != nil { + log.Fatal(err) + } + }) + + AfterEach(func() { + testServer.Close() + }) + + It("uploads zip files", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/bits", + Matcher: uploadBodyMatcher(defaultZipCheck), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` + { + "metadata":{ + "guid": "my-job-guid", + "url": "/v2/jobs/my-job-guid" + } + }`, + }, + }), + createProgressEndpoint("running"), + createProgressEndpoint("finished"), + ) + + apiErr := repo.UploadBits("my-cool-app-guid", uploadFile, []resources.AppFileResource{file1, file2}) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("returns a failure when uploading bits fails", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/bits", + Matcher: uploadBodyMatcher(defaultZipCheck), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` + { + "metadata":{ + "guid": "my-job-guid", + "url": "/v2/jobs/my-job-guid" + } + }`, + }, + }), + createProgressEndpoint("running"), + createProgressEndpoint("failed"), + ) + apiErr := repo.UploadBits("my-cool-app-guid", uploadFile, []resources.AppFileResource{file1, file2}) + + Expect(apiErr).To(HaveOccurred()) + }) + + Context("when there are no files to upload", func() { + It("makes a request without a zipfile", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/bits", + Matcher: func(request *http.Request) { + err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes) + Expect(err).NotTo(HaveOccurred()) + defer request.MultipartForm.RemoveAll() + + Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value") + valuePart, ok := request.MultipartForm.Value["resources"] + + Expect(ok).To(BeTrue(), "Resource manifest not present") + Expect(valuePart).To(Equal([]string{"[]"})) + Expect(request.MultipartForm.File).To(BeEmpty()) + }, + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` + { + "metadata":{ + "guid": "my-job-guid", + "url": "/v2/jobs/my-job-guid" + } + }`, + }, + }), + createProgressEndpoint("running"), + createProgressEndpoint("finished"), + ) + + apiErr := repo.UploadBits("my-cool-app-guid", nil, []resources.AppFileResource{}) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + It("marshals a nil presentFiles parameter into an empty array", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/bits", + Matcher: func(request *http.Request) { + err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes) + Expect(err).NotTo(HaveOccurred()) + defer request.MultipartForm.RemoveAll() + + Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value") + valuePart, ok := request.MultipartForm.Value["resources"] + + Expect(ok).To(BeTrue(), "Resource manifest not present") + Expect(valuePart).To(Equal([]string{"[]"})) + Expect(request.MultipartForm.File).To(BeEmpty()) + }, + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` + { + "metadata":{ + "guid": "my-job-guid", + "url": "/v2/jobs/my-job-guid" + } + }`, + }, + }), + createProgressEndpoint("running"), + createProgressEndpoint("finished"), + ) + + apiErr := repo.UploadBits("my-cool-app-guid", nil, nil) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe(".GetApplicationFiles", func() { + It("accepts a slice of files and returns a slice of the files that it already has", func() { + setupTestServer(matchResourceRequest) + matchedFiles, err := repo.GetApplicationFiles([]resources.AppFileResource{file1, file2, file3, file4}) + Expect(matchedFiles).To(Equal([]resources.AppFileResource{file3, file4})) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +var matchedResources = testnet.RemoveWhiteSpaceFromBody(`[ + { + "fn": "Gemfile", + "sha1": "d9c3a51de5c89c11331d3b90b972789f1a14699a", + "size": 59 + }, + { + "fn": "Gemfile.lock", + "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d", + "size": 229 + } +]`) + +var unmatchedResources = testnet.RemoveWhiteSpaceFromBody(`[ + { + "fn": "app.rb", + "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b", + "size": 51 + }, + { + "fn": "config.ru", + "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", + "size": 70 + } +]`) + +func uploadApplicationRequest(zipCheck func(*zip.Reader)) testnet.TestRequest { + return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/bits", + Matcher: uploadBodyMatcher(zipCheck), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` +{ + "metadata":{ + "guid": "my-job-guid", + "url": "/v2/jobs/my-job-guid" + } +} + `}, + }) +} + +var matchResourceRequest = testnet.TestRequest{ + Method: "PUT", + Path: "/v2/resource_match", + Matcher: testnet.RequestBodyMatcher(testnet.RemoveWhiteSpaceFromBody(`[ + { + "fn": "app.rb", + "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b", + "size": 51 + }, + { + "fn": "config.ru", + "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", + "size": 70 + }, + { + "fn": "Gemfile", + "sha1": "d9c3a51de5c89c11331d3b90b972789f1a14699a", + "size": 59 + }, + { + "fn": "Gemfile.lock", + "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d", + "size": 229 + } +]`)), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: matchedResources, + }, +} + +var defaultZipCheck = func(zipReader *zip.Reader) { + Expect(len(zipReader.File)).To(Equal(2), "Wrong number of files in zip") + + var expectedPermissionBits os.FileMode + if runtime.GOOS == "windows" { + expectedPermissionBits = 0111 + } else { + expectedPermissionBits = 0755 + } + + Expect(zipReader.File[0].Name).To(Equal("app.rb")) + Expect(executableBits(zipReader.File[0].Mode())).To(Equal(executableBits(expectedPermissionBits))) + +nextFile: + for _, f := range zipReader.File { + for _, expected := range expectedApplicationContent { + if f.Name == expected { + continue nextFile + } + } + Fail("Expected " + f.Name + " but did not find it") + } +} + +var defaultRequests = []testnet.TestRequest{ + uploadApplicationRequest(defaultZipCheck), + createProgressEndpoint("running"), + createProgressEndpoint("finished"), +} + +var expectedApplicationContent = []string{"app.rb", "config.ru"} + +const maxMultipartResponseSizeInBytes = 4096 + +func uploadBodyMatcher(zipChecks func(zipReader *zip.Reader)) func(*http.Request) { + return func(request *http.Request) { + defer GinkgoRecover() + err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes) + if err != nil { + Fail(fmt.Sprintf("Failed parsing multipart form %v", err)) + return + } + defer request.MultipartForm.RemoveAll() + + Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value") + valuePart, ok := request.MultipartForm.Value["resources"] + Expect(ok).To(BeTrue(), "Resource manifest not present") + Expect(len(valuePart)).To(Equal(1), "Wrong number of values") + + resourceManifest := valuePart[0] + chompedResourceManifest := strings.Replace(resourceManifest, "\n", "", -1) + Expect(chompedResourceManifest).To(Equal(unmatchedResources), "Resources do not match") + + Expect(len(request.MultipartForm.File)).To(Equal(1), "Wrong number of files") + + fileHeaders, ok := request.MultipartForm.File["application"] + Expect(ok).To(BeTrue(), "Application file part not present") + Expect(len(fileHeaders)).To(Equal(1), "Wrong number of files") + + applicationFile := fileHeaders[0] + Expect(applicationFile.Filename).To(Equal("application.zip"), "Wrong file name") + + file, err := applicationFile.Open() + if err != nil { + Fail(fmt.Sprintf("Cannot get multipart file %v", err.Error())) + return + } + + length, err := strconv.ParseInt(applicationFile.Header.Get("content-length"), 10, 64) + if err != nil { + Fail(fmt.Sprintf("Cannot convert content-length to int %v", err.Error())) + return + } + + if zipChecks != nil { + zipReader, err := zip.NewReader(file, length) + if err != nil { + Fail(fmt.Sprintf("Error reading zip content %v", err.Error())) + return + } + + zipChecks(zipReader) + } + } +} + +func createProgressEndpoint(status string) (req testnet.TestRequest) { + body := fmt.Sprintf(` + { + "entity":{ + "status":"%s" + } + }`, status) + + req.Method = "GET" + req.Path = "/v2/jobs/my-job-guid" + req.Response = testnet.TestResponse{ + Status: http.StatusCreated, + Body: body, + } + + return +} + +var matchExcludedResourceRequest = testnet.TestRequest{ + Method: "PUT", + Path: "/v2/resource_match", + Matcher: testnet.RequestBodyMatcher(testnet.RemoveWhiteSpaceFromBody(`[ + { + "fn": ".svn", + "sha1": "0", + "size": 0 + }, + { + "fn": ".svn/test", + "sha1": "456b1d3f7cfbadc66d390de79cbbb6e6a10662da", + "size": 12 + }, + { + "fn": "_darcs", + "sha1": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "size": 4 + } +]`)), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: matchedResources, + }, +} + +func executableBits(mode os.FileMode) os.FileMode { + return mode & 0111 +} diff --git a/cf/api/application_bits/fakes/fake_application_bits_repository.go b/cf/api/application_bits/fakes/fake_application_bits_repository.go new file mode 100644 index 00000000000..57293ec44fd --- /dev/null +++ b/cf/api/application_bits/fakes/fake_application_bits_repository.go @@ -0,0 +1,98 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/application_bits" + "github.com/cloudfoundry/cli/cf/api/resources" + "os" + "sync" +) + +type FakeApplicationBitsRepository struct { + GetApplicationFilesStub func(appFilesRequest []resources.AppFileResource) ([]resources.AppFileResource, error) + getApplicationFilesMutex sync.RWMutex + getApplicationFilesArgsForCall []struct { + arg1 []resources.AppFileResource + } + getApplicationFilesReturns struct { + result1 []resources.AppFileResource + result2 error + } + UploadBitsStub func(appGuid string, zipFile *os.File, presentFiles []resources.AppFileResource) (apiErr error) + uploadBitsMutex sync.RWMutex + uploadBitsArgsForCall []struct { + arg1 string + arg2 *os.File + arg3 []resources.AppFileResource + } + uploadBitsReturns struct { + result1 error + } +} + +func (fake *FakeApplicationBitsRepository) GetApplicationFiles(arg1 []resources.AppFileResource) ([]resources.AppFileResource, error) { + fake.getApplicationFilesMutex.Lock() + defer fake.getApplicationFilesMutex.Unlock() + fake.getApplicationFilesArgsForCall = append(fake.getApplicationFilesArgsForCall, struct { + arg1 []resources.AppFileResource + }{arg1}) + if fake.GetApplicationFilesStub != nil { + return fake.GetApplicationFilesStub(arg1) + } else { + return fake.getApplicationFilesReturns.result1, fake.getApplicationFilesReturns.result2 + } +} + +func (fake *FakeApplicationBitsRepository) GetApplicationFilesCallCount() int { + fake.getApplicationFilesMutex.RLock() + defer fake.getApplicationFilesMutex.RUnlock() + return len(fake.getApplicationFilesArgsForCall) +} + +func (fake *FakeApplicationBitsRepository) GetApplicationFilesArgsForCall(i int) []resources.AppFileResource { + fake.getApplicationFilesMutex.RLock() + defer fake.getApplicationFilesMutex.RUnlock() + return fake.getApplicationFilesArgsForCall[i].arg1 +} + +func (fake *FakeApplicationBitsRepository) GetApplicationFilesReturns(result1 []resources.AppFileResource, result2 error) { + fake.getApplicationFilesReturns = struct { + result1 []resources.AppFileResource + result2 error + }{result1, result2} +} + +func (fake *FakeApplicationBitsRepository) UploadBits(arg1 string, arg2 *os.File, arg3 []resources.AppFileResource) (apiErr error) { + fake.uploadBitsMutex.Lock() + defer fake.uploadBitsMutex.Unlock() + fake.uploadBitsArgsForCall = append(fake.uploadBitsArgsForCall, struct { + arg1 string + arg2 *os.File + arg3 []resources.AppFileResource + }{arg1, arg2, arg3}) + if fake.UploadBitsStub != nil { + return fake.UploadBitsStub(arg1, arg2, arg3) + } else { + return fake.uploadBitsReturns.result1 + } +} + +func (fake *FakeApplicationBitsRepository) UploadBitsCallCount() int { + fake.uploadBitsMutex.RLock() + defer fake.uploadBitsMutex.RUnlock() + return len(fake.uploadBitsArgsForCall) +} + +func (fake *FakeApplicationBitsRepository) UploadBitsArgsForCall(i int) (string, *os.File, []resources.AppFileResource) { + fake.uploadBitsMutex.RLock() + defer fake.uploadBitsMutex.RUnlock() + return fake.uploadBitsArgsForCall[i].arg1, fake.uploadBitsArgsForCall[i].arg2, fake.uploadBitsArgsForCall[i].arg3 +} + +func (fake *FakeApplicationBitsRepository) UploadBitsReturns(result1 error) { + fake.uploadBitsReturns = struct { + result1 error + }{result1} +} + +var _ ApplicationBitsRepository = new(FakeApplicationBitsRepository) diff --git a/cf/api/applications/applications.go b/cf/api/applications/applications.go new file mode 100644 index 00000000000..3adb066427e --- /dev/null +++ b/cf/api/applications/applications.go @@ -0,0 +1,127 @@ +package applications + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ApplicationRepository interface { + Create(params models.AppParams) (createdApp models.Application, apiErr error) + Read(name string) (app models.Application, apiErr error) + ReadFromSpace(name string, spaceGuid string) (app models.Application, apiErr error) + Update(appGuid string, params models.AppParams) (updatedApp models.Application, apiErr error) + Delete(appGuid string) (apiErr error) + ReadEnv(guid string) (*models.Environment, error) + CreateRestageRequest(guid string) (apiErr error) +} + +type CloudControllerApplicationRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerApplicationRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerApplicationRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerApplicationRepository) Create(params models.AppParams) (createdApp models.Application, apiErr error) { + data, err := repo.formatAppJSON(params) + if err != nil { + apiErr = errors.NewWithError(T("Failed to marshal JSON"), err) + return + } + + resource := new(resources.ApplicationResource) + apiErr = repo.gateway.CreateResource(repo.config.ApiEndpoint(), "/v2/apps", strings.NewReader(data), resource) + if apiErr != nil { + return + } + + createdApp = resource.ToModel() + return +} + +func (repo CloudControllerApplicationRepository) Read(name string) (app models.Application, apiErr error) { + return repo.ReadFromSpace(name, repo.config.SpaceFields().Guid) +} + +func (repo CloudControllerApplicationRepository) ReadFromSpace(name string, spaceGuid string) (app models.Application, apiErr error) { + path := fmt.Sprintf("%s/v2/spaces/%s/apps?q=%s&inline-relations-depth=1", repo.config.ApiEndpoint(), spaceGuid, url.QueryEscape("name:"+name)) + appResources := new(resources.PaginatedApplicationResources) + apiErr = repo.gateway.GetResource(path, appResources) + if apiErr != nil { + return + } + + if len(appResources.Resources) == 0 { + apiErr = errors.NewModelNotFoundError("App", name) + return + } + + res := appResources.Resources[0] + app = res.ToModel() + return +} + +func (repo CloudControllerApplicationRepository) Update(appGuid string, params models.AppParams) (updatedApp models.Application, apiErr error) { + data, err := repo.formatAppJSON(params) + if err != nil { + apiErr = errors.NewWithError(T("Failed to marshal JSON"), err) + return + } + + path := fmt.Sprintf("/v2/apps/%s?inline-relations-depth=1", appGuid) + resource := new(resources.ApplicationResource) + apiErr = repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(data), resource) + if apiErr != nil { + return + } + + updatedApp = resource.ToModel() + return +} + +func (repo CloudControllerApplicationRepository) formatAppJSON(input models.AppParams) (data string, err error) { + appResource := resources.NewApplicationEntityFromAppParams(input) + bytes, err := json.Marshal(appResource) + data = string(bytes) + return +} + +func (repo CloudControllerApplicationRepository) Delete(appGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/apps/%s?recursive=true", appGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerApplicationRepository) ReadEnv(guid string) (*models.Environment, error) { + var ( + err error + ) + + path := fmt.Sprintf("%s/v2/apps/%s/env", repo.config.ApiEndpoint(), guid) + appResource := models.NewEnvironment() + + err = repo.gateway.GetResource(path, appResource) + if err != nil { + return &models.Environment{}, err + } + + return appResource, err +} + +func (repo CloudControllerApplicationRepository) CreateRestageRequest(guid string) error { + path := fmt.Sprintf("/v2/apps/%s/restage", guid) + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(""), nil) +} diff --git a/cf/api/applications/applications_suite_test.go b/cf/api/applications/applications_suite_test.go new file mode 100644 index 00000000000..73192d207a6 --- /dev/null +++ b/cf/api/applications/applications_suite_test.go @@ -0,0 +1,19 @@ +package applications_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestApplications(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Applications Suite") +} diff --git a/cf/api/applications/applications_test.go b/cf/api/applications/applications_test.go new file mode 100644 index 00000000000..733c569e881 --- /dev/null +++ b/cf/api/applications/applications_test.go @@ -0,0 +1,450 @@ +package applications_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/applications" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplicationsRepository", func() { + Describe("finding apps by name", func() { + It("returns the app when it is found", func() { + ts, handler, repo := createAppRepo([]testnet.TestRequest{findAppRequest}) + defer ts.Close() + + app, apiErr := repo.Read("My App") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(app.Name).To(Equal("My App")) + Expect(app.Guid).To(Equal("app1-guid")) + Expect(app.Memory).To(Equal(int64(128))) + Expect(app.DiskQuota).To(Equal(int64(512))) + Expect(app.InstanceCount).To(Equal(1)) + Expect(app.EnvironmentVars).To(Equal(map[string]interface{}{"foo": "bar", "baz": "boom"})) + Expect(app.Routes[0].Host).To(Equal("app1")) + Expect(app.Routes[0].Domain.Name).To(Equal("cfapps.io")) + Expect(app.Stack.Name).To(Equal("awesome-stacks-ahoy")) + }) + + It("returns a failure response when the app is not found", func() { + request := testapi.NewCloudControllerTestRequest(findAppRequest) + request.Response = testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`} + + ts, handler, repo := createAppRepo([]testnet.TestRequest{request}) + defer ts.Close() + + _, apiErr := repo.Read("My App") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe(".ReadFromSpace", func() { + It("returns an application using the given space guid", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/another-space-guid/apps?q=name%3AMy+App&inline-relations-depth=1", + Response: singleAppResponse, + }) + ts, handler, repo := createAppRepo([]testnet.TestRequest{request}) + defer ts.Close() + app, err := repo.ReadFromSpace("My App", "another-space-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(app.Name).To(Equal("My App")) + }) + }) + + Describe("creating applications", func() { + It("makes the right request", func() { + ts, handler, repo := createAppRepo([]testnet.TestRequest{createApplicationRequest}) + defer ts.Close() + + params := defaultAppParams() + createdApp, apiErr := repo.Create(params) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + app := models.Application{} + app.Name = "my-cool-app" + app.Guid = "my-cool-app-guid" + Expect(createdApp).To(Equal(app)) + }) + + It("omits fields that are not set", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/apps", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-app","instances":3,"memory":2048,"disk_quota":512,"space_guid":"some-space-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: createApplicationResponse}, + }) + + ts, handler, repo := createAppRepo([]testnet.TestRequest{request}) + defer ts.Close() + + params := defaultAppParams() + params.BuildpackUrl = nil + params.StackGuid = nil + params.Command = nil + + _, apiErr := repo.Create(params) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("reading environment for an app", func() { + Context("when the response can be parsed as json", func() { + var ( + testServer *httptest.Server + userEnv *models.Environment + err error + handler *testnet.TestHandler + repo ApplicationRepository + ) + + AfterEach(func() { + testServer.Close() + }) + + Context("when there are system provided env vars", func() { + BeforeEach(func() { + + var appEnvRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/apps/some-cool-app-guid/env", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "staging_env_json": { + "STAGING_ENV": "staging_value", + "staging": true, + "number": 42 + }, + "running_env_json": { + "RUNNING_ENV": "running_value", + "running": false, + "number": 37 + }, + "environment_json": { + "key": "value", + "number": 123, + "bool": true + }, + "system_env_json": { + "VCAP_SERVICES": { + "system_hash": { + "system_key": "system_value" + } + } + } +} +`, + }}) + + testServer, handler, repo = createAppRepo([]testnet.TestRequest{appEnvRequest}) + userEnv, err = repo.ReadEnv("some-cool-app-guid") + Expect(err).ToNot(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + It("returns the user environment, vcap services, running/staging env variables", func() { + Expect(userEnv.Environment["key"]).To(Equal("value")) + Expect(userEnv.Environment["number"]).To(Equal(float64(123))) + Expect(userEnv.Environment["bool"]).To(BeTrue()) + Expect(userEnv.Running["RUNNING_ENV"]).To(Equal("running_value")) + Expect(userEnv.Running["running"]).To(BeFalse()) + Expect(userEnv.Running["number"]).To(Equal(float64(37))) + Expect(userEnv.Staging["STAGING_ENV"]).To(Equal("staging_value")) + Expect(userEnv.Staging["staging"]).To(BeTrue()) + Expect(userEnv.Staging["number"]).To(Equal(float64(42))) + + vcapServices := userEnv.System["VCAP_SERVICES"] + data, err := json.Marshal(vcapServices) + + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(ContainSubstring("\"system_key\":\"system_value\"")) + }) + + }) + + Context("when there are no environment variables", func() { + BeforeEach(func() { + var emptyEnvRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/apps/some-cool-app-guid/env", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"system_env_json": {"VCAP_SERVICES": {} }}`, + }}) + + testServer, handler, repo = createAppRepo([]testnet.TestRequest{emptyEnvRequest}) + userEnv, err = repo.ReadEnv("some-cool-app-guid") + Expect(err).ToNot(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + It("returns an empty string", func() { + Expect(len(userEnv.Environment)).To(Equal(0)) + Expect(len(userEnv.System["VCAP_SERVICES"].(map[string]interface{}))).To(Equal(0)) + }) + }) + }) + }) + + Describe("restaging applications", func() { + It("POSTs to the right URL", func() { + appRestageRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/apps/some-cool-app-guid/restage", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: "", + }, + }) + + ts, handler, repo := createAppRepo([]testnet.TestRequest{appRestageRequest}) + defer ts.Close() + + repo.CreateRestageRequest("some-cool-app-guid") + Expect(handler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("updating applications", func() { + It("makes the right request", func() { + ts, handler, repo := createAppRepo([]testnet.TestRequest{updateApplicationRequest}) + defer ts.Close() + + app := models.Application{} + app.Guid = "my-app-guid" + app.Name = "my-cool-app" + app.BuildpackUrl = "buildpack-url" + app.Command = "some-command" + app.Memory = 2048 + app.InstanceCount = 3 + app.Stack = &models.Stack{Guid: "some-stack-guid"} + app.SpaceGuid = "some-space-guid" + app.State = "started" + app.DiskQuota = 512 + Expect(app.EnvironmentVars).To(BeNil()) + + updatedApp, apiErr := repo.Update(app.Guid, app.ToParams()) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(updatedApp.Command).To(Equal("some-command")) + Expect(updatedApp.DetectedStartCommand).To(Equal("detected command")) + Expect(updatedApp.Name).To(Equal("my-cool-app")) + Expect(updatedApp.Guid).To(Equal("my-cool-app-guid")) + }) + + It("sets environment variables", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/app1-guid", + Matcher: testnet.RequestBodyMatcher(`{"environment_json":{"DATABASE_URL":"mysql://example.com/my-db"}}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createAppRepo([]testnet.TestRequest{request}) + defer ts.Close() + + envParams := map[string]interface{}{"DATABASE_URL": "mysql://example.com/my-db"} + params := models.AppParams{EnvironmentVars: &envParams} + + _, apiErr := repo.Update("app1-guid", params) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("can remove environment variables", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/app1-guid", + Matcher: testnet.RequestBodyMatcher(`{"environment_json":{}}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createAppRepo([]testnet.TestRequest{request}) + defer ts.Close() + + envParams := map[string]interface{}{} + params := models.AppParams{EnvironmentVars: &envParams} + + _, apiErr := repo.Update("app1-guid", params) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + It("deletes applications", func() { + deleteApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/apps/my-cool-app-guid?recursive=true", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ""}, + }) + + ts, handler, repo := createAppRepo([]testnet.TestRequest{deleteApplicationRequest}) + defer ts.Close() + + apiErr := repo.Delete("my-cool-app-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) +}) + +var singleAppResponse = testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "app1-guid" + }, + "entity": { + "name": "My App", + "environment_json": { + "foo": "bar", + "baz": "boom" + }, + "memory": 128, + "instances": 1, + "disk_quota": 512, + "state": "STOPPED", + "stack": { + "metadata": { + "guid": "app1-route-guid" + }, + "entity": { + "name": "awesome-stacks-ahoy" + } + }, + "routes": [ + { + "metadata": { + "guid": "app1-route-guid" + }, + "entity": { + "host": "app1", + "domain": { + "metadata": { + "guid": "domain1-guid" + }, + "entity": { + "name": "cfapps.io" + } + } + } + } + ] + } + } + ] +}`} + +var findAppRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/apps?q=name%3AMy+App&inline-relations-depth=1", + Response: singleAppResponse, +}) + +var createApplicationResponse = ` +{ + "metadata": { + "guid": "my-cool-app-guid" + }, + "entity": { + "name": "my-cool-app" + } +}` + +var createApplicationRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/apps", + Matcher: testnet.RequestBodyMatcher(`{ + "name":"my-cool-app", + "instances":3, + "buildpack":"buildpack-url", + "memory":2048, + "disk_quota": 512, + "space_guid":"some-space-guid", + "stack_guid":"some-stack-guid", + "command":"some-command" + }`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: createApplicationResponse}, +}) + +func defaultAppParams() models.AppParams { + name := "my-cool-app" + buildpackUrl := "buildpack-url" + spaceGuid := "some-space-guid" + stackGuid := "some-stack-guid" + command := "some-command" + memory := int64(2048) + diskQuota := int64(512) + instanceCount := 3 + + return models.AppParams{ + Name: &name, + BuildpackUrl: &buildpackUrl, + SpaceGuid: &spaceGuid, + StackGuid: &stackGuid, + Command: &command, + Memory: &memory, + DiskQuota: &diskQuota, + InstanceCount: &instanceCount, + } +} + +var updateApplicationResponse = ` +{ + "metadata": { + "guid": "my-cool-app-guid" + }, + "entity": { + "name": "my-cool-app", + "command": "some-command", + "detected_start_command": "detected command" + } +}` + +var updateApplicationRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-app-guid?inline-relations-depth=1", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-app","instances":3,"buildpack":"buildpack-url","memory":2048,"disk_quota":512,"space_guid":"some-space-guid","state":"STARTED","stack_guid":"some-stack-guid","command":"some-command"}`), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: updateApplicationResponse}, +}) + +func createAppRepo(requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ApplicationRepository) { + ts, handler = testnet.NewServer(requests) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerApplicationRepository(configRepo, gateway) + return +} diff --git a/cf/api/applications/fakes/fake_application_repository.go b/cf/api/applications/fakes/fake_application_repository.go new file mode 100644 index 00000000000..b0f67a96578 --- /dev/null +++ b/cf/api/applications/fakes/fake_application_repository.go @@ -0,0 +1,196 @@ +package fakes + +import ( + "errors" + "sync" + + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeApplicationRepository struct { + FindAllApps []models.Application + + ReadCalls int + ReadArgs struct { + Name string + } + ReadReturns struct { + App models.Application + Error error + } + + CreateAppParams []models.AppParams + + UpdateParams models.AppParams + UpdateAppGuid string + UpdateAppResult models.Application + UpdateErr bool + + DeletedAppGuid string + + CreateRestageRequestArgs struct { + AppGuid string + } + + ReadFromSpaceStub func(name string, spaceGuid string) (app models.Application, apiErr error) + readFromSpaceMutex sync.RWMutex + readFromSpaceArgsForCall []struct { + name string + spaceGuid string + } + readFromSpaceReturns struct { + result1 models.Application + result2 error + } + + ReadEnvStub func(guid string) (*models.Environment, error) + readEnvMutex sync.RWMutex + readEnvArgsForCall []struct { + guid string + } + readEnvReturns struct { + result1 *models.Environment + result2 error + } +} + +//counterfeiter section +func (fake *FakeApplicationRepository) ReadFromSpace(name string, spaceGuid string) (app models.Application, apiErr error) { + fake.readFromSpaceMutex.Lock() + defer fake.readFromSpaceMutex.Unlock() + fake.readFromSpaceArgsForCall = append(fake.readFromSpaceArgsForCall, struct { + name string + spaceGuid string + }{name, spaceGuid}) + if fake.ReadFromSpaceStub != nil { + return fake.ReadFromSpaceStub(name, spaceGuid) + } else { + return fake.readFromSpaceReturns.result1, fake.readFromSpaceReturns.result2 + } +} + +func (fake *FakeApplicationRepository) ReadFromSpaceCallCount() int { + fake.readFromSpaceMutex.RLock() + defer fake.readFromSpaceMutex.RUnlock() + return len(fake.readFromSpaceArgsForCall) +} + +func (fake *FakeApplicationRepository) ReadFromSpaceArgsForCall(i int) (string, string) { + fake.readFromSpaceMutex.RLock() + defer fake.readFromSpaceMutex.RUnlock() + return fake.readFromSpaceArgsForCall[i].name, fake.readFromSpaceArgsForCall[i].spaceGuid +} + +func (fake *FakeApplicationRepository) ReadFromSpaceReturns(result1 models.Application, result2 error) { + fake.readFromSpaceReturns = struct { + result1 models.Application + result2 error + }{result1, result2} +} +func (fake *FakeApplicationRepository) ReadEnv(guid string) (*models.Environment, error) { + fake.readEnvMutex.Lock() + fake.readEnvArgsForCall = append(fake.readEnvArgsForCall, struct { + guid string + }{guid}) + fake.readEnvMutex.Unlock() + if fake.ReadEnvStub != nil { + return fake.ReadEnvStub(guid) + } else { + return fake.readEnvReturns.result1, fake.readEnvReturns.result2 + } +} + +func (fake *FakeApplicationRepository) ReadEnvCallCount() int { + fake.readEnvMutex.RLock() + defer fake.readEnvMutex.RUnlock() + return len(fake.readEnvArgsForCall) +} + +func (fake *FakeApplicationRepository) ReadEnvArgsForCall(i int) string { + fake.readEnvMutex.RLock() + defer fake.readEnvMutex.RUnlock() + return fake.readEnvArgsForCall[i].guid +} + +func (fake *FakeApplicationRepository) ReadEnvReturns(result1 *models.Environment, result2 error) { + fake.ReadEnvStub = nil + fake.readEnvReturns = struct { + result1 *models.Environment + result2 error + }{result1, result2} +} + +//End counterfeiter section + +func (repo *FakeApplicationRepository) Read(name string) (app models.Application, apiErr error) { + repo.ReadCalls++ + repo.ReadArgs.Name = name + return repo.ReadReturns.App, repo.ReadReturns.Error +} + +func (repo *FakeApplicationRepository) CreatedAppParams() (params models.AppParams) { + if len(repo.CreateAppParams) > 0 { + params = repo.CreateAppParams[0] + } + return +} + +func (repo *FakeApplicationRepository) Create(params models.AppParams) (resultApp models.Application, apiErr error) { + if repo.CreateAppParams == nil { + repo.CreateAppParams = []models.AppParams{} + } + + repo.CreateAppParams = append(repo.CreateAppParams, params) + + resultApp.Guid = *params.Name + "-guid" + resultApp.Name = *params.Name + resultApp.State = "stopped" + resultApp.EnvironmentVars = map[string]interface{}{} + + if params.SpaceGuid != nil { + resultApp.SpaceGuid = *params.SpaceGuid + } + if params.BuildpackUrl != nil { + resultApp.BuildpackUrl = *params.BuildpackUrl + } + if params.Command != nil { + resultApp.Command = *params.Command + } + if params.DiskQuota != nil { + resultApp.DiskQuota = *params.DiskQuota + } + if params.InstanceCount != nil { + resultApp.InstanceCount = *params.InstanceCount + } + if params.Memory != nil { + resultApp.Memory = *params.Memory + } + if params.EnvironmentVars != nil { + resultApp.EnvironmentVars = *params.EnvironmentVars + } + + return +} + +func (repo *FakeApplicationRepository) Update(appGuid string, params models.AppParams) (updatedApp models.Application, apiErr error) { + repo.UpdateAppGuid = appGuid + repo.UpdateParams = params + updatedApp = repo.UpdateAppResult + if repo.UpdateErr { + apiErr = errors.New("Error updating app.") + } + return +} + +func (repo *FakeApplicationRepository) Delete(appGuid string) (apiErr error) { + repo.DeletedAppGuid = appGuid + return +} + +func (repo *FakeApplicationRepository) CreateRestageRequest(guid string) (apiErr error) { + repo.CreateRestageRequestArgs.AppGuid = guid + return nil +} + +var _ applications.ApplicationRepository = new(FakeApplicationRepository) diff --git a/cf/api/authentication/authentication.go b/cf/api/authentication/authentication.go new file mode 100644 index 00000000000..10aa70d16c2 --- /dev/null +++ b/cf/api/authentication/authentication.go @@ -0,0 +1,147 @@ +package authentication + +import ( + "encoding/base64" + "fmt" + "net/url" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/net" +) + +type TokenRefresher interface { + RefreshAuthToken() (updatedToken string, apiErr error) +} + +type AuthenticationRepository interface { + RefreshAuthToken() (updatedToken string, apiErr error) + Authenticate(credentials map[string]string) (apiErr error) + GetLoginPromptsAndSaveUAAServerURL() (map[string]core_config.AuthPrompt, error) +} + +type UAAAuthenticationRepository struct { + config core_config.ReadWriter + gateway net.Gateway +} + +func NewUAAAuthenticationRepository(gateway net.Gateway, config core_config.ReadWriter) (uaa UAAAuthenticationRepository) { + uaa.gateway = gateway + uaa.config = config + return +} + +func (uaa UAAAuthenticationRepository) Authenticate(credentials map[string]string) (apiErr error) { + data := url.Values{ + "grant_type": {"password"}, + "scope": {""}, + } + for key, val := range credentials { + data[key] = []string{val} + } + + apiErr = uaa.getAuthToken(data) + switch response := apiErr.(type) { + case errors.HttpError: + if response.StatusCode() == 401 { + apiErr = errors.New(T("Credentials were rejected, please try again.")) + } + } + + return +} + +type LoginResource struct { + Prompts map[string][]string + Links map[string]string +} + +var knownAuthPromptTypes = map[string]core_config.AuthPromptType{ + "text": core_config.AuthPromptTypeText, + "password": core_config.AuthPromptTypePassword, +} + +func (r *LoginResource) parsePrompts() (prompts map[string]core_config.AuthPrompt) { + prompts = make(map[string]core_config.AuthPrompt) + for key, val := range r.Prompts { + prompts[key] = core_config.AuthPrompt{ + Type: knownAuthPromptTypes[val[0]], + DisplayName: val[1], + } + } + return +} + +func (uaa UAAAuthenticationRepository) GetLoginPromptsAndSaveUAAServerURL() (prompts map[string]core_config.AuthPrompt, apiErr error) { + url := fmt.Sprintf("%s/login", uaa.config.AuthenticationEndpoint()) + resource := &LoginResource{} + apiErr = uaa.gateway.GetResource(url, resource) + + prompts = resource.parsePrompts() + if resource.Links["uaa"] == "" { + uaa.config.SetUaaEndpoint(uaa.config.AuthenticationEndpoint()) + } else { + uaa.config.SetUaaEndpoint(resource.Links["uaa"]) + } + return +} + +func (uaa UAAAuthenticationRepository) RefreshAuthToken() (string, error) { + data := url.Values{ + "refresh_token": {uaa.config.RefreshToken()}, + "grant_type": {"refresh_token"}, + "scope": {""}, + } + + apiErr := uaa.getAuthToken(data) + updatedToken := uaa.config.AccessToken() + + return updatedToken, apiErr +} + +func (uaa UAAAuthenticationRepository) getAuthToken(data url.Values) error { + type uaaErrorResponse struct { + Code string `json:"error"` + Description string `json:"error_description"` + } + + type AuthenticationResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + Error uaaErrorResponse `json:"error"` + } + + path := fmt.Sprintf("%s/oauth/token", uaa.config.AuthenticationEndpoint()) + request, err := uaa.gateway.NewRequest("POST", path, "Basic "+base64.StdEncoding.EncodeToString([]byte("cf:")), strings.NewReader(data.Encode())) + if err != nil { + return errors.NewWithError(T("Failed to start oauth request"), err) + } + request.HttpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + response := new(AuthenticationResponse) + _, err = uaa.gateway.PerformRequestForJSONResponse(request, &response) + + switch err.(type) { + case nil: + case errors.HttpError: + return err + case *errors.InvalidTokenError: + return errors.New(T("Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a -u -o -s ` to log back in and re-authenticate.")) + default: + return errors.NewWithError(T("auth request failed"), err) + } + + // TODO: get the actual status code + if response.Error.Code != "" { + return errors.NewHttpError(0, response.Error.Code, response.Error.Description) + } + + uaa.config.SetAccessToken(fmt.Sprintf("%s %s", response.TokenType, response.AccessToken)) + uaa.config.SetRefreshToken(response.RefreshToken) + + return nil +} diff --git a/cf/api/authentication/authentication_suite_test.go b/cf/api/authentication/authentication_suite_test.go new file mode 100644 index 00000000000..9aecfdbbbbf --- /dev/null +++ b/cf/api/authentication/authentication_suite_test.go @@ -0,0 +1,19 @@ +package authentication_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAuthentication(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Authentication Suite") +} diff --git a/cf/api/authentication/authentication_test.go b/cf/api/authentication/authentication_test.go new file mode 100644 index 00000000000..28c4aaaf4de --- /dev/null +++ b/cf/api/authentication/authentication_test.go @@ -0,0 +1,337 @@ +package authentication_test + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/authentication" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AuthenticationRepository", func() { + var ( + gateway net.Gateway + testServer *httptest.Server + handler *testnet.TestHandler + config core_config.ReadWriter + auth AuthenticationRepository + ) + + BeforeEach(func() { + config = testconfig.NewRepository() + gateway = net.NewUAAGateway(config, &testterm.FakeUI{}) + auth = NewUAAAuthenticationRepository(gateway, config) + }) + + AfterEach(func() { + testServer.Close() + }) + + var setupTestServer = func(request testnet.TestRequest) { + testServer, handler = testnet.NewServer([]testnet.TestRequest{request}) + config.SetAuthenticationEndpoint(testServer.URL) + } + + Describe("authenticating", func() { + var err error + + JustBeforeEach(func() { + err = auth.Authenticate(map[string]string{ + "username": "foo@example.com", + "password": "bar", + }) + }) + + Describe("when login succeeds", func() { + BeforeEach(func() { + setupTestServer(successfulLoginRequest) + }) + + It("stores the access and refresh tokens in the config", func() { + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(config.AuthenticationEndpoint()).To(Equal(testServer.URL)) + Expect(config.AccessToken()).To(Equal("BEARER my_access_token")) + Expect(config.RefreshToken()).To(Equal("my_refresh_token")) + }) + }) + + Describe("when login fails", func() { + BeforeEach(func() { + setupTestServer(unsuccessfulLoginRequest) + }) + + It("returns an error", func() { + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(Equal("Credentials were rejected, please try again.")) + Expect(config.AccessToken()).To(BeEmpty()) + Expect(config.RefreshToken()).To(BeEmpty()) + }) + }) + + Describe("when an error occurs during login", func() { + BeforeEach(func() { + setupTestServer(errorLoginRequest) + }) + + It("returns a failure response", func() { + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("Server error, status code: 500, error code: , message: ")) + Expect(config.AccessToken()).To(BeEmpty()) + }) + }) + + Describe("when the UAA server has an error but still returns a 200", func() { + BeforeEach(func() { + setupTestServer(errorMaskedAsSuccessLoginRequest) + }) + + It("returns an error", func() { + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io")) + Expect(config.AccessToken()).To(BeEmpty()) + }) + }) + }) + + Describe("getting login info", func() { + var ( + apiErr error + prompts map[string]core_config.AuthPrompt + ) + + JustBeforeEach(func() { + prompts, apiErr = auth.GetLoginPromptsAndSaveUAAServerURL() + }) + + Describe("when the login info API succeeds", func() { + BeforeEach(func() { + setupTestServer(loginServerLoginRequest) + }) + + It("does not return an error", func() { + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("gets the login prompts", func() { + Expect(prompts).To(Equal(map[string]core_config.AuthPrompt{ + "username": core_config.AuthPrompt{ + DisplayName: "Email", + Type: core_config.AuthPromptTypeText, + }, + "pin": core_config.AuthPrompt{ + DisplayName: "PIN Number", + Type: core_config.AuthPromptTypePassword, + }, + })) + }) + + It("saves the UAA server to the config", func() { + Expect(config.UaaEndpoint()).To(Equal("https://uaa.run.pivotal.io")) + }) + }) + + Describe("when the login info API fails", func() { + BeforeEach(func() { + setupTestServer(loginServerLoginFailureRequest) + }) + + It("returns a failure response when the login info API fails", func() { + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + Expect(prompts).To(BeEmpty()) + }) + }) + + Context("when the response does not contain links", func() { + BeforeEach(func() { + setupTestServer(uaaServerLoginRequest) + }) + + It("presumes that the authorization server is the UAA", func() { + Expect(config.UaaEndpoint()).To(Equal(config.AuthenticationEndpoint())) + }) + }) + }) + + Describe("refreshing the auth token", func() { + var refreshedToken string + var apiErr error + + JustBeforeEach(func() { + refreshedToken, apiErr = auth.RefreshAuthToken() + }) + + Context("when the refresh token has expired", func() { + BeforeEach(func() { + setupTestServer(refreshTokenExpiredRequestError) + }) + It("the returns the reauthentication error message", func() { + Expect(apiErr.Error()).To(Equal("Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a -u -o -s ` to log back in and re-authenticate.")) + }) + }) + Context("when there is a UAA error", func() { + BeforeEach(func() { + setupTestServer(errorLoginRequest) + }) + + It("returns the API error", func() { + Expect(apiErr).NotTo(BeNil()) + }) + }) + }) +}) + +var authHeaders = http.Header{ + "accept": {"application/json"}, + "content-type": {"application/x-www-form-urlencoded"}, + "authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("cf:"))}, +} + +var successfulLoginRequest = testnet.TestRequest{ + Method: "POST", + Path: "/oauth/token", + Header: authHeaders, + Matcher: successfulLoginMatcher, + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "access_token": "my_access_token", + "token_type": "BEARER", + "refresh_token": "my_refresh_token", + "scope": "openid", + "expires_in": 98765 +} `}, +} + +var successfulLoginMatcher = func(request *http.Request) { + err := request.ParseForm() + if err != nil { + Fail(fmt.Sprintf("Failed to parse form: %s", err)) + return + } + + Expect(request.Form.Get("username")).To(Equal("foo@example.com")) + Expect(request.Form.Get("password")).To(Equal("bar")) + Expect(request.Form.Get("grant_type")).To(Equal("password")) + Expect(request.Form.Get("scope")).To(Equal("")) +} + +var unsuccessfulLoginRequest = testnet.TestRequest{ + Method: "POST", + Path: "/oauth/token", + Response: testnet.TestResponse{ + Status: http.StatusUnauthorized, + }, +} +var refreshTokenExpiredRequestError = testnet.TestRequest{ + Method: "POST", + Path: "/oauth/token", + Response: testnet.TestResponse{ + Status: http.StatusUnauthorized, + Body: ` +{ + "error": "invalid_token", + "error_description": "Invalid auth token: Invalid refresh token (expired): eyJhbGckjsdfdf" +} +`}, +} + +var errorLoginRequest = testnet.TestRequest{ + Method: "POST", + Path: "/oauth/token", + Response: testnet.TestResponse{ + Status: http.StatusInternalServerError, + }, +} + +var errorMaskedAsSuccessLoginRequest = testnet.TestRequest{ + Method: "POST", + Path: "/oauth/token", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "error": { + "error": "rest_client_error", + "error_description": "I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io" + } +} +`}, +} + +var loginServerLoginRequest = testnet.TestRequest{ + Method: "GET", + Path: "/login", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "timestamp":"2013-12-18T11:26:53-0700", + "app":{ + "artifact":"cloudfoundry-identity-uaa", + "description":"User Account and Authentication Service", + "name":"UAA", + "version":"1.4.7" + }, + "commit_id":"2701cc8", + "links":{ + "register":"https://console.run.pivotal.io/register", + "passwd":"https://console.run.pivotal.io/password_resets/new", + "home":"https://console.run.pivotal.io", + "support":"https://support.cloudfoundry.com/home", + "login":"https://login.run.pivotal.io", + "uaa":"https://uaa.run.pivotal.io" + }, + "prompts":{ + "username": ["text","Email"], + "pin": ["password", "PIN Number"] + } +}`, + }, +} + +var loginServerLoginFailureRequest = testnet.TestRequest{ + Method: "GET", + Path: "/login", + Response: testnet.TestResponse{ + Status: http.StatusInternalServerError, + }, +} + +var uaaServerLoginRequest = testnet.TestRequest{ + Method: "GET", + Path: "/login", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "timestamp":"2013-12-18T11:26:53-0700", + "app":{ + "artifact":"cloudfoundry-identity-uaa", + "description":"User Account and Authentication Service", + "name":"UAA", + "version":"1.4.7" + }, + "commit_id":"2701cc8", + "prompts":{ + "username": ["text","Email"], + "pin": ["password", "PIN Number"] + } +}`, + }, +} diff --git a/cf/api/buildpack_bits.go b/cf/api/buildpack_bits.go new file mode 100644 index 00000000000..cad02f88894 --- /dev/null +++ b/cf/api/buildpack_bits.go @@ -0,0 +1,257 @@ +package api + +import ( + "archive/zip" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "mime/multipart" + gonet "net" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/gofileutils/fileutils" +) + +type BuildpackBitsRepository interface { + UploadBuildpack(buildpack models.Buildpack, dir string) (apiErr error) +} + +type CloudControllerBuildpackBitsRepository struct { + config core_config.Reader + gateway net.Gateway + zipper app_files.Zipper + TrustedCerts []tls.Certificate +} + +func NewCloudControllerBuildpackBitsRepository(config core_config.Reader, gateway net.Gateway, zipper app_files.Zipper) (repo CloudControllerBuildpackBitsRepository) { + repo.config = config + repo.gateway = gateway + repo.zipper = zipper + return +} + +func (repo CloudControllerBuildpackBitsRepository) UploadBuildpack(buildpack models.Buildpack, buildpackLocation string) (apiErr error) { + fileutils.TempFile("buildpack-upload", func(zipFileToUpload *os.File, err error) { + if err != nil { + apiErr = errors.NewWithError(T("Couldn't create temp file for upload"), err) + return + } + + var buildpackFileName string + if isWebURL(buildpackLocation) { + buildpackFileName = path.Base(buildpackLocation) + repo.downloadBuildpack(buildpackLocation, func(downloadFile *os.File, downloadErr error) { + if downloadErr != nil { + err = downloadErr + return + } + + err = normalizeBuildpackArchive(downloadFile, zipFileToUpload) + }) + } else { + buildpackFileName = filepath.Base(buildpackLocation) + + stats, statError := os.Stat(buildpackLocation) + if statError != nil { + apiErr = errors.NewWithError(T("Error opening buildpack file"), statError) + err = statError + return + } + + if stats.IsDir() { + buildpackFileName += ".zip" // FIXME: remove once #71167394 is fixed + err = repo.zipper.Zip(buildpackLocation, zipFileToUpload) + } else { + specifiedFile, openError := os.Open(buildpackLocation) + if openError != nil { + apiErr = errors.NewWithError(T("Couldn't open buildpack file"), openError) + err = openError + return + } + err = normalizeBuildpackArchive(specifiedFile, zipFileToUpload) + } + } + + if err != nil { + apiErr = errors.NewWithError(T("Couldn't write zip file"), err) + return + } + + apiErr = repo.uploadBits(buildpack, zipFileToUpload, buildpackFileName) + }) + + return +} + +func normalizeBuildpackArchive(inputFile *os.File, outputFile *os.File) error { + stats, err := inputFile.Stat() + if err != nil { + return err + } + + reader, err := zip.NewReader(inputFile, stats.Size()) + if err != nil { + return err + } + + contents := reader.File + + parentPath, hasBuildpack := findBuildpackPath(contents) + + if !hasBuildpack { + return errors.New(T("Zip archive does not contain a buildpack")) + } + + writer := zip.NewWriter(outputFile) + + for _, file := range contents { + name := file.Name + if strings.HasPrefix(name, parentPath) { + relativeFilename := strings.TrimPrefix(name, parentPath+"/") + if relativeFilename == "" { + continue + } + + fileInfo := file.FileInfo() + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + header.Name = relativeFilename + + w, err := writer.CreateHeader(header) + if err != nil { + return err + } + + r, err := file.Open() + if err != nil { + return err + } + + io.Copy(w, r) + err = r.Close() + if err != nil { + return err + } + } + } + + writer.Close() + outputFile.Seek(0, 0) + return nil +} + +func findBuildpackPath(zipFiles []*zip.File) (parentPath string, foundBuildpack bool) { + needle := "bin/compile" + + for _, file := range zipFiles { + if strings.HasSuffix(file.Name, needle) { + foundBuildpack = true + parentPath = path.Join(file.Name, "..", "..") + if parentPath == "." { + parentPath = "" + } + return + } + } + return +} + +func isWebURL(path string) bool { + return strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") +} + +func (repo CloudControllerBuildpackBitsRepository) downloadBuildpack(url string, cb func(*os.File, error)) { + fileutils.TempFile("buildpack-download", func(tempfile *os.File, err error) { + if err != nil { + cb(nil, err) + return + } + + var certPool *x509.CertPool + if len(repo.TrustedCerts) > 0 { + certPool = x509.NewCertPool() + for _, tlsCert := range repo.TrustedCerts { + cert, _ := x509.ParseCertificate(tlsCert.Certificate[0]) + certPool.AddCert(cert) + } + } + + client := &http.Client{ + Transport: &http.Transport{ + Dial: (&gonet.Dialer{Timeout: 5 * time.Second}).Dial, + TLSClientConfig: &tls.Config{RootCAs: certPool}, + Proxy: http.ProxyFromEnvironment, + }, + } + + response, err := client.Get(url) + if err != nil { + cb(nil, err) + return + } + defer response.Body.Close() + + io.Copy(tempfile, response.Body) + tempfile.Seek(0, 0) + cb(tempfile, nil) + }) +} + +func (repo CloudControllerBuildpackBitsRepository) uploadBits(buildpack models.Buildpack, body io.Reader, buildpackName string) error { + return repo.performMultiPartUpload( + fmt.Sprintf("%s/v2/buildpacks/%s/bits", repo.config.ApiEndpoint(), buildpack.Guid), + "buildpack", + buildpackName, + body) +} + +func (repo CloudControllerBuildpackBitsRepository) performMultiPartUpload(url string, fieldName string, fileName string, body io.Reader) (apiErr error) { + fileutils.TempFile("requests", func(requestFile *os.File, err error) { + if err != nil { + apiErr = err + return + } + + writer := multipart.NewWriter(requestFile) + part, err := writer.CreateFormFile(fieldName, fileName) + + if err != nil { + writer.Close() + return + } + + _, err = io.Copy(part, body) + writer.Close() + + if err != nil { + apiErr = errors.NewWithError(T("Error creating upload"), err) + return + } + + var request *net.Request + request, apiErr = repo.gateway.NewRequestForFile("PUT", url, repo.config.AccessToken(), requestFile) + contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary()) + request.HttpReq.Header.Set("Content-Type", contentType) + if apiErr != nil { + return + } + + _, apiErr = repo.gateway.PerformRequest(request) + }) + + return +} diff --git a/cf/api/buildpack_bits_test.go b/cf/api/buildpack_bits_test.go new file mode 100644 index 00000000000..985d459e635 --- /dev/null +++ b/cf/api/buildpack_bits_test.go @@ -0,0 +1,232 @@ +package api_test + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "sort" + "time" + + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildpackBitsRepository", func() { + var ( + buildpacksDir string + configRepo core_config.Repository + repo CloudControllerBuildpackBitsRepository + buildpack models.Buildpack + testServer *httptest.Server + testServerHandler *testnet.TestHandler + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + pwd, _ := os.Getwd() + + buildpacksDir = filepath.Join(pwd, "../../fixtures/buildpacks") + repo = NewCloudControllerBuildpackBitsRepository(configRepo, gateway, app_files.ApplicationZipper{}) + buildpack = models.Buildpack{Name: "my-cool-buildpack", Guid: "my-cool-buildpack-guid"} + + testServer, testServerHandler = testnet.NewServer([]testnet.TestRequest{uploadBuildpackRequest()}) + configRepo.SetApiEndpoint(testServer.URL) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("#UploadBuildpack", func() { + It("fails to upload a buildpack with an invalid directory", func() { + apiErr := repo.UploadBuildpack(buildpack, "/foo/bar") + Expect(apiErr).NotTo(BeNil()) + Expect(apiErr.Error()).To(ContainSubstring("Error opening buildpack file")) + }) + + It("uploads a valid buildpack directory", func() { + buildpackPath := filepath.Join(buildpacksDir, "example-buildpack") + + os.Chmod(filepath.Join(buildpackPath, "bin/compile"), 0755) + os.Chmod(filepath.Join(buildpackPath, "bin/detect"), 0755) + err := os.Chmod(filepath.Join(buildpackPath, "bin/release"), 0755) + Expect(err).NotTo(HaveOccurred()) + + apiErr := repo.UploadBuildpack(buildpack, buildpackPath) + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("uploads a valid zipped buildpack", func() { + buildpackPath := filepath.Join(buildpacksDir, "example-buildpack.zip") + + apiErr := repo.UploadBuildpack(buildpack, buildpackPath) + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + Describe("when the buildpack is wrapped in an extra top-level directory", func() { + It("uploads a zip file containing only the actual buildpack", func() { + buildpackPath := filepath.Join(buildpacksDir, "example-buildpack-in-dir.zip") + + apiErr := repo.UploadBuildpack(buildpack, buildpackPath) + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("when given the URL of a buildpack", func() { + var buildpackFileServerHandler = func(buildpackName string) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + Expect(request.URL.Path).To(Equal(fmt.Sprintf("/place/%s", buildpackName))) + f, err := os.Open(filepath.Join(buildpacksDir, buildpackName)) + Expect(err).NotTo(HaveOccurred()) + io.Copy(writer, f) + } + } + + Context("when the downloaded resource is not a valid zip file", func() { + It("fails gracefully", func() { + fileServer := httptest.NewServer(buildpackFileServerHandler("bad-buildpack.zip")) + defer fileServer.Close() + + apiErr := repo.UploadBuildpack(buildpack, fileServer.URL+"/place/bad-buildpack.zip") + Expect(testServerHandler).NotTo(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + }) + }) + + It("uploads the file over HTTP", func() { + fileServer := httptest.NewServer(buildpackFileServerHandler("example-buildpack.zip")) + defer fileServer.Close() + + apiErr := repo.UploadBuildpack(buildpack, fileServer.URL+"/place/example-buildpack.zip") + + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("uploads the file over HTTPS", func() { + fileServer := httptest.NewTLSServer(buildpackFileServerHandler("example-buildpack.zip")) + defer fileServer.Close() + + repo.TrustedCerts = fileServer.TLS.Certificates + apiErr := repo.UploadBuildpack(buildpack, fileServer.URL+"/place/example-buildpack.zip") + + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("fails when the server's SSL cert cannot be verified", func() { + fileServer := httptest.NewTLSServer(buildpackFileServerHandler("example-buildpack.zip")) + defer fileServer.Close() + + apiErr := repo.UploadBuildpack(buildpack, fileServer.URL+"/place/example-buildpack.zip") + + Expect(testServerHandler).NotTo(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + }) + + Describe("when the buildpack is wrapped in an extra top-level directory", func() { + It("uploads a zip file containing only the actual buildpack", func() { + fileServer := httptest.NewTLSServer(buildpackFileServerHandler("example-buildpack-in-dir.zip")) + defer fileServer.Close() + + repo.TrustedCerts = fileServer.TLS.Certificates + apiErr := repo.UploadBuildpack(buildpack, fileServer.URL+"/place/example-buildpack-in-dir.zip") + + Expect(testServerHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + It("returns an unsuccessful response when the server cannot be reached", func() { + apiErr := repo.UploadBuildpack(buildpack, "https://domain.bad-domain:223453/no-place/example-buildpack.zip") + Expect(testServerHandler).NotTo(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + }) + }) + }) +}) + +func uploadBuildpackRequest() testnet.TestRequest { + return testnet.TestRequest{ + Method: "PUT", + Path: "/v2/buildpacks/my-cool-buildpack-guid/bits", + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{ "metadata":{ "guid": "my-job-guid" } }`, + }, + Matcher: func(request *http.Request) { + err := request.ParseMultipartForm(4096) + defer request.MultipartForm.RemoveAll() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(request.MultipartForm.Value)).To(Equal(0)) + Expect(len(request.MultipartForm.File)).To(Equal(1)) + + files, ok := request.MultipartForm.File["buildpack"] + Expect(ok).To(BeTrue(), "Buildpack file part not present") + Expect(len(files)).To(Equal(1), "Wrong number of files") + + buildpackFile := files[0] + file, err := buildpackFile.Open() + Expect(err).NotTo(HaveOccurred()) + + Expect(buildpackFile.Filename).To(ContainSubstring(".zip")) + + zipReader, err := zip.NewReader(file, 4096) + Expect(err).NotTo(HaveOccurred()) + + actualFileNames := []string{} + actualFileContents := []string{} + for _, f := range zipReader.File { + actualFileNames = append(actualFileNames, f.Name) + c, _ := f.Open() + content, _ := ioutil.ReadAll(c) + actualFileContents = append(actualFileContents, string(content)) + } + sort.Strings(actualFileNames) + + Expect(actualFileNames).To(Equal([]string{ + "bin/", + "bin/compile", + "bin/detect", + "bin/release", + "lib/", + "lib/helper", + })) + Expect(actualFileContents).To(Equal([]string{ + "", + "the-compile-script\n", + "the-detect-script\n", + "the-release-script\n", + "", + "the-helper-script\n", + })) + + if runtime.GOOS != "windows" { + for i := 1; i < 4; i++ { + Expect(zipReader.File[i].Mode()).To(Equal(os.FileMode(0755))) + } + } + }, + } +} diff --git a/cf/api/buildpacks.go b/cf/api/buildpacks.go new file mode 100644 index 00000000000..a3a3135cb43 --- /dev/null +++ b/cf/api/buildpacks.go @@ -0,0 +1,116 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type BuildpackRepository interface { + FindByName(name string) (buildpack models.Buildpack, apiErr error) + ListBuildpacks(func(models.Buildpack) bool) error + Create(name string, position *int, enabled *bool, locked *bool) (createdBuildpack models.Buildpack, apiErr error) + Delete(buildpackGuid string) (apiErr error) + Update(buildpack models.Buildpack) (updatedBuildpack models.Buildpack, apiErr error) +} + +type CloudControllerBuildpackRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerBuildpackRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerBuildpackRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerBuildpackRepository) ListBuildpacks(cb func(models.Buildpack) bool) error { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + buildpacks_path, + resources.BuildpackResource{}, + func(resource interface{}) bool { + return cb(resource.(resources.BuildpackResource).ToFields()) + }) +} + +func (repo CloudControllerBuildpackRepository) FindByName(name string) (buildpack models.Buildpack, apiErr error) { + foundIt := false + apiErr = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("%s?q=%s", buildpacks_path, url.QueryEscape("name:"+name)), + resources.BuildpackResource{}, + func(resource interface{}) bool { + buildpack = resource.(resources.BuildpackResource).ToFields() + foundIt = true + return false + }) + + if !foundIt { + apiErr = errors.NewModelNotFoundError("Buildpack", name) + } + return +} + +func (repo CloudControllerBuildpackRepository) Create(name string, position *int, enabled *bool, locked *bool) (createdBuildpack models.Buildpack, apiErr error) { + entity := resources.BuildpackEntity{Name: name, Position: position, Enabled: enabled, Locked: locked} + body, err := json.Marshal(entity) + if err != nil { + apiErr = errors.NewWithError(T("Could not serialize information"), err) + return + } + + resource := new(resources.BuildpackResource) + apiErr = repo.gateway.CreateResource(repo.config.ApiEndpoint(), buildpacks_path, bytes.NewReader(body), resource) + if apiErr != nil { + return + } + + createdBuildpack = resource.ToFields() + return +} + +func (repo CloudControllerBuildpackRepository) Delete(buildpackGuid string) (apiErr error) { + path := fmt.Sprintf("%s/%s", buildpacks_path, buildpackGuid) + apiErr = repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) + return +} + +func (repo CloudControllerBuildpackRepository) Update(buildpack models.Buildpack) (updatedBuildpack models.Buildpack, apiErr error) { + path := fmt.Sprintf("%s/%s", buildpacks_path, buildpack.Guid) + + entity := resources.BuildpackEntity{ + Name: buildpack.Name, + Position: buildpack.Position, + Enabled: buildpack.Enabled, + Key: "", + Filename: "", + Locked: buildpack.Locked, + } + + body, err := json.Marshal(entity) + if err != nil { + apiErr = errors.NewWithError(T("Could not serialize updates."), err) + return + } + + resource := new(resources.BuildpackResource) + apiErr = repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, bytes.NewReader(body), resource) + if apiErr != nil { + return + } + + updatedBuildpack = resource.ToFields() + return +} + +const buildpacks_path = "/v2/buildpacks" diff --git a/cf/api/buildpacks_test.go b/cf/api/buildpacks_test.go new file mode 100644 index 00000000000..f583d680b47 --- /dev/null +++ b/cf/api/buildpacks_test.go @@ -0,0 +1,334 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Buildpacks repo", func() { + var ( + ts *httptest.Server + handler *testnet.TestHandler + config core_config.ReadWriter + repo BuildpackRepository + ) + + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway((config), time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerBuildpackRepository(config, gateway) + }) + + AfterEach(func() { + ts.Close() + }) + + var setupTestServer = func(requests ...testnet.TestRequest) { + ts, handler = testnet.NewServer(requests) + config.SetApiEndpoint(ts.URL) + } + + It("lists buildpacks", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/buildpacks", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/buildpacks?page=2", + "resources": [ + { + "metadata": { + "guid": "buildpack1-guid" + }, + "entity": { + "name": "Buildpack1", + "position" : 1, + "filename" : "firstbp.zip" + } + } + ] + }`}}), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/buildpacks?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "resources": [ + { + "metadata": { + "guid": "buildpack2-guid" + }, + "entity": { + "name": "Buildpack2", + "position" : 2 + } + } + ] + }`}, + })) + + buildpacks := []models.Buildpack{} + err := repo.ListBuildpacks(func(b models.Buildpack) bool { + buildpacks = append(buildpacks, b) + return true + }) + + one := 1 + two := 2 + Expect(buildpacks).To(ConsistOf([]models.Buildpack{ + { + Guid: "buildpack1-guid", + Name: "Buildpack1", + Position: &one, + Filename: "firstbp.zip", + }, + { + Guid: "buildpack2-guid", + Name: "Buildpack2", + Position: &two, + }, + })) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + Describe("finding buildpacks by name", func() { + It("returns the buildpack with that name", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/buildpacks?q=name%3ABuildpack1", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": [ + { + "metadata": { + "guid": "buildpack1-guid" + }, + "entity": { + "name": "Buildpack1", + "position": 10 + } + } + ] + }`}})) + + buildpack, apiErr := repo.FindByName("Buildpack1") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(buildpack.Name).To(Equal("Buildpack1")) + Expect(buildpack.Guid).To(Equal("buildpack1-guid")) + Expect(*buildpack.Position).To(Equal(10)) + }) + + It("returns a ModelNotFoundError when the buildpack is not found", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/buildpacks?q=name%3ABuildpack1", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": []}`, + }, + })) + + _, apiErr := repo.FindByName("Buildpack1") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe("creating buildpacks", func() { + It("returns an error when the buildpack has an invalid name", func() { + setupTestServer(testnet.TestRequest{ + Method: "POST", + Path: "/v2/buildpacks", + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{ + "code":290003, + "description":"Buildpack is invalid: [\"name name can only contain alphanumeric characters\"]", + "error_code":"CF-BuildpackInvalid" + }`, + }}) + + one := 1 + createdBuildpack, apiErr := repo.Create("name with space", &one, nil, nil) + Expect(apiErr).To(HaveOccurred()) + Expect(createdBuildpack).To(Equal(models.Buildpack{})) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(Equal("290003")) + Expect(apiErr.Error()).To(ContainSubstring("Buildpack is invalid")) + }) + + It("sets the position flag when creating a buildpack", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/buildpacks", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","position":999}`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{ + "metadata": { + "guid": "my-cool-buildpack-guid" + }, + "entity": { + "name": "my-cool-buildpack", + "position":999 + } + }`}, + })) + + position := 999 + created, apiErr := repo.Create("my-cool-buildpack", &position, nil, nil) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(created.Guid).NotTo(BeNil()) + Expect("my-cool-buildpack").To(Equal(created.Name)) + Expect(999).To(Equal(*created.Position)) + }) + + It("sets the enabled flag when creating a buildpack", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/buildpacks", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","position":999, "enabled":true}`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{ + "metadata": { + "guid": "my-cool-buildpack-guid" + }, + "entity": { + "name": "my-cool-buildpack", + "position":999, + "enabled":true + } + }`}, + })) + + position := 999 + enabled := true + created, apiErr := repo.Create("my-cool-buildpack", &position, &enabled, nil) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(created.Guid).NotTo(BeNil()) + Expect(created.Name).To(Equal("my-cool-buildpack")) + Expect(999).To(Equal(*created.Position)) + }) + }) + + It("deletes buildpacks", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/buildpacks/my-cool-buildpack-guid", + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }})) + + err := repo.Delete("my-cool-buildpack-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + Describe("updating buildpacks", func() { + It("updates a buildpack's name, position and enabled flag", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/buildpacks/my-cool-buildpack-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","position":555,"enabled":false}`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{ + "metadata": { + "guid": "my-cool-buildpack-guid" + }, + "entity": { + "name": "my-cool-buildpack", + "position":555, + "enabled":false + } + }`}, + })) + + position := 555 + enabled := false + buildpack := models.Buildpack{ + Name: "my-cool-buildpack", + Guid: "my-cool-buildpack-guid", + Position: &position, + Enabled: &enabled, + } + + updated, err := repo.Update(buildpack) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(updated).To(Equal(buildpack)) + }) + + It("sets the locked attribute on the buildpack", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/buildpacks/my-cool-buildpack-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","locked":true}`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{ + + "metadata": { + "guid": "my-cool-buildpack-guid" + }, + "entity": { + "name": "my-cool-buildpack", + "position":123, + "locked": true + } + }`}, + })) + + locked := true + + buildpack := models.Buildpack{ + Name: "my-cool-buildpack", + Guid: "my-cool-buildpack-guid", + Locked: &locked, + } + + updated, err := repo.Update(buildpack) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + position := 123 + Expect(updated).To(Equal(models.Buildpack{ + Name: "my-cool-buildpack", + Guid: "my-cool-buildpack-guid", + Position: &position, + Locked: &locked, + })) + }) + }) +}) diff --git a/cf/api/copy_application_source/copy_application_source.go b/cf/api/copy_application_source/copy_application_source.go new file mode 100644 index 00000000000..dd30462fc26 --- /dev/null +++ b/cf/api/copy_application_source/copy_application_source.go @@ -0,0 +1,31 @@ +package copy_application_source + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" +) + +type CopyApplicationSourceRepository interface { + CopyApplication(sourceAppGuid, targetAppGuid string) error +} + +type CloudControllerApplicationSourceRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerCopyApplicationSourceRepository(config core_config.Reader, gateway net.Gateway) *CloudControllerApplicationSourceRepository { + return &CloudControllerApplicationSourceRepository{ + config: config, + gateway: gateway, + } +} + +func (repo *CloudControllerApplicationSourceRepository) CopyApplication(sourceAppGuid, targetAppGuid string) error { + url := fmt.Sprintf("/v2/apps/%s/copy_bits", targetAppGuid) + body := fmt.Sprintf(`{"source_app_guid":"%s"}`, sourceAppGuid) + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), url, strings.NewReader(body), new(interface{})) +} diff --git a/cf/api/copy_application_source/copy_application_source_suite_test.go b/cf/api/copy_application_source/copy_application_source_suite_test.go new file mode 100644 index 00000000000..63c903ecf0c --- /dev/null +++ b/cf/api/copy_application_source/copy_application_source_suite_test.go @@ -0,0 +1,19 @@ +package copy_application_source_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCopyApplicationSource(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "CopyApplicationSource Suite") +} diff --git a/cf/api/copy_application_source/copy_application_source_test.go b/cf/api/copy_application_source/copy_application_source_test.go new file mode 100644 index 00000000000..b6b46ec3e55 --- /dev/null +++ b/cf/api/copy_application_source/copy_application_source_test.go @@ -0,0 +1,62 @@ +package copy_application_source_test + +import ( + "net/http" + "net/http/httptest" + "time" + + . "github.com/cloudfoundry/cli/cf/api/copy_application_source" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CopyApplicationSource", func() { + var ( + repo CopyApplicationSourceRepository + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerCopyApplicationSourceRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe(".CopyApplication", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/apps/target-app-guid/copy_bits", + Matcher: testnet.RequestBodyMatcher(`{ + "source_app_guid": "source-app-guid" + }`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + }, + })) + }) + + It("should return a CopyApplicationModel", func() { + err := repo.CopyApplication("source-app-guid", "target-app-guid") + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/cf/api/copy_application_source/fakes/fake_copy_application_source_repository.go b/cf/api/copy_application_source/fakes/fake_copy_application_source_repository.go new file mode 100644 index 00000000000..057eef998e3 --- /dev/null +++ b/cf/api/copy_application_source/fakes/fake_copy_application_source_repository.go @@ -0,0 +1,54 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/copy_application_source" + + "sync" +) + +type FakeCopyApplicationSourceRepository struct { + CopyApplicationStub func(sourceAppGuid, targetAppGuid string) error + copyApplicationMutex sync.RWMutex + copyApplicationArgsForCall []struct { + sourceAppGuid string + targetAppGuid string + } + copyApplicationReturns struct { + result1 error + } +} + +func (fake *FakeCopyApplicationSourceRepository) CopyApplication(sourceAppGuid string, targetAppGuid string) error { + fake.copyApplicationMutex.Lock() + defer fake.copyApplicationMutex.Unlock() + fake.copyApplicationArgsForCall = append(fake.copyApplicationArgsForCall, struct { + sourceAppGuid string + targetAppGuid string + }{sourceAppGuid, targetAppGuid}) + if fake.CopyApplicationStub != nil { + return fake.CopyApplicationStub(sourceAppGuid, targetAppGuid) + } else { + return fake.copyApplicationReturns.result1 + } +} + +func (fake *FakeCopyApplicationSourceRepository) CopyApplicationCallCount() int { + fake.copyApplicationMutex.RLock() + defer fake.copyApplicationMutex.RUnlock() + return len(fake.copyApplicationArgsForCall) +} + +func (fake *FakeCopyApplicationSourceRepository) CopyApplicationArgsForCall(i int) (string, string) { + fake.copyApplicationMutex.RLock() + defer fake.copyApplicationMutex.RUnlock() + return fake.copyApplicationArgsForCall[i].sourceAppGuid, fake.copyApplicationArgsForCall[i].targetAppGuid +} + +func (fake *FakeCopyApplicationSourceRepository) CopyApplicationReturns(result1 error) { + fake.copyApplicationReturns = struct { + result1 error + }{result1} +} + +var _ CopyApplicationSourceRepository = new(FakeCopyApplicationSourceRepository) diff --git a/cf/api/curl.go b/cf/api/curl.go new file mode 100644 index 00000000000..72d1c5f6031 --- /dev/null +++ b/cf/api/curl.go @@ -0,0 +1,87 @@ +package api + +import ( + "bufio" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/textproto" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/net" +) + +type CurlRepository interface { + Request(method, path, header, body string) (resHeaders, resBody string, apiErr error) +} + +type CloudControllerCurlRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerCurlRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerCurlRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerCurlRepository) Request(method, path, headerString, body string) (resHeaders, resBody string, err error) { + url := fmt.Sprintf("%s/%s", repo.config.ApiEndpoint(), strings.TrimLeft(path, "/")) + + req, err := repo.gateway.NewRequest(method, url, repo.config.AccessToken(), strings.NewReader(body)) + if err != nil { + return + } + + err = mergeHeaders(req.HttpReq.Header, headerString) + if err != nil { + err = errors.NewWithError(T("Error parsing headers"), err) + return + } + + res, err := repo.gateway.PerformRequest(req) + + if _, ok := err.(errors.HttpError); ok { + err = nil + } + + if err != nil { + return + } + defer res.Body.Close() + + headerBytes, _ := httputil.DumpResponse(res, false) + resHeaders = string(headerBytes) + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + err = errors.NewWithError(T("Error reading response"), err) + } + resBody = string(bytes) + + return +} + +func mergeHeaders(destination http.Header, headerString string) (err error) { + headerString = strings.TrimSpace(headerString) + headerString += "\n\n" + headerReader := bufio.NewReader(strings.NewReader(headerString)) + headers, err := textproto.NewReader(headerReader).ReadMIMEHeader() + if err != nil { + return + } + + for key, values := range headers { + destination.Del(key) + for _, value := range values { + destination.Add(key, value) + } + } + + return +} diff --git a/cf/api/curl_test.go b/cf/api/curl_test.go new file mode 100644 index 00000000000..c631b48e584 --- /dev/null +++ b/cf/api/curl_test.go @@ -0,0 +1,179 @@ +package api_test + +import ( + "net/http" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerCurlRepository ", func() { + var ( + headers string + body string + apiErr error + ) + + Describe("GET requests", func() { + BeforeEach(func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/endpoint", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: expectedJSONResponse}, + }) + ts, handler := testnet.NewServer([]testnet.TestRequest{req}) + defer ts.Close() + + deps := newCurlDependencies() + deps.config.SetApiEndpoint(ts.URL) + + repo := NewCloudControllerCurlRepository(deps.config, deps.gateway) + headers, body, apiErr = repo.Request("GET", "/v2/endpoint", "", "") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("returns headers with the status code", func() { + Expect(headers).To(ContainSubstring("200")) + }) + + It("returns the header content type", func() { + Expect(headers).To(ContainSubstring("Content-Type")) + Expect(headers).To(ContainSubstring("text/plain")) + }) + + It("returns the body as a JSON-encoded string", func() { + testassert.JSONStringEquals(body, expectedJSONResponse) + }) + }) + + Describe("POST requests", func() { + BeforeEach(func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/endpoint", + Matcher: testnet.RequestBodyMatcher(`{"key":"val"}`), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: expectedJSONResponse}, + }) + + ts, handler := testnet.NewServer([]testnet.TestRequest{req}) + defer ts.Close() + + deps := newCurlDependencies() + deps.config.SetApiEndpoint(ts.URL) + + repo := NewCloudControllerCurlRepository(deps.config, deps.gateway) + headers, body, apiErr = repo.Request("POST", "/v2/endpoint", "", `{"key":"val"}`) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + It("does not return an error", func() { + Expect(apiErr).NotTo(HaveOccurred()) + }) + + Context("when the server returns a 400 Bad Request header", func() { + BeforeEach(func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/endpoint", + Matcher: testnet.RequestBodyMatcher(`{"key":"val"}`), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: expectedJSONResponse}, + }) + + ts, handler := testnet.NewServer([]testnet.TestRequest{req}) + defer ts.Close() + + deps := newCurlDependencies() + deps.config.SetApiEndpoint(ts.URL) + + repo := NewCloudControllerCurlRepository(deps.config, deps.gateway) + _, body, apiErr = repo.Request("POST", "/v2/endpoint", "", `{"key":"val"}`) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + It("returns the response body", func() { + testassert.JSONStringEquals(body, expectedJSONResponse) + }) + + It("does not return an error", func() { + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Context("when provided with invalid headers", func() { + It("returns an error", func() { + deps := newCurlDependencies() + repo := NewCloudControllerCurlRepository(deps.config, deps.gateway) + _, _, apiErr := repo.Request("POST", "/v2/endpoint", "not-valid", "") + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.Error()).To(ContainSubstring("headers")) + }) + }) + + Context("when provided with valid headers", func() { + It("sends them along with the POST body", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/endpoint", + Matcher: func(req *http.Request) { + Expect(req.Header.Get("content-type")).To(Equal("ascii/cats")) + Expect(req.Header.Get("x-something-else")).To(Equal("5")) + }, + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: expectedJSONResponse}, + }) + ts, handler := testnet.NewServer([]testnet.TestRequest{req}) + defer ts.Close() + + deps := newCurlDependencies() + deps.config.SetApiEndpoint(ts.URL) + + headers := "content-type: ascii/cats\nx-something-else:5" + repo := NewCloudControllerCurlRepository(deps.config, deps.gateway) + _, _, apiErr := repo.Request("POST", "/v2/endpoint", headers, "") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + }) +}) + +const expectedJSONResponse = ` + {"resources": [ + { + "metadata": { "guid": "my-quota-guid" }, + "entity": { "name": "my-remote-quota", "memory_limit": 1024 } + } + ]} +` + +type curlDependencies struct { + config core_config.ReadWriter + gateway net.Gateway +} + +func newCurlDependencies() (deps curlDependencies) { + deps.config = testconfig.NewRepository() + deps.config.SetAccessToken("BEARER my_access_token") + deps.gateway = net.NewCloudControllerGateway(deps.config, time.Now, &testterm.FakeUI{}) + return +} diff --git a/cf/api/domains.go b/cf/api/domains.go new file mode 100644 index 00000000000..3e8aa5431ef --- /dev/null +++ b/cf/api/domains.go @@ -0,0 +1,175 @@ +package api + +import ( + "encoding/json" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/api/strategy" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type DomainRepository interface { + ListDomainsForOrg(orgGuid string, cb func(models.DomainFields) bool) error + FindByName(name string) (domain models.DomainFields, apiErr error) + FindByNameInOrg(name string, owningOrgGuid string) (domain models.DomainFields, apiErr error) + Create(domainName string, owningOrgGuid string) (createdDomain models.DomainFields, apiErr error) + CreateSharedDomain(domainName string) (apiErr error) + Delete(domainGuid string) (apiErr error) + DeleteSharedDomain(domainGuid string) (apiErr error) + FirstOrDefault(orgGuid string, name *string) (domain models.DomainFields, error error) +} + +type CloudControllerDomainRepository struct { + config core_config.Reader + gateway net.Gateway + strategy strategy.EndpointStrategy +} + +func NewCloudControllerDomainRepository(config core_config.Reader, gateway net.Gateway, strategy strategy.EndpointStrategy) CloudControllerDomainRepository { + return CloudControllerDomainRepository{ + config: config, + gateway: gateway, + strategy: strategy, + } +} + +func (repo CloudControllerDomainRepository) ListDomainsForOrg(orgGuid string, cb func(models.DomainFields) bool) error { + err := repo.listDomains(repo.strategy.PrivateDomainsByOrgURL(orgGuid), cb) + if err != nil { + return err + } + err = repo.listDomains(repo.strategy.SharedDomainsURL(), cb) + return err +} + +func (repo CloudControllerDomainRepository) listDomains(path string, cb func(models.DomainFields) bool) (apiErr error) { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.DomainResource{}, + func(resource interface{}) bool { + return cb(resource.(resources.DomainResource).ToFields()) + }) +} + +func (repo CloudControllerDomainRepository) isOrgDomain(orgGuid string, domain models.DomainFields) bool { + return orgGuid == domain.OwningOrganizationGuid || domain.Shared +} + +func (repo CloudControllerDomainRepository) FindByName(name string) (domain models.DomainFields, apiErr error) { + return repo.findOneWithPath(repo.strategy.DomainURL(name), name) +} + +func (repo CloudControllerDomainRepository) FindByNameInOrg(name string, orgGuid string) (domain models.DomainFields, apiErr error) { + domain, apiErr = repo.findOneWithPath(repo.strategy.OrgDomainURL(orgGuid, name), name) + + switch apiErr.(type) { + case *errors.ModelNotFoundError: + domain, apiErr = repo.FindByName(name) + if !domain.Shared { + apiErr = errors.NewModelNotFoundError("Domain", name) + } + } + + return +} + +func (repo CloudControllerDomainRepository) findOneWithPath(path, name string) (domain models.DomainFields, apiErr error) { + foundDomain := false + apiErr = repo.listDomains(path, func(result models.DomainFields) bool { + domain = result + foundDomain = true + return false + }) + + if apiErr == nil && !foundDomain { + apiErr = errors.NewModelNotFoundError("Domain", name) + } + + return +} + +func (repo CloudControllerDomainRepository) Create(domainName string, owningOrgGuid string) (createdDomain models.DomainFields, err error) { + data, err := json.Marshal(resources.DomainEntity{ + Name: domainName, + OwningOrganizationGuid: owningOrgGuid, + Wildcard: true, + }) + + if err != nil { + return + } + + resource := new(resources.DomainResource) + err = repo.gateway.CreateResource( + repo.config.ApiEndpoint(), + repo.strategy.PrivateDomainsURL(), + strings.NewReader(string(data)), + resource) + + if err != nil { + return + } + + createdDomain = resource.ToFields() + return +} + +func (repo CloudControllerDomainRepository) CreateSharedDomain(domainName string) (apiErr error) { + data, err := json.Marshal(resources.DomainEntity{ + Name: domainName, + Wildcard: true, + }) + + if err != nil { + return + } + + apiErr = repo.gateway.CreateResource( + repo.config.ApiEndpoint(), + repo.strategy.SharedDomainsURL(), + strings.NewReader(string(data))) + + return +} + +func (repo CloudControllerDomainRepository) Delete(domainGuid string) error { + return repo.gateway.DeleteResource( + repo.config.ApiEndpoint(), + repo.strategy.DeleteDomainURL(domainGuid)) +} + +func (repo CloudControllerDomainRepository) DeleteSharedDomain(domainGuid string) error { + return repo.gateway.DeleteResource( + repo.config.ApiEndpoint(), + repo.strategy.DeleteSharedDomainURL(domainGuid)) +} + +func (repo CloudControllerDomainRepository) FirstOrDefault(orgGuid string, name *string) (domain models.DomainFields, error error) { + if name == nil { + domain, error = repo.defaultDomain(orgGuid) + } else { + domain, error = repo.FindByNameInOrg(*name, orgGuid) + } + return +} + +func (repo CloudControllerDomainRepository) defaultDomain(orgGuid string) (models.DomainFields, error) { + var foundDomain *models.DomainFields + repo.ListDomainsForOrg(orgGuid, func(domain models.DomainFields) bool { + foundDomain = &domain + return !domain.Shared + }) + + if foundDomain == nil { + return models.DomainFields{}, errors.New(T("Could not find a default domain")) + } + + return *foundDomain, nil +} diff --git a/cf/api/domains_test.go b/cf/api/domains_test.go new file mode 100644 index 00000000000..83fd82a7a68 --- /dev/null +++ b/cf/api/domains_test.go @@ -0,0 +1,575 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/strategy" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DomainRepository", func() { + var ( + ts *httptest.Server + handler *testnet.TestHandler + repo DomainRepository + config core_config.ReadWriter + ) + + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + }) + + JustBeforeEach(func() { + gateway := net.NewCloudControllerGateway((config), time.Now, &testterm.FakeUI{}) + strategy := strategy.NewEndpointStrategy(config.ApiVersion()) + repo = NewCloudControllerDomainRepository(config, gateway, strategy) + }) + + AfterEach(func() { + ts.Close() + }) + + var setupTestServer = func(reqs ...testnet.TestRequest) { + ts, handler = testnet.NewServer(reqs) + config.SetApiEndpoint(ts.URL) + } + + Describe("listing domains", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.0") + setupTestServer(firstPagePrivateDomainsRequest, secondPagePrivateDomainsRequest, firstPageSharedDomainsRequest, secondPageSharedDomainsRequest) + }) + + It("uses the organization-scoped domains endpoints", func() { + receivedDomains := []models.DomainFields{} + apiErr := repo.ListDomainsForOrg("my-org-guid", func(d models.DomainFields) bool { + receivedDomains = append(receivedDomains, d) + return true + }) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(len(receivedDomains)).To(Equal(6)) + Expect(receivedDomains[0].Guid).To(Equal("domain1-guid")) + Expect(receivedDomains[1].Guid).To(Equal("domain2-guid")) + Expect(receivedDomains[2].Guid).To(Equal("domain3-guid")) + Expect(receivedDomains[3].Guid).To(Equal("shared-domain1-guid")) + Expect(receivedDomains[4].Guid).To(Equal("shared-domain2-guid")) + Expect(receivedDomains[5].Guid).To(Equal("shared-domain3-guid")) + Expect(handler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("getting default domain", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.0") + setupTestServer(firstPagePrivateDomainsRequest, secondPagePrivateDomainsRequest, firstPageSharedDomainsRequest, secondPageSharedDomainsRequest) + }) + + It("should always return back the first shared domain", func() { + domain, apiErr := repo.FirstOrDefault("my-org-guid", nil) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(domain.Guid).To(Equal("shared-domain1-guid")) + }) + }) + + It("finds a domain by name", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { "guid": "domain2-guid" }, + "entity": { "name": "domain2.cf-app.com" } + } + ] + }`}, + })) + + domain, apiErr := repo.FindByName("domain2.cf-app.com") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(domain.Name).To(Equal("domain2.cf-app.com")) + Expect(domain.Guid).To(Equal("domain2-guid")) + }) + + Describe("finding a domain by name in an org", func() { + It("looks in the org's domains first", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { "guid": "my-domain-guid" }, + "entity": { + "name": "my-example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] + }`}, + })) + + domain, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(domain.Name).To(Equal("my-example.com")) + Expect(domain.Guid).To(Equal("my-domain-guid")) + Expect(domain.Shared).To(BeFalse()) + }) + + It("looks for shared domains if no there are no org-specific domains", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + }), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { "guid": "shared-domain-guid" }, + "entity": { + "name": "shared-example.com", + "owning_organization_guid": null + } + } + ] + }`}, + })) + + domain, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(domain.Name).To(Equal("shared-example.com")) + Expect(domain.Guid).To(Equal("shared-domain-guid")) + Expect(domain.Shared).To(BeTrue()) + }) + + It("returns not found when neither endpoint returns a domain", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + }), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + })) + + _, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + + It("returns not found when the global endpoint returns a non-shared domain", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + }), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { "guid": "shared-domain-guid" }, + "entity": { + "name": "shared-example.com", + "owning_organization_guid": "some-other-org-guid" + } + } + ] + }`}})) + + _, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe("creating domains", func() { + Context("when the private domains endpoint is not available", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/domains", + Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","owning_organization_guid":"org-guid", "wildcard": true}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "abc-123" }, + "entity": { "name": "example.com" } + }`}, + }), + ) + }) + + It("uses the general domains endpoint", func() { + createdDomain, apiErr := repo.Create("example.com", "org-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(createdDomain.Guid).To(Equal("abc-123")) + }) + }) + + Context("when the private domains endpoint is available", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.1") + }) + + It("uses that endpoint", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/private_domains", + Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","owning_organization_guid":"org-guid", "wildcard": true}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "abc-123" }, + "entity": { "name": "example.com" } + }`}, + })) + + createdDomain, apiErr := repo.Create("example.com", "org-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(createdDomain.Guid).To(Equal("abc-123")) + }) + }) + }) + + Describe("creating shared domains", func() { + Context("targeting a newer cloud controller", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.0") + }) + + It("uses the shared domains endpoint", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/shared_domains", + Matcher: testnet.RequestBodyMatcher(`{"name":"example.com", "wildcard": true}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "abc-123" }, + "entity": { "name": "example.com" } + }`}}), + ) + + apiErr := repo.CreateSharedDomain("example.com") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Context("when targeting an older cloud controller", func() { + It("uses the general domains endpoint", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/domains", + Matcher: testnet.RequestBodyMatcher(`{"name":"example.com", "wildcard": true}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "abc-123" }, + "entity": { "name": "example.com" } + }`}, + }), + ) + + apiErr := repo.CreateSharedDomain("example.com") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("deleting domains", func() { + Context("when the private domains endpoint is available", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.0") + setupTestServer(deleteDomainReq(http.StatusOK)) + }) + + It("uses the private domains endpoint", func() { + apiErr := repo.Delete("my-domain-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Context("when the private domains endpoint is NOT available", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/domains/my-domain-guid?recursive=true", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + }) + + It("uses the general domains endpoint", func() { + apiErr := repo.Delete("my-domain-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("deleting shared domains", func() { + Context("when the shared domains endpoint is available", func() { + BeforeEach(func() { + config.SetApiVersion("2.2.0") + setupTestServer(deleteSharedDomainReq(http.StatusOK)) + }) + + It("uses the shared domains endpoint", func() { + apiErr := repo.DeleteSharedDomain("my-domain-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("returns an error when the delete fails", func() { + setupTestServer(deleteSharedDomainReq(http.StatusBadRequest)) + + apiErr := repo.DeleteSharedDomain("my-domain-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(BeNil()) + }) + }) + + Context("when the shared domains endpoint is not available", func() { + It("uses the old domains endpoint", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/domains/my-domain-guid?recursive=true", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + apiErr := repo.DeleteSharedDomain("my-domain-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + }) + +}) + +var oldEndpointDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/domains", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ + "resources": [ + { + "metadata": { + "guid": "domain-guid" + }, + "entity": { + "name": "example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] +}`}}) + +var firstPageDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/private_domains", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/organizations/my-org-guid/domains?page=2", + "resources": [ + { + "metadata": { + "guid": "domain1-guid", + }, + "entity": { + "name": "example.com", + "owning_organization_guid": "my-org-guid" + } + }, + { + "metadata": { + "guid": "domain2-guid" + }, + "entity": { + "name": "some-example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] +}`}, +}) + +var secondPageDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/domains?page=2", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "domain3-guid" + }, + "entity": { + "name": "example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] +}`}, +}) + +var firstPageSharedDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/shared_domains", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/shared_domains?page=2", + "resources": [ + { + "metadata": { + "guid": "shared-domain1-guid" + }, + "entity": { + "name": "sharedexample.com" + } + }, + { + "metadata": { + "guid": "shared-domain2-guid" + }, + "entity": { + "name": "some-other-shared-example.com" + } + } + ] +}`}, +}) + +var secondPageSharedDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/shared_domains?page=2", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "shared-domain3-guid" + }, + "entity": { + "name": "yet-another-shared-example.com" + } + } + ] +}`}, +}) + +var firstPagePrivateDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/private_domains", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/organizations/my-org-guid/private_domains?page=2", + "resources": [ + { + "metadata": { + "guid": "domain1-guid" + }, + "entity": { + "name": "example.com", + "owning_organization_guid": "my-org-guid" + } + }, + { + "metadata": { + "guid": "domain2-guid" + }, + "entity": { + "name": "some-example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] +}`}, +}) + +var secondPagePrivateDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/private_domains?page=2", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "domain3-guid" + }, + "entity": { + "name": "example.com", + "owning_organization_guid": "my-org-guid" + } + } + ] +}`}, +}) + +func deleteDomainReq(statusCode int) testnet.TestRequest { + return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/private_domains/my-domain-guid?recursive=true", + Response: testnet.TestResponse{Status: statusCode}, + }) +} + +func deleteSharedDomainReq(statusCode int) testnet.TestRequest { + return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/shared_domains/my-domain-guid?recursive=true", + Response: testnet.TestResponse{Status: statusCode}, + }) +} diff --git a/cf/api/endpoints.go b/cf/api/endpoints.go new file mode 100644 index 00000000000..68e0f1d87c0 --- /dev/null +++ b/cf/api/endpoints.go @@ -0,0 +1,97 @@ +package api + +import ( + "fmt" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/net" + "regexp" + "strings" +) + +type EndpointRepository interface { + UpdateEndpoint(endpoint string) (finalEndpoint string, apiErr error) +} + +type RemoteEndpointRepository struct { + config core_config.ReadWriter + gateway net.Gateway +} + +type endpointResource struct { + ApiVersion string `json:"api_version"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + LoggregatorEndpoint string `json:"logging_endpoint"` +} + +func NewEndpointRepository(config core_config.ReadWriter, gateway net.Gateway) (repo RemoteEndpointRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo RemoteEndpointRepository) UpdateEndpoint(endpoint string) (finalEndpoint string, apiErr error) { + defer func() { + if apiErr != nil { + repo.config.SetApiEndpoint("") + } + }() + + endpointMissingScheme := !strings.HasPrefix(endpoint, "https://") && !strings.HasPrefix(endpoint, "http://") + + if endpointMissingScheme { + finalEndpoint := "https://" + endpoint + apiErr := repo.attemptUpdate(finalEndpoint) + + switch apiErr.(type) { + case nil: + case *errors.InvalidSSLCert: + return endpoint, apiErr + default: + finalEndpoint = "http://" + endpoint + apiErr = repo.attemptUpdate(finalEndpoint) + } + + return finalEndpoint, apiErr + } else { + apiErr := repo.attemptUpdate(endpoint) + return endpoint, apiErr + } +} + +func (repo RemoteEndpointRepository) attemptUpdate(endpoint string) error { + serverResponse := new(endpointResource) + err := repo.gateway.GetResource(endpoint+"/v2/info", &serverResponse) + if err != nil { + return err + } + + if endpoint != repo.config.ApiEndpoint() { + repo.config.ClearSession() + } + + repo.config.SetApiEndpoint(endpoint) + repo.config.SetApiVersion(serverResponse.ApiVersion) + repo.config.SetAuthenticationEndpoint(serverResponse.AuthorizationEndpoint) + + if serverResponse.LoggregatorEndpoint == "" { + repo.config.SetLoggregatorEndpoint(defaultLoggregatorEndpoint(endpoint)) + } else { + repo.config.SetLoggregatorEndpoint(serverResponse.LoggregatorEndpoint) + } + + return nil +} + +// FIXME: needs semantic versioning +func defaultLoggregatorEndpoint(apiEndpoint string) string { + matches := endpointDomainRegex.FindStringSubmatch(apiEndpoint) + url := fmt.Sprintf("ws%s://loggregator.%s", matches[1], matches[2]) + if url[0:3] == "wss" { + return url + ":443" + } else { + return url + ":80" + } +} + +var endpointDomainRegex = regexp.MustCompile(`^http(s?)://[^\.]+\.([^:]+)`) diff --git a/cf/api/endpoints_test.go b/cf/api/endpoints_test.go new file mode 100644 index 00000000000..7898292c45e --- /dev/null +++ b/cf/api/endpoints_test.go @@ -0,0 +1,232 @@ +package api_test + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "time" + + . "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func validApiInfoEndpoint(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v2/info" { + w.WriteHeader(http.StatusNotFound) + return + } + + fmt.Fprintf(w, ` +{ + "name": "vcap", + "build": "2222", + "support": "http://support.cloudfoundry.com", + "version": 2, + "description": "Cloud Foundry sponsored by Pivotal", + "authorization_endpoint": "https://login.example.com", + "logging_endpoint": "wss://loggregator.foo.example.org:4443", + "api_version": "42.0.0" +}`) +} + +func apiInfoEndpointWithoutLogURL(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, "/v2/info") { + w.WriteHeader(http.StatusNotFound) + return + } + + fmt.Fprintln(w, ` +{ + "name": "vcap", + "build": "2222", + "support": "http://support.cloudfoundry.com", + "version": 2, + "description": "Cloud Foundry sponsored by Pivotal", + "authorization_endpoint": "https://login.example.com", + "api_version": "42.0.0" +}`) +} + +var _ = Describe("Endpoints Repository", func() { + var ( + config core_config.ReadWriter + gateway net.Gateway + testServer *httptest.Server + repo EndpointRepository + testServerFn func(w http.ResponseWriter, r *http.Request) + ) + + BeforeEach(func() { + config = testconfig.NewRepository() + testServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testServerFn(w, r) + })) + gateway = net.NewCloudControllerGateway((config), time.Now, &testterm.FakeUI{}) + gateway.SetTrustedCerts(testServer.TLS.Certificates) + repo = NewEndpointRepository(config, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("updating the endpoints", func() { + Context("when the API request is successful", func() { + var ( + org models.OrganizationFields + space models.SpaceFields + ) + BeforeEach(func() { + org.Name = "my-org" + org.Guid = "my-org-guid" + + space.Name = "my-space" + space.Guid = "my-space-guid" + + config.SetOrganizationFields(org) + config.SetSpaceFields(space) + testServerFn = validApiInfoEndpoint + }) + + It("stores the data from the /info api in the config", func() { + repo.UpdateEndpoint(testServer.URL) + + Expect(config.AccessToken()).To(Equal("")) + Expect(config.AuthenticationEndpoint()).To(Equal("https://login.example.com")) + Expect(config.LoggregatorEndpoint()).To(Equal("wss://loggregator.foo.example.org:4443")) + Expect(config.ApiEndpoint()).To(Equal(testServer.URL)) + Expect(config.ApiVersion()).To(Equal("42.0.0")) + Expect(config.HasOrganization()).To(BeFalse()) + Expect(config.HasSpace()).To(BeFalse()) + }) + + Context("when the api endpoint does not change", func() { + BeforeEach(func() { + config.SetApiEndpoint(testServer.URL) + config.SetAccessToken("some access token") + config.SetRefreshToken("some refresh token") + }) + + It("does not clear the session if the api endpoint does not change", func() { + repo.UpdateEndpoint(testServer.URL) + + Expect(config.OrganizationFields()).To(Equal(org)) + Expect(config.SpaceFields()).To(Equal(space)) + Expect(config.AccessToken()).To(Equal("some access token")) + Expect(config.RefreshToken()).To(Equal("some refresh token")) + }) + }) + }) + + Context("when the API request fails", func() { + ItClearsTheConfig := func() { + Expect(config.ApiEndpoint()).To(BeEmpty()) + } + + BeforeEach(func() { + config.SetApiEndpoint("example.com") + }) + + It("returns a failure response when the server has a bad certificate", func() { + testServer.TLS.Certificates = []tls.Certificate{testnet.MakeExpiredTLSCert()} + + _, apiErr := repo.UpdateEndpoint(testServer.URL) + Expect(apiErr).NotTo(BeNil()) + ItClearsTheConfig() + }) + + It("returns a failure response when the API request fails", func() { + testServerFn = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + } + + _, apiErr := repo.UpdateEndpoint(testServer.URL) + + Expect(apiErr).NotTo(BeNil()) + ItClearsTheConfig() + }) + + It("returns a failure response when the API returns invalid JSON", func() { + testServerFn = func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `Foo`) + } + + _, apiErr := repo.UpdateEndpoint(testServer.URL) + + Expect(apiErr).NotTo(BeNil()) + ItClearsTheConfig() + }) + }) + + Describe("when the specified API url doesn't have a scheme", func() { + It("uses https if possible", func() { + testServerFn = validApiInfoEndpoint + + schemelessURL := strings.Replace(testServer.URL, "https://", "", 1) + endpoint, apiErr := repo.UpdateEndpoint(schemelessURL) + Expect(endpoint).To(Equal("https://" + schemelessURL)) + + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(config.AccessToken()).To(Equal("")) + Expect(config.AuthenticationEndpoint()).To(Equal("https://login.example.com")) + Expect(config.ApiEndpoint()).To(Equal(testServer.URL)) + Expect(config.ApiVersion()).To(Equal("42.0.0")) + }) + + It("uses http when the server doesn't respond over https", func() { + testServer.Close() + testServer = httptest.NewServer(http.HandlerFunc(validApiInfoEndpoint)) + schemelessURL := strings.Replace(testServer.URL, "http://", "", 1) + + endpoint, apiErr := repo.UpdateEndpoint(schemelessURL) + + Expect(endpoint).To(Equal("http://" + schemelessURL)) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(config.AccessToken()).To(Equal("")) + Expect(config.AuthenticationEndpoint()).To(Equal("https://login.example.com")) + Expect(config.ApiEndpoint()).To(Equal(testServer.URL)) + Expect(config.ApiVersion()).To(Equal("42.0.0")) + }) + }) + + Describe("when the loggregator endpoint is not returned by the /info API (old CC)", func() { + BeforeEach(func() { + testServerFn = apiInfoEndpointWithoutLogURL + }) + + It("extrapolates the loggregator URL based on the API URL (SSL API)", func() { + _, err := repo.UpdateEndpoint(testServer.URL) + Expect(err).NotTo(HaveOccurred()) + Expect(config.LoggregatorEndpoint()).To(Equal("wss://loggregator.0.0.1:443")) + }) + + It("extrapolates the loggregator URL if there is a trailing slash", func() { + _, err := repo.UpdateEndpoint(testServer.URL + "/") + Expect(err).NotTo(HaveOccurred()) + Expect(config.LoggregatorEndpoint()).To(Equal("wss://loggregator.0.0.1:443")) + }) + + It("extrapolates the loggregator URL based on the API URL (non-SSL API)", func() { + testServer.Close() + testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testServerFn(w, r) + })) + + _, err := repo.UpdateEndpoint(testServer.URL) + Expect(err).NotTo(HaveOccurred()) + Expect(config.LoggregatorEndpoint()).To(Equal("ws://loggregator.0.0.1:80")) + }) + }) + }) +}) diff --git a/cf/api/environment_variable_groups/environment_variable_groups.go b/cf/api/environment_variable_groups/environment_variable_groups.go new file mode 100644 index 00000000000..53bb174b297 --- /dev/null +++ b/cf/api/environment_variable_groups/environment_variable_groups.go @@ -0,0 +1,94 @@ +package environment_variable_groups + +import ( + "errors" + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type EnvironmentVariableGroupsRepository interface { + ListRunning() (variables []models.EnvironmentVariable, apiErr error) + ListStaging() (variables []models.EnvironmentVariable, apiErr error) + SetStaging(string) error + SetRunning(string) error +} + +type CloudControllerEnvironmentVariableGroupsRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerEnvironmentVariableGroupsRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerEnvironmentVariableGroupsRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) ListRunning() (variables []models.EnvironmentVariable, apiErr error) { + var raw_response interface{} + url := fmt.Sprintf("%s/v2/config/environment_variable_groups/running", repo.config.ApiEndpoint()) + apiErr = repo.gateway.GetResource(url, &raw_response) + if apiErr != nil { + return + } + + variables, err := repo.marshalToEnvironmentVariables(raw_response) + if err != nil { + return nil, err + } + + return variables, nil +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) ListStaging() (variables []models.EnvironmentVariable, apiErr error) { + var raw_response interface{} + url := fmt.Sprintf("%s/v2/config/environment_variable_groups/staging", repo.config.ApiEndpoint()) + apiErr = repo.gateway.GetResource(url, &raw_response) + if apiErr != nil { + return + } + + variables, err := repo.marshalToEnvironmentVariables(raw_response) + if err != nil { + return nil, err + } + + return variables, nil +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) SetStaging(staging_vars string) error { + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), "/v2/config/environment_variable_groups/staging", strings.NewReader(staging_vars)) +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) SetRunning(running_vars string) error { + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), "/v2/config/environment_variable_groups/running", strings.NewReader(running_vars)) +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) marshalToEnvironmentVariables(raw_response interface{}) ([]models.EnvironmentVariable, error) { + var variables []models.EnvironmentVariable + for key, value := range raw_response.(map[string]interface{}) { + stringvalue, err := repo.convertValueToString(value) + if err != nil { + return nil, err + } + variable := models.EnvironmentVariable{Name: key, Value: stringvalue} + variables = append(variables, variable) + } + return variables, nil +} + +func (repo CloudControllerEnvironmentVariableGroupsRepository) convertValueToString(value interface{}) (string, error) { + stringvalue, ok := value.(string) + if !ok { + floatvalue, ok := value.(float64) + if !ok { + return "", errors.New(fmt.Sprintf("Attempted to read environment variable value of unknown type: %#v", value)) + } + stringvalue = fmt.Sprintf("%d", int(floatvalue)) + } + return stringvalue, nil +} diff --git a/cf/api/environment_variable_groups/environment_variable_groups_suite_test.go b/cf/api/environment_variable_groups/environment_variable_groups_suite_test.go new file mode 100644 index 00000000000..fcbba175ce5 --- /dev/null +++ b/cf/api/environment_variable_groups/environment_variable_groups_suite_test.go @@ -0,0 +1,19 @@ +package environment_variable_groups_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestEnvironmentVariableGroups(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "EnvironmentVariableGroups Suite") +} diff --git a/cf/api/environment_variable_groups/environment_variable_groups_test.go b/cf/api/environment_variable_groups/environment_variable_groups_test.go new file mode 100644 index 00000000000..15c018629a8 --- /dev/null +++ b/cf/api/environment_variable_groups/environment_variable_groups_test.go @@ -0,0 +1,147 @@ +package environment_variable_groups_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerEnvironmentVariableGroupsRepository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerEnvironmentVariableGroupsRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerEnvironmentVariableGroupsRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe("ListRunning", func() { + BeforeEach(func() { + setupTestServer(listRunningRequest) + }) + + It("lists the environment variables in the running group", func() { + envVars, err := repo.ListRunning() + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + + Expect(envVars).To(ConsistOf([]models.EnvironmentVariable{ + models.EnvironmentVariable{Name: "abc", Value: "123"}, + models.EnvironmentVariable{Name: "do-re-mi", Value: "fa-sol-la-ti"}, + })) + }) + }) + + Describe("ListStaging", func() { + BeforeEach(func() { + setupTestServer(listStagingRequest) + }) + + It("lists the environment variables in the staging group", func() { + envVars, err := repo.ListStaging() + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(envVars).To(ConsistOf([]models.EnvironmentVariable{ + models.EnvironmentVariable{Name: "abc", Value: "123"}, + models.EnvironmentVariable{Name: "do-re-mi", Value: "fa-sol-la-ti"}, + })) + }) + }) + + Describe("SetStaging", func() { + BeforeEach(func() { + setupTestServer(setStagingRequest) + }) + + It("sets the environment variables in the staging group", func() { + err := repo.SetStaging(`{"abc": "one-two-three", "def": 456}`) + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("SetRunning", func() { + BeforeEach(func() { + setupTestServer(setRunningRequest) + }) + + It("sets the environment variables in the running group", func() { + err := repo.SetRunning(`{"abc": "one-two-three", "def": 456}`) + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) +}) + +var listRunningRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/environment_variable_groups/running", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "abc": 123, + "do-re-mi": "fa-sol-la-ti" +}`, + }, +}) + +var listStagingRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/environment_variable_groups/staging", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "abc": 123, + "do-re-mi": "fa-sol-la-ti" +}`, + }, +}) + +var setStagingRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/environment_variable_groups/staging", + Matcher: testnet.RequestBodyMatcher(`{ + "abc": "one-two-three", + "def": 456 + }`), +}) + +var setRunningRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/environment_variable_groups/running", + Matcher: testnet.RequestBodyMatcher(`{ + "abc": "one-two-three", + "def": 456 + }`), +}) diff --git a/cf/api/environment_variable_groups/fakes/fake_environment_variable_groups_repository.go b/cf/api/environment_variable_groups/fakes/fake_environment_variable_groups_repository.go new file mode 100644 index 00000000000..d75c91e17de --- /dev/null +++ b/cf/api/environment_variable_groups/fakes/fake_environment_variable_groups_repository.go @@ -0,0 +1,154 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeEnvironmentVariableGroupsRepository struct { + ListRunningStub func() (variables []models.EnvironmentVariable, apiErr error) + listRunningMutex sync.RWMutex + listRunningArgsForCall []struct{} + listRunningReturns struct { + result1 []models.EnvironmentVariable + result2 error + } + ListStagingStub func() (variables []models.EnvironmentVariable, apiErr error) + listStagingMutex sync.RWMutex + listStagingArgsForCall []struct{} + listStagingReturns struct { + result1 []models.EnvironmentVariable + result2 error + } + SetStagingStub func(string) error + setStagingMutex sync.RWMutex + setStagingArgsForCall []struct { + arg1 string + } + setStagingReturns struct { + result1 error + } + SetRunningStub func(string) error + setRunningMutex sync.RWMutex + setRunningArgsForCall []struct { + arg1 string + } + setRunningReturns struct { + result1 error + } +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListRunning() (variables []models.EnvironmentVariable, apiErr error) { + fake.listRunningMutex.Lock() + defer fake.listRunningMutex.Unlock() + fake.listRunningArgsForCall = append(fake.listRunningArgsForCall, struct{}{}) + if fake.ListRunningStub != nil { + return fake.ListRunningStub() + } else { + return fake.listRunningReturns.result1, fake.listRunningReturns.result2 + } +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListRunningCallCount() int { + fake.listRunningMutex.RLock() + defer fake.listRunningMutex.RUnlock() + return len(fake.listRunningArgsForCall) +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListRunningReturns(result1 []models.EnvironmentVariable, result2 error) { + fake.listRunningReturns = struct { + result1 []models.EnvironmentVariable + result2 error + }{result1, result2} +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListStaging() (variables []models.EnvironmentVariable, apiErr error) { + fake.listStagingMutex.Lock() + defer fake.listStagingMutex.Unlock() + fake.listStagingArgsForCall = append(fake.listStagingArgsForCall, struct{}{}) + if fake.ListStagingStub != nil { + return fake.ListStagingStub() + } else { + return fake.listStagingReturns.result1, fake.listStagingReturns.result2 + } +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListStagingCallCount() int { + fake.listStagingMutex.RLock() + defer fake.listStagingMutex.RUnlock() + return len(fake.listStagingArgsForCall) +} + +func (fake *FakeEnvironmentVariableGroupsRepository) ListStagingReturns(result1 []models.EnvironmentVariable, result2 error) { + fake.listStagingReturns = struct { + result1 []models.EnvironmentVariable + result2 error + }{result1, result2} +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetStaging(arg1 string) error { + fake.setStagingMutex.Lock() + defer fake.setStagingMutex.Unlock() + fake.setStagingArgsForCall = append(fake.setStagingArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetStagingStub != nil { + return fake.SetStagingStub(arg1) + } else { + return fake.setStagingReturns.result1 + } +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetStagingCallCount() int { + fake.setStagingMutex.RLock() + defer fake.setStagingMutex.RUnlock() + return len(fake.setStagingArgsForCall) +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetStagingArgsForCall(i int) string { + fake.setStagingMutex.RLock() + defer fake.setStagingMutex.RUnlock() + return fake.setStagingArgsForCall[i].arg1 +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetStagingReturns(result1 error) { + fake.setStagingReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetRunning(arg1 string) error { + fake.setRunningMutex.Lock() + defer fake.setRunningMutex.Unlock() + fake.setRunningArgsForCall = append(fake.setRunningArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetRunningStub != nil { + return fake.SetRunningStub(arg1) + } else { + return fake.setRunningReturns.result1 + } +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetRunningCallCount() int { + fake.setRunningMutex.RLock() + defer fake.setRunningMutex.RUnlock() + return len(fake.setRunningArgsForCall) +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetRunningArgsForCall(i int) string { + fake.setRunningMutex.RLock() + defer fake.setRunningMutex.RUnlock() + return fake.setRunningArgsForCall[i].arg1 +} + +func (fake *FakeEnvironmentVariableGroupsRepository) SetRunningReturns(result1 error) { + fake.setRunningReturns = struct { + result1 error + }{result1} +} + +var _ EnvironmentVariableGroupsRepository = new(FakeEnvironmentVariableGroupsRepository) diff --git a/cf/api/fakes/fake_app_summary_repo.go b/cf/api/fakes/fake_app_summary_repo.go new file mode 100644 index 00000000000..57c85c3e008 --- /dev/null +++ b/cf/api/fakes/fake_app_summary_repo.go @@ -0,0 +1,30 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeAppSummaryRepo struct { + GetSummariesInCurrentSpaceApps []models.Application + + GetSummaryErrorCode string + GetSummaryAppGuid string + GetSummarySummary models.Application +} + +func (repo *FakeAppSummaryRepo) GetSummariesInCurrentSpace() (apps []models.Application, apiErr error) { + apps = repo.GetSummariesInCurrentSpaceApps + return +} + +func (repo *FakeAppSummaryRepo) GetSummary(appGuid string) (summary models.Application, apiErr error) { + repo.GetSummaryAppGuid = appGuid + summary = repo.GetSummarySummary + + if repo.GetSummaryErrorCode != "" { + apiErr = errors.NewHttpError(400, repo.GetSummaryErrorCode, "Error") + } + + return +} diff --git a/cf/api/fakes/fake_auth_token_repo.go b/cf/api/fakes/fake_auth_token_repo.go new file mode 100644 index 00000000000..5b5c7ec0b4a --- /dev/null +++ b/cf/api/fakes/fake_auth_token_repo.go @@ -0,0 +1,46 @@ +package fakes + +import "github.com/cloudfoundry/cli/cf/models" + +type FakeAuthTokenRepo struct { + CreatedServiceAuthTokenFields models.ServiceAuthTokenFields + + FindAllAuthTokens []models.ServiceAuthTokenFields + + FindByLabelAndProviderLabel string + FindByLabelAndProviderProvider string + FindByLabelAndProviderServiceAuthTokenFields models.ServiceAuthTokenFields + FindByLabelAndProviderApiResponse error + + UpdatedServiceAuthTokenFields models.ServiceAuthTokenFields + + DeletedServiceAuthTokenFields models.ServiceAuthTokenFields +} + +func (repo *FakeAuthTokenRepo) Create(authToken models.ServiceAuthTokenFields) (apiErr error) { + repo.CreatedServiceAuthTokenFields = authToken + return +} + +func (repo *FakeAuthTokenRepo) FindAll() (authTokens []models.ServiceAuthTokenFields, apiErr error) { + authTokens = repo.FindAllAuthTokens + return +} +func (repo *FakeAuthTokenRepo) FindByLabelAndProvider(label, provider string) (authToken models.ServiceAuthTokenFields, apiErr error) { + repo.FindByLabelAndProviderLabel = label + repo.FindByLabelAndProviderProvider = provider + + authToken = repo.FindByLabelAndProviderServiceAuthTokenFields + apiErr = repo.FindByLabelAndProviderApiResponse + return +} + +func (repo *FakeAuthTokenRepo) Delete(authToken models.ServiceAuthTokenFields) (apiErr error) { + repo.DeletedServiceAuthTokenFields = authToken + return +} + +func (repo *FakeAuthTokenRepo) Update(authToken models.ServiceAuthTokenFields) (apiErr error) { + repo.UpdatedServiceAuthTokenFields = authToken + return +} diff --git a/cf/api/fakes/fake_authenticator.go b/cf/api/fakes/fake_authenticator.go new file mode 100644 index 00000000000..d8f792c0a12 --- /dev/null +++ b/cf/api/fakes/fake_authenticator.go @@ -0,0 +1,65 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" +) + +type FakeAuthenticationRepository struct { + Config core_config.ReadWriter + AuthenticateArgs struct { + Credentials []map[string]string + } + GetLoginPromptsWasCalled bool + GetLoginPromptsReturns struct { + Error error + Prompts map[string]core_config.AuthPrompt + } + + AuthError bool + AccessToken string + RefreshToken string + RefreshTokenCalled bool + RefreshTokenError error +} + +func (auth *FakeAuthenticationRepository) Authenticate(credentials map[string]string) (apiErr error) { + auth.AuthenticateArgs.Credentials = append(auth.AuthenticateArgs.Credentials, copyMap(credentials)) + + if auth.AuthError { + apiErr = errors.New("Error authenticating.") + return + } + + if auth.AccessToken == "" { + auth.AccessToken = "BEARER some_access_token" + } + + auth.Config.SetAccessToken(auth.AccessToken) + auth.Config.SetRefreshToken(auth.RefreshToken) + + return +} + +func (auth *FakeAuthenticationRepository) RefreshAuthToken() (string, error) { + auth.RefreshTokenCalled = true + if auth.RefreshTokenError == nil { + return auth.RefreshToken, nil + } + return "", auth.RefreshTokenError +} + +func (auth *FakeAuthenticationRepository) GetLoginPromptsAndSaveUAAServerURL() (prompts map[string]core_config.AuthPrompt, apiErr error) { + auth.GetLoginPromptsWasCalled = true + prompts = auth.GetLoginPromptsReturns.Prompts + apiErr = auth.GetLoginPromptsReturns.Error + return +} + +func copyMap(input map[string]string) map[string]string { + output := map[string]string{} + for key, val := range input { + output[key] = val + } + return output +} diff --git a/cf/api/fakes/fake_buildpack_bits_repo.go b/cf/api/fakes/fake_buildpack_bits_repo.go new file mode 100644 index 00000000000..58c83ece28e --- /dev/null +++ b/cf/api/fakes/fake_buildpack_bits_repo.go @@ -0,0 +1,21 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeBuildpackBitsRepository struct { + UploadBuildpackErr bool + UploadBuildpackApiResponse error + UploadBuildpackPath string +} + +func (repo *FakeBuildpackBitsRepository) UploadBuildpack(buildpack models.Buildpack, dir string) error { + if repo.UploadBuildpackErr { + return errors.New("Invalid buildpack") + } + + repo.UploadBuildpackPath = dir + return repo.UploadBuildpackApiResponse +} diff --git a/cf/api/fakes/fake_buildpack_repo.go b/cf/api/fakes/fake_buildpack_repo.go new file mode 100644 index 00000000000..cb04c760467 --- /dev/null +++ b/cf/api/fakes/fake_buildpack_repo.go @@ -0,0 +1,69 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeBuildpackRepository struct { + Buildpacks []models.Buildpack + + FindByNameNotFound bool + FindByNameName string + FindByNameBuildpack models.Buildpack + FindByNameApiResponse error + + CreateBuildpackExists bool + CreateBuildpack models.Buildpack + CreateApiResponse error + + DeleteBuildpackGuid string + DeleteApiResponse error + + UpdateBuildpackArgs struct { + Buildpack models.Buildpack + } + + UpdateBuildpackReturns struct { + Error error + } +} + +func (repo *FakeBuildpackRepository) ListBuildpacks(cb func(models.Buildpack) bool) error { + for _, b := range repo.Buildpacks { + cb(b) + } + return nil +} + +func (repo *FakeBuildpackRepository) FindByName(name string) (buildpack models.Buildpack, apiErr error) { + repo.FindByNameName = name + buildpack = repo.FindByNameBuildpack + + if repo.FindByNameNotFound { + apiErr = errors.NewModelNotFoundError("Buildpack", name) + } + + return +} + +func (repo *FakeBuildpackRepository) Create(name string, position *int, enabled *bool, locked *bool) (createdBuildpack models.Buildpack, apiErr error) { + if repo.CreateBuildpackExists { + return repo.CreateBuildpack, errors.NewHttpError(400, errors.BUILDPACK_EXISTS, "Buildpack already exists") + } + + repo.CreateBuildpack = models.Buildpack{Name: name, Position: position, Enabled: enabled, Locked: locked} + return repo.CreateBuildpack, repo.CreateApiResponse +} + +func (repo *FakeBuildpackRepository) Delete(buildpackGuid string) (apiErr error) { + repo.DeleteBuildpackGuid = buildpackGuid + apiErr = repo.DeleteApiResponse + return +} + +func (repo *FakeBuildpackRepository) Update(buildpack models.Buildpack) (updatedBuildpack models.Buildpack, apiErr error) { + repo.UpdateBuildpackArgs.Buildpack = buildpack + apiErr = repo.UpdateBuildpackReturns.Error + return +} diff --git a/cf/api/fakes/fake_cc_request.go b/cf/api/fakes/fake_cc_request.go new file mode 100644 index 00000000000..dac105bf517 --- /dev/null +++ b/cf/api/fakes/fake_cc_request.go @@ -0,0 +1,15 @@ +package fakes + +import ( + testnet "github.com/cloudfoundry/cli/testhelpers/net" + "net/http" +) + +func NewCloudControllerTestRequest(request testnet.TestRequest) testnet.TestRequest { + request.Header = http.Header{ + "accept": {"application/json"}, + "authorization": {"BEARER my_access_token"}, + } + + return request +} diff --git a/cf/api/fakes/fake_curl_repo.go b/cf/api/fakes/fake_curl_repo.go new file mode 100644 index 00000000000..17cbfa0eee2 --- /dev/null +++ b/cf/api/fakes/fake_curl_repo.go @@ -0,0 +1,23 @@ +package fakes + +type FakeCurlRepository struct { + Method string + Path string + Header string + Body string + ResponseHeader string + ResponseBody string + Error error +} + +func (repo *FakeCurlRepository) Request(method, path, header, body string) (resHeaders, resBody string, apiErr error) { + repo.Method = method + repo.Path = path + repo.Header = header + repo.Body = body + + resHeaders = repo.ResponseHeader + resBody = repo.ResponseBody + apiErr = repo.Error + return +} diff --git a/cf/api/fakes/fake_domain_repo.go b/cf/api/fakes/fake_domain_repo.go new file mode 100644 index 00000000000..a1c9882cfbd --- /dev/null +++ b/cf/api/fakes/fake_domain_repo.go @@ -0,0 +1,109 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeDomainRepository struct { + ListDomainsForOrgGuid string + ListDomainsForOrgDomains []models.DomainFields + ListDomainsForOrgApiResponse error + + FindByNameInOrgName string + FindByNameInOrgGuid string + FindByNameInOrgDomain models.DomainFields + FindByNameInOrgApiResponse error + + FindByNameName string + FindByNameDomain models.DomainFields + FindByNameNotFound bool + FindByNameErr bool + + CreateDomainName string + CreateDomainOwningOrgGuid string + + CreateSharedDomainName string + + DeleteDomainGuid string + DeleteApiResponse error + + DeleteSharedDomainGuid string + DeleteSharedApiResponse error +} + +func (repo *FakeDomainRepository) ListDomainsForOrg(orgGuid string, cb func(models.DomainFields) bool) error { + repo.ListDomainsForOrgGuid = orgGuid + for _, d := range repo.ListDomainsForOrgDomains { + cb(d) + } + return repo.ListDomainsForOrgApiResponse +} + +func (repo *FakeDomainRepository) FindByName(name string) (domain models.DomainFields, apiErr error) { + repo.FindByNameName = name + domain = repo.FindByNameDomain + + if repo.FindByNameNotFound { + apiErr = errors.NewModelNotFoundError("Domain", name) + } + if repo.FindByNameErr { + apiErr = errors.New("Error finding domain") + } + + return +} + +func (repo *FakeDomainRepository) FindByNameInOrg(name string, owningOrgGuid string) (domain models.DomainFields, apiErr error) { + repo.FindByNameInOrgName = name + repo.FindByNameInOrgGuid = owningOrgGuid + domain = repo.FindByNameInOrgDomain + apiErr = repo.FindByNameInOrgApiResponse + return +} + +func (repo *FakeDomainRepository) Create(domainName string, owningOrgGuid string) (createdDomain models.DomainFields, apiErr error) { + repo.CreateDomainName = domainName + repo.CreateDomainOwningOrgGuid = owningOrgGuid + return +} + +func (repo *FakeDomainRepository) CreateSharedDomain(domainName string) (apiErr error) { + repo.CreateSharedDomainName = domainName + return +} + +func (repo *FakeDomainRepository) Delete(domainGuid string) (apiErr error) { + repo.DeleteDomainGuid = domainGuid + apiErr = repo.DeleteApiResponse + return +} + +func (repo *FakeDomainRepository) DeleteSharedDomain(domainGuid string) (apiErr error) { + repo.DeleteSharedDomainGuid = domainGuid + apiErr = repo.DeleteSharedApiResponse + return +} + +func (repo *FakeDomainRepository) FirstOrDefault(orgGuid string, name *string) (domain models.DomainFields, error error) { + if name == nil { + domain, error = repo.defaultDomain(orgGuid) + } else { + domain, error = repo.FindByNameInOrg(*name, orgGuid) + } + return +} + +func (repo *FakeDomainRepository) defaultDomain(orgGuid string) (models.DomainFields, error) { + var foundDomain *models.DomainFields + repo.ListDomainsForOrg(orgGuid, func(domain models.DomainFields) bool { + foundDomain = &domain + return !domain.Shared + }) + + if foundDomain == nil { + return models.DomainFields{}, errors.New("Could not find a default domain") + } + + return *foundDomain, nil +} diff --git a/cf/api/fakes/fake_endpoint_repo.go b/cf/api/fakes/fake_endpoint_repo.go new file mode 100644 index 00000000000..8c50123b5b0 --- /dev/null +++ b/cf/api/fakes/fake_endpoint_repo.go @@ -0,0 +1,11 @@ +package fakes + +type FakeEndpointRepo struct { + UpdateEndpointReceived string + UpdateEndpointError error +} + +func (repo *FakeEndpointRepo) UpdateEndpoint(endpoint string) (string, error) { + repo.UpdateEndpointReceived = endpoint + return endpoint, repo.UpdateEndpointError +} diff --git a/cf/api/fakes/fake_loggregator_consumer.go b/cf/api/fakes/fake_loggregator_consumer.go new file mode 100644 index 00000000000..6337b015545 --- /dev/null +++ b/cf/api/fakes/fake_loggregator_consumer.go @@ -0,0 +1,68 @@ +package fakes + +import ( + "github.com/cloudfoundry/loggregator_consumer" + "github.com/cloudfoundry/loggregatorlib/logmessage" +) + +type FakeLoggregatorConsumer struct { + RecentCalledWith struct { + AppGuid string + AuthToken string + } + + RecentReturns struct { + Messages []*logmessage.LogMessage + Err []error + callIndex int + } + + TailFunc func(appGuid, token string) (<-chan *logmessage.LogMessage, error) + + IsClosed bool + + OnConnectCallback func() + + closeChan chan bool +} + +func NewFakeLoggregatorConsumer() *FakeLoggregatorConsumer { + return &FakeLoggregatorConsumer{ + closeChan: make(chan bool, 1), + } +} + +func (c *FakeLoggregatorConsumer) Recent(appGuid string, authToken string) ([]*logmessage.LogMessage, error) { + c.RecentCalledWith.AppGuid = appGuid + c.RecentCalledWith.AuthToken = authToken + + var err error + if c.RecentReturns.callIndex < len(c.RecentReturns.Err) { + err = c.RecentReturns.Err[c.RecentReturns.callIndex] + c.RecentReturns.callIndex++ + } + + return c.RecentReturns.Messages, err +} + +func (c *FakeLoggregatorConsumer) Close() error { + c.IsClosed = true + c.closeChan <- true + return nil +} + +func (c *FakeLoggregatorConsumer) SetOnConnectCallback(cb func()) { + c.OnConnectCallback = cb +} + +func (c *FakeLoggregatorConsumer) Tail(appGuid string, authToken string) (<-chan *logmessage.LogMessage, error) { + return c.TailFunc(appGuid, authToken) +} + +func (c *FakeLoggregatorConsumer) WaitForClose() { + <-c.closeChan +} + +func (c *FakeLoggregatorConsumer) SetDebugPrinter(debugPrinter loggregator_consumer.DebugPrinter) { + <-c.closeChan +} diff --git a/cf/api/fakes/fake_logs_repository.go b/cf/api/fakes/fake_logs_repository.go new file mode 100644 index 00000000000..748b5604be2 --- /dev/null +++ b/cf/api/fakes/fake_logs_repository.go @@ -0,0 +1,118 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/loggregatorlib/logmessage" +) + +type FakeLogsRepository struct { + RecentLogsForStub func(appGuid string) ([]*logmessage.LogMessage, error) + recentLogsForMutex sync.RWMutex + recentLogsForArgsForCall []struct { + appGuid string + } + recentLogsForReturns struct { + result1 []*logmessage.LogMessage + result2 error + } + TailLogsForStub func(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error + tailLogsForMutex sync.RWMutex + tailLogsForArgsForCall []struct { + appGuid string + onConnect func() + onMessage func(*logmessage.LogMessage) + } + tailLogsForReturns struct { + result1 error + } + CloseStub func() + closeMutex sync.RWMutex + closeArgsForCall []struct{} +} + +func (fake *FakeLogsRepository) RecentLogsFor(appGuid string) ([]*logmessage.LogMessage, error) { + fake.recentLogsForMutex.Lock() + defer fake.recentLogsForMutex.Unlock() + fake.recentLogsForArgsForCall = append(fake.recentLogsForArgsForCall, struct { + appGuid string + }{appGuid}) + if fake.RecentLogsForStub != nil { + return fake.RecentLogsForStub(appGuid) + } else { + return fake.recentLogsForReturns.result1, fake.recentLogsForReturns.result2 + } +} + +func (fake *FakeLogsRepository) RecentLogsForCallCount() int { + fake.recentLogsForMutex.RLock() + defer fake.recentLogsForMutex.RUnlock() + return len(fake.recentLogsForArgsForCall) +} + +func (fake *FakeLogsRepository) RecentLogsForArgsForCall(i int) string { + fake.recentLogsForMutex.RLock() + defer fake.recentLogsForMutex.RUnlock() + return fake.recentLogsForArgsForCall[i].appGuid +} + +func (fake *FakeLogsRepository) RecentLogsForReturns(result1 []*logmessage.LogMessage, result2 error) { + fake.RecentLogsForStub = nil + fake.recentLogsForReturns = struct { + result1 []*logmessage.LogMessage + result2 error + }{result1, result2} +} + +func (fake *FakeLogsRepository) TailLogsFor(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error { + fake.tailLogsForMutex.Lock() + defer fake.tailLogsForMutex.Unlock() + fake.tailLogsForArgsForCall = append(fake.tailLogsForArgsForCall, struct { + appGuid string + onConnect func() + onMessage func(*logmessage.LogMessage) + }{appGuid, onConnect, onMessage}) + if fake.TailLogsForStub != nil { + return fake.TailLogsForStub(appGuid, onConnect, onMessage) + } else { + return fake.tailLogsForReturns.result1 + } +} + +func (fake *FakeLogsRepository) TailLogsForCallCount() int { + fake.tailLogsForMutex.RLock() + defer fake.tailLogsForMutex.RUnlock() + return len(fake.tailLogsForArgsForCall) +} + +func (fake *FakeLogsRepository) TailLogsForArgsForCall(i int) (string, func(), func(*logmessage.LogMessage)) { + fake.tailLogsForMutex.RLock() + defer fake.tailLogsForMutex.RUnlock() + return fake.tailLogsForArgsForCall[i].appGuid, fake.tailLogsForArgsForCall[i].onConnect, fake.tailLogsForArgsForCall[i].onMessage +} + +func (fake *FakeLogsRepository) TailLogsForReturns(result1 error) { + fake.TailLogsForStub = nil + fake.tailLogsForReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeLogsRepository) Close() { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) + if fake.CloseStub != nil { + fake.CloseStub() + } +} + +func (fake *FakeLogsRepository) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +var _ api.LogsRepository = new(FakeLogsRepository) diff --git a/cf/api/fakes/fake_pwd_repo.go b/cf/api/fakes/fake_pwd_repo.go new file mode 100644 index 00000000000..f78886b631c --- /dev/null +++ b/cf/api/fakes/fake_pwd_repo.go @@ -0,0 +1,23 @@ +package fakes + +import "github.com/cloudfoundry/cli/cf/errors" + +type FakePasswordRepo struct { + Score string + ScoredPassword string + + UpdateUnauthorized bool + UpdateNewPassword string + UpdateOldPassword string +} + +func (repo *FakePasswordRepo) UpdatePassword(old string, new string) (apiErr error) { + repo.UpdateOldPassword = old + repo.UpdateNewPassword = new + + if repo.UpdateUnauthorized { + apiErr = errors.NewHttpError(401, "unauthorized", "Authorization Failed") + } + + return +} diff --git a/cf/api/fakes/fake_route_repo.go b/cf/api/fakes/fake_route_repo.go new file mode 100644 index 00000000000..5146479c0a3 --- /dev/null +++ b/cf/api/fakes/fake_route_repo.go @@ -0,0 +1,124 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeRouteRepository struct { + FindByHostAndDomainCalledWith struct { + Host string + Domain models.DomainFields + } + + FindByHostAndDomainReturns struct { + Route models.Route + Error error + } + + CreatedHost string + CreatedDomainGuid string + CreatedRoute models.Route + + CreateInSpaceHost string + CreateInSpaceDomainGuid string + CreateInSpaceSpaceGuid string + CreateInSpaceCreatedRoute models.Route + CreateInSpaceErr bool + + CheckIfExistsFound bool + CheckIfExistsError error + + BindErr error + BoundRouteGuid string + BoundAppGuid string + + UnboundRouteGuid string + UnboundAppGuid string + + ListErr bool + Routes []models.Route + + DeletedRouteGuids []string + DeleteErr error +} + +func (repo *FakeRouteRepository) ListRoutes(cb func(models.Route) bool) (apiErr error) { + if repo.ListErr { + return errors.New("WHOOPSIE") + } + + for _, route := range repo.Routes { + if !cb(route) { + break + } + } + return +} + +func (repo *FakeRouteRepository) FindByHostAndDomain(host string, domain models.DomainFields) (route models.Route, apiErr error) { + repo.FindByHostAndDomainCalledWith.Host = host + repo.FindByHostAndDomainCalledWith.Domain = domain + + if repo.FindByHostAndDomainReturns.Error != nil { + apiErr = repo.FindByHostAndDomainReturns.Error + } + + route = repo.FindByHostAndDomainReturns.Route + return +} + +func (repo *FakeRouteRepository) Create(host string, domain models.DomainFields) (createdRoute models.Route, apiErr error) { + repo.CreatedHost = host + repo.CreatedDomainGuid = domain.Guid + + createdRoute.Guid = host + "-route-guid" + createdRoute.Domain = domain + createdRoute.Host = host + + return +} + +func (repo *FakeRouteRepository) CheckIfExists(host string, domain models.DomainFields) (found bool, apiErr error) { + if repo.CheckIfExistsFound { + found = true + } else { + found = false + } + + if repo.CheckIfExistsError != nil { + apiErr = repo.CheckIfExistsError + } + return +} +func (repo *FakeRouteRepository) CreateInSpace(host, domainGuid, spaceGuid string) (createdRoute models.Route, apiErr error) { + repo.CreateInSpaceHost = host + repo.CreateInSpaceDomainGuid = domainGuid + repo.CreateInSpaceSpaceGuid = spaceGuid + + if repo.CreateInSpaceErr { + apiErr = errors.New("Error") + } else { + createdRoute = repo.CreateInSpaceCreatedRoute + } + + return +} + +func (repo *FakeRouteRepository) Bind(routeGuid, appGuid string) (apiErr error) { + repo.BoundRouteGuid = routeGuid + repo.BoundAppGuid = appGuid + return repo.BindErr +} + +func (repo *FakeRouteRepository) Unbind(routeGuid, appGuid string) (apiErr error) { + repo.UnboundRouteGuid = routeGuid + repo.UnboundAppGuid = appGuid + return +} + +func (repo *FakeRouteRepository) Delete(routeGuid string) (apiErr error) { + repo.DeletedRouteGuids = append(repo.DeletedRouteGuids, routeGuid) + apiErr = repo.DeleteErr + return +} diff --git a/cf/api/fakes/fake_service_binding_repo.go b/cf/api/fakes/fake_service_binding_repo.go new file mode 100644 index 00000000000..af9d3844e8f --- /dev/null +++ b/cf/api/fakes/fake_service_binding_repo.go @@ -0,0 +1,34 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeServiceBindingRepo struct { + CreateServiceInstanceGuid string + CreateApplicationGuid string + CreateErrorCode string + + DeleteServiceInstance models.ServiceInstance + DeleteApplicationGuid string + DeleteBindingNotFound bool +} + +func (repo *FakeServiceBindingRepo) Create(instanceGuid, appGuid string) (apiErr error) { + repo.CreateServiceInstanceGuid = instanceGuid + repo.CreateApplicationGuid = appGuid + + if repo.CreateErrorCode != "" { + apiErr = errors.NewHttpError(400, repo.CreateErrorCode, "Error binding service") + } + + return +} + +func (repo *FakeServiceBindingRepo) Delete(instance models.ServiceInstance, appGuid string) (found bool, apiErr error) { + repo.DeleteServiceInstance = instance + repo.DeleteApplicationGuid = appGuid + found = !repo.DeleteBindingNotFound + return +} diff --git a/cf/api/fakes/fake_service_broker_repo.go b/cf/api/fakes/fake_service_broker_repo.go new file mode 100644 index 00000000000..439c8e28c58 --- /dev/null +++ b/cf/api/fakes/fake_service_broker_repo.go @@ -0,0 +1,89 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeServiceBrokerRepo struct { + FindByNameName string + FindByNameServiceBroker models.ServiceBroker + FindByNameNotFound bool + + FindByGuidGuid string + FindByGuidServiceBroker models.ServiceBroker + FindByGuidNotFound bool + + CreateName string + CreateUrl string + CreateUsername string + CreatePassword string + + UpdatedServiceBroker models.ServiceBroker + RenamedServiceBrokerGuid string + RenamedServiceBrokerName string + DeletedServiceBrokerGuid string + + ServiceBrokers []models.ServiceBroker + ListErr bool +} + +func (repo *FakeServiceBrokerRepo) FindByName(name string) (serviceBroker models.ServiceBroker, apiErr error) { + repo.FindByNameName = name + serviceBroker = repo.FindByNameServiceBroker + + if repo.FindByNameNotFound { + apiErr = errors.NewModelNotFoundError("Service Broker", name) + } + + return +} + +func (repo *FakeServiceBrokerRepo) FindByGuid(guid string) (serviceBroker models.ServiceBroker, apiErr error) { + repo.FindByGuidGuid = guid + serviceBroker = repo.FindByGuidServiceBroker + + if repo.FindByGuidNotFound { + apiErr = errors.NewModelNotFoundError("Service Broker", guid) + } + + return +} + +func (repo *FakeServiceBrokerRepo) ListServiceBrokers(callback func(broker models.ServiceBroker) bool) error { + for _, broker := range repo.ServiceBrokers { + if !callback(broker) { + break + } + } + + if repo.ListErr { + return errors.New("Error finding service brokers") + } else { + return nil + } +} + +func (repo *FakeServiceBrokerRepo) Create(name, url, username, password string) (apiErr error) { + repo.CreateName = name + repo.CreateUrl = url + repo.CreateUsername = username + repo.CreatePassword = password + return +} + +func (repo *FakeServiceBrokerRepo) Update(serviceBroker models.ServiceBroker) (apiErr error) { + repo.UpdatedServiceBroker = serviceBroker + return +} + +func (repo *FakeServiceBrokerRepo) Rename(guid, name string) (apiErr error) { + repo.RenamedServiceBrokerGuid = guid + repo.RenamedServiceBrokerName = name + return +} + +func (repo *FakeServiceBrokerRepo) Delete(guid string) (apiErr error) { + repo.DeletedServiceBrokerGuid = guid + return +} diff --git a/cf/api/fakes/fake_service_plan_repo.go b/cf/api/fakes/fake_service_plan_repo.go new file mode 100644 index 00000000000..d78480ee80e --- /dev/null +++ b/cf/api/fakes/fake_service_plan_repo.go @@ -0,0 +1,96 @@ +package fakes + +import ( + "sort" + "strings" + "sync" + + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeServicePlanRepo struct { + SearchReturns map[string][]models.ServicePlanFields + SearchErr error + + UpdateStub func(models.ServicePlanFields, string, bool) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 models.ServicePlanFields + arg2 string + arg3 bool + } + updateReturns struct { + result1 error + } +} + +func (fake *FakeServicePlanRepo) Search(queryParams map[string]string) ([]models.ServicePlanFields, error) { + if fake.SearchErr != nil { + return nil, fake.SearchErr + } + + if queryParams == nil { + //return everything + var returnPlans []models.ServicePlanFields + for _, value := range fake.SearchReturns { + returnPlans = append(returnPlans, value...) + } + return returnPlans, nil + } + + searchKey := combineKeys(queryParams) + if fake.SearchReturns[searchKey] != nil { + return fake.SearchReturns[searchKey], nil + } + + return []models.ServicePlanFields{}, nil +} + +func combineKeys(mapToCombine map[string]string) string { + keys := []string{} + for key, _ := range mapToCombine { + keys = append(keys, key) + } + sort.Strings(keys) + + values := []string{} + for _, key := range keys { + values = append(values, mapToCombine[key]) + } + + return strings.Join(values, ":") +} + +func (fake *FakeServicePlanRepo) Update(arg1 models.ServicePlanFields, arg2 string, arg3 bool) error { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 models.ServicePlanFields + arg2 string + arg3 bool + }{arg1, arg2, arg3}) + if fake.UpdateStub != nil { + return fake.UpdateStub(arg1, arg2, arg3) + } else { + return fake.updateReturns.result1 + } +} + +func (fake *FakeServicePlanRepo) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeServicePlanRepo) UpdateArgsForCall(i int) (models.ServicePlanFields, string, bool) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return fake.updateArgsForCall[i].arg1, fake.updateArgsForCall[i].arg2, fake.updateArgsForCall[i].arg3 +} + +func (fake *FakeServicePlanRepo) UpdateReturns(result1 error) { + fake.UpdateStub = nil + fake.updateReturns = struct { + result1 error + }{result1} +} diff --git a/cf/api/fakes/fake_service_plan_visibility_repository.go b/cf/api/fakes/fake_service_plan_visibility_repository.go new file mode 100644 index 00000000000..8e594bae53b --- /dev/null +++ b/cf/api/fakes/fake_service_plan_visibility_repository.go @@ -0,0 +1,167 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api" + + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeServicePlanVisibilityRepository struct { + CreateStub func(string, string) error + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 string + arg2 string + } + createReturns struct { + result1 error + } + ListStub func() ([]models.ServicePlanVisibilityFields, error) + listMutex sync.RWMutex + listArgsForCall []struct{} + listReturns struct { + result1 []models.ServicePlanVisibilityFields + result2 error + } + DeleteStub func(string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 string + } + deleteReturns struct { + result1 error + } + SearchStub func(map[string]string) ([]models.ServicePlanVisibilityFields, error) + searchMutex sync.RWMutex + searchArgsForCall []struct { + arg1 map[string]string + } + searchReturns struct { + result1 []models.ServicePlanVisibilityFields + result2 error + } +} + +func (fake *FakeServicePlanVisibilityRepository) Create(arg1 string, arg2 string) error { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.CreateStub != nil { + return fake.CreateStub(arg1, arg2) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeServicePlanVisibilityRepository) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeServicePlanVisibilityRepository) CreateArgsForCall(i int) (string, string) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].arg1, fake.createArgsForCall[i].arg2 +} + +func (fake *FakeServicePlanVisibilityRepository) CreateReturns(result1 error) { + fake.createReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeServicePlanVisibilityRepository) List() ([]models.ServicePlanVisibilityFields, error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.listArgsForCall = append(fake.listArgsForCall, struct{}{}) + if fake.ListStub != nil { + return fake.ListStub() + } else { + return fake.listReturns.result1, fake.listReturns.result2 + } +} + +func (fake *FakeServicePlanVisibilityRepository) ListCallCount() int { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + return len(fake.listArgsForCall) +} + +func (fake *FakeServicePlanVisibilityRepository) ListReturns(result1 []models.ServicePlanVisibilityFields, result2 error) { + fake.listReturns = struct { + result1 []models.ServicePlanVisibilityFields + result2 error + }{result1, result2} +} + +func (fake *FakeServicePlanVisibilityRepository) Delete(arg1 string) error { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 string + }{arg1}) + if fake.DeleteStub != nil { + return fake.DeleteStub(arg1) + } else { + return fake.deleteReturns.result1 + } +} + +func (fake *FakeServicePlanVisibilityRepository) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeServicePlanVisibilityRepository) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].arg1 +} + +func (fake *FakeServicePlanVisibilityRepository) DeleteReturns(result1 error) { + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeServicePlanVisibilityRepository) Search(arg1 map[string]string) ([]models.ServicePlanVisibilityFields, error) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.searchArgsForCall = append(fake.searchArgsForCall, struct { + arg1 map[string]string + }{arg1}) + if fake.SearchStub != nil { + return fake.SearchStub(arg1) + } else { + return fake.searchReturns.result1, fake.searchReturns.result2 + } +} + +func (fake *FakeServicePlanVisibilityRepository) SearchCallCount() int { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + return len(fake.searchArgsForCall) +} + +func (fake *FakeServicePlanVisibilityRepository) SearchArgsForCall(i int) map[string]string { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + return fake.searchArgsForCall[i].arg1 +} + +func (fake *FakeServicePlanVisibilityRepository) SearchReturns(result1 []models.ServicePlanVisibilityFields, result2 error) { + fake.searchReturns = struct { + result1 []models.ServicePlanVisibilityFields + result2 error + }{result1, result2} +} + +var _ ServicePlanVisibilityRepository = new(FakeServicePlanVisibilityRepository) diff --git a/cf/api/fakes/fake_service_repo.go b/cf/api/fakes/fake_service_repo.go new file mode 100644 index 00000000000..3f1ecccf9de --- /dev/null +++ b/cf/api/fakes/fake_service_repo.go @@ -0,0 +1,233 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/generic" +) + +type FakeServiceRepo struct { + GetServiceOfferingByGuidReturns struct { + ServiceOffering models.ServiceOffering + Error error + } + + GetAllServiceOfferingsReturns struct { + ServiceOfferings []models.ServiceOffering + Error error + } + + GetServiceOfferingsForSpaceReturns struct { + ServiceOfferings []models.ServiceOffering + Error error + } + GetServiceOfferingsForSpaceArgs struct { + SpaceGuid string + } + + FindServiceOfferingsForSpaceByLabelArgs struct { + SpaceGuid string + Name string + } + + FindServiceOfferingsForSpaceByLabelReturns struct { + ServiceOfferings models.ServiceOfferings + Error error + } + + CreateServiceInstanceArgs struct { + Name string + PlanGuid string + } + CreateServiceInstanceReturns struct { + Error error + } + + UpdateServiceInstanceArgs struct { + InstanceGuid string + PlanGuid string + } + + UpdateServiceInstanceReturnsErr bool + + FindInstanceByNameName string + FindInstanceByNameServiceInstance models.ServiceInstance + FindInstanceByNameErr bool + FindInstanceByNameNotFound bool + + FindInstanceByNameMap generic.Map + + DeleteServiceServiceInstance models.ServiceInstance + + RenameServiceServiceInstance models.ServiceInstance + RenameServiceNewName string + + PurgedServiceOffering models.ServiceOffering + PurgeServiceOfferingCalled bool + PurgeServiceOfferingApiResponse error + + FindServiceOfferingByLabelAndProviderName string + FindServiceOfferingByLabelAndProviderProvider string + FindServiceOfferingByLabelAndProviderServiceOffering models.ServiceOffering + FindServiceOfferingByLabelAndProviderApiResponse error + FindServiceOfferingByLabelAndProviderCalled bool + + FindServiceOfferingsByLabelName string + FindServiceOfferingsByLabelServiceOfferings models.ServiceOfferings + FindServiceOfferingsByLabelApiResponse error + FindServiceOfferingsByLabelCalled bool + + ListServicesFromBrokerReturns map[string][]models.ServiceOffering + ListServicesFromBrokerErr error + + V1ServicePlanDescription resources.ServicePlanDescription + V2ServicePlanDescription resources.ServicePlanDescription + FindServicePlanByDescriptionArguments []resources.ServicePlanDescription + FindServicePlanByDescriptionResultGuids []string + FindServicePlanByDescriptionResponses []error + findServicePlanByDescriptionCallCount int + + ServiceInstanceCountForServicePlan int + ServiceInstanceCountApiResponse error + + V1GuidToMigrate string + V2GuidToMigrate string + MigrateServicePlanFromV1ToV2Called bool + MigrateServicePlanFromV1ToV2ReturnedCount int + MigrateServicePlanFromV1ToV2Response error +} + +func (repo *FakeServiceRepo) GetServiceOfferingByGuid(guid string) (models.ServiceOffering, error) { + return repo.GetServiceOfferingByGuidReturns.ServiceOffering, repo.GetServiceOfferingByGuidReturns.Error +} + +func (repo *FakeServiceRepo) GetAllServiceOfferings() (models.ServiceOfferings, error) { + return repo.GetAllServiceOfferingsReturns.ServiceOfferings, repo.GetAllServiceOfferingsReturns.Error +} + +func (repo *FakeServiceRepo) GetServiceOfferingsForSpace(spaceGuid string) (models.ServiceOfferings, error) { + repo.GetServiceOfferingsForSpaceArgs.SpaceGuid = spaceGuid + return repo.GetServiceOfferingsForSpaceReturns.ServiceOfferings, repo.GetServiceOfferingsForSpaceReturns.Error +} + +func (repo *FakeServiceRepo) FindServiceOfferingsForSpaceByLabel(spaceGuid, name string) (models.ServiceOfferings, error) { + repo.FindServiceOfferingsForSpaceByLabelArgs.Name = name + repo.FindServiceOfferingsForSpaceByLabelArgs.SpaceGuid = spaceGuid + return repo.FindServiceOfferingsForSpaceByLabelReturns.ServiceOfferings, repo.FindServiceOfferingsForSpaceByLabelReturns.Error +} + +func (repo *FakeServiceRepo) PurgeServiceOffering(offering models.ServiceOffering) (apiErr error) { + repo.PurgedServiceOffering = offering + repo.PurgeServiceOfferingCalled = true + return repo.PurgeServiceOfferingApiResponse +} + +func (repo *FakeServiceRepo) FindServiceOfferingByLabelAndProvider(name, provider string) (offering models.ServiceOffering, apiErr error) { + repo.FindServiceOfferingByLabelAndProviderCalled = true + repo.FindServiceOfferingByLabelAndProviderName = name + repo.FindServiceOfferingByLabelAndProviderProvider = provider + apiErr = repo.FindServiceOfferingByLabelAndProviderApiResponse + offering = repo.FindServiceOfferingByLabelAndProviderServiceOffering + return +} + +func (repo *FakeServiceRepo) FindServiceOfferingsByLabel(name string) (offerings models.ServiceOfferings, apiErr error) { + repo.FindServiceOfferingsByLabelCalled = true + repo.FindServiceOfferingsByLabelName = name + apiErr = repo.FindServiceOfferingsByLabelApiResponse + offerings = repo.FindServiceOfferingsByLabelServiceOfferings + return +} + +func (repo *FakeServiceRepo) CreateServiceInstance(name, planGuid string) (apiErr error) { + repo.CreateServiceInstanceArgs.Name = name + repo.CreateServiceInstanceArgs.PlanGuid = planGuid + + return repo.CreateServiceInstanceReturns.Error +} + +func (repo *FakeServiceRepo) UpdateServiceInstance(instanceGuid, planGuid string) (apiErr error) { + + if repo.UpdateServiceInstanceReturnsErr { + apiErr = errors.New("Error updating service instance") + } else { + repo.UpdateServiceInstanceArgs.InstanceGuid = instanceGuid + repo.UpdateServiceInstanceArgs.PlanGuid = planGuid + } + + return +} + +func (repo *FakeServiceRepo) FindInstanceByName(name string) (instance models.ServiceInstance, apiErr error) { + repo.FindInstanceByNameName = name + + if repo.FindInstanceByNameMap != nil && repo.FindInstanceByNameMap.Has(name) { + instance = repo.FindInstanceByNameMap.Get(name).(models.ServiceInstance) + } else { + instance = repo.FindInstanceByNameServiceInstance + } + + if repo.FindInstanceByNameErr { + apiErr = errors.New("Error finding instance") + } + + if repo.FindInstanceByNameNotFound { + apiErr = errors.NewModelNotFoundError("Service instance", name) + } + + return +} + +func (repo *FakeServiceRepo) DeleteService(instance models.ServiceInstance) (apiErr error) { + repo.DeleteServiceServiceInstance = instance + return +} + +func (repo *FakeServiceRepo) RenameService(instance models.ServiceInstance, newName string) (apiErr error) { + repo.RenameServiceServiceInstance = instance + repo.RenameServiceNewName = newName + return +} + +func (repo *FakeServiceRepo) FindServicePlanByDescription(planDescription resources.ServicePlanDescription) (planGuid string, apiErr error) { + + repo.FindServicePlanByDescriptionArguments = + append(repo.FindServicePlanByDescriptionArguments, planDescription) + + if len(repo.FindServicePlanByDescriptionResultGuids) > repo.findServicePlanByDescriptionCallCount { + planGuid = repo.FindServicePlanByDescriptionResultGuids[repo.findServicePlanByDescriptionCallCount] + } + if len(repo.FindServicePlanByDescriptionResponses) > repo.findServicePlanByDescriptionCallCount { + apiErr = repo.FindServicePlanByDescriptionResponses[repo.findServicePlanByDescriptionCallCount] + } + repo.findServicePlanByDescriptionCallCount += 1 + return +} + +func (repo *FakeServiceRepo) ListServicesFromBroker(brokerGuid string) ([]models.ServiceOffering, error) { + if repo.ListServicesFromBrokerErr != nil { + return nil, repo.ListServicesFromBrokerErr + } + + if repo.ListServicesFromBrokerReturns[brokerGuid] != nil { + return repo.ListServicesFromBrokerReturns[brokerGuid], nil + } + + return []models.ServiceOffering{}, nil +} + +func (repo *FakeServiceRepo) GetServiceInstanceCountForServicePlan(v1PlanGuid string) (count int, apiErr error) { + count = repo.ServiceInstanceCountForServicePlan + apiErr = repo.ServiceInstanceCountApiResponse + return +} + +func (repo *FakeServiceRepo) MigrateServicePlanFromV1ToV2(v1PlanGuid, v2PlanGuid string) (changedCount int, apiErr error) { + repo.MigrateServicePlanFromV1ToV2Called = true + repo.V1GuidToMigrate = v1PlanGuid + repo.V2GuidToMigrate = v2PlanGuid + changedCount = repo.MigrateServicePlanFromV1ToV2ReturnedCount + apiErr = repo.MigrateServicePlanFromV1ToV2Response + return +} diff --git a/cf/api/fakes/fake_service_summary_repo.go b/cf/api/fakes/fake_service_summary_repo.go new file mode 100644 index 00000000000..deb0fc7cbfe --- /dev/null +++ b/cf/api/fakes/fake_service_summary_repo.go @@ -0,0 +1,12 @@ +package fakes + +import "github.com/cloudfoundry/cli/cf/models" + +type FakeServiceSummaryRepo struct { + GetSummariesInCurrentSpaceInstances []models.ServiceInstance +} + +func (repo *FakeServiceSummaryRepo) GetSummariesInCurrentSpace() (instances []models.ServiceInstance, apiErr error) { + instances = repo.GetSummariesInCurrentSpaceInstances + return +} diff --git a/cf/api/fakes/fake_space_repo.go b/cf/api/fakes/fake_space_repo.go new file mode 100644 index 00000000000..8210c8efd07 --- /dev/null +++ b/cf/api/fakes/fake_space_repo.go @@ -0,0 +1,107 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeSpaceRepository struct { + CurrentSpace models.Space + + Spaces []models.Space + + FindByNameName string + FindByNameSpace models.Space + FindByNameErr bool + FindByNameNotFound bool + + FindByNameInOrgName string + FindByNameInOrgOrgGuid string + FindByNameInOrgSpace models.Space + FindByNameInOrgError error + + SummarySpace models.Space + + CreateSpaceName string + CreateSpaceOrgGuid string + CreateSpaceSpaceQuotaGuid string + CreateSpaceExists bool + CreateSpaceSpace models.Space + + RenameSpaceGuid string + RenameNewName string + + DeletedSpaceGuid string +} + +func (repo FakeSpaceRepository) GetCurrentSpace() (space models.Space) { + return repo.CurrentSpace +} + +func (repo FakeSpaceRepository) ListSpaces(callback func(models.Space) bool) error { + for _, space := range repo.Spaces { + if !callback(space) { + break + } + } + return nil +} + +func (repo *FakeSpaceRepository) FindByName(name string) (space models.Space, apiErr error) { + repo.FindByNameName = name + + var foundSpace bool = false + for _, someSpace := range repo.Spaces { + if name == someSpace.Name { + foundSpace = true + space = someSpace + break + } + } + + if repo.FindByNameErr || !foundSpace { + apiErr = errors.New("Error finding space by name.") + } + + if repo.FindByNameNotFound { + apiErr = errors.NewModelNotFoundError("Space", name) + } + + return +} + +func (repo *FakeSpaceRepository) FindByNameInOrg(name, orgGuid string) (space models.Space, apiErr error) { + repo.FindByNameInOrgName = name + repo.FindByNameInOrgOrgGuid = orgGuid + space = repo.FindByNameInOrgSpace + apiErr = repo.FindByNameInOrgError + return +} + +func (repo *FakeSpaceRepository) GetSummary() (space models.Space, apiErr error) { + space = repo.SummarySpace + return +} + +func (repo *FakeSpaceRepository) Create(name, orgGuid, spaceQuotaGuid string) (space models.Space, apiErr error) { + if repo.CreateSpaceExists { + apiErr = errors.NewHttpError(400, errors.SPACE_EXISTS, "Space already exists") + return + } + repo.CreateSpaceName = name + repo.CreateSpaceOrgGuid = orgGuid + repo.CreateSpaceSpaceQuotaGuid = spaceQuotaGuid + space = repo.CreateSpaceSpace + return +} + +func (repo *FakeSpaceRepository) Rename(spaceGuid, newName string) (apiErr error) { + repo.RenameSpaceGuid = spaceGuid + repo.RenameNewName = newName + return +} + +func (repo *FakeSpaceRepository) Delete(spaceGuid string) (apiErr error) { + repo.DeletedSpaceGuid = spaceGuid + return +} diff --git a/cf/api/fakes/fake_user_provided_service_instance_repo.go b/cf/api/fakes/fake_user_provided_service_instance_repo.go new file mode 100644 index 00000000000..7011d0fc16c --- /dev/null +++ b/cf/api/fakes/fake_user_provided_service_instance_repo.go @@ -0,0 +1,23 @@ +package fakes + +import "github.com/cloudfoundry/cli/cf/models" + +type FakeUserProvidedServiceInstanceRepo struct { + CreateName string + CreateDrainUrl string + CreateParams map[string]interface{} + + UpdateServiceInstance models.ServiceInstanceFields +} + +func (repo *FakeUserProvidedServiceInstanceRepo) Create(name, drainUrl string, params map[string]interface{}) (apiErr error) { + repo.CreateName = name + repo.CreateDrainUrl = drainUrl + repo.CreateParams = params + return +} + +func (repo *FakeUserProvidedServiceInstanceRepo) Update(serviceInstance models.ServiceInstanceFields) (apiErr error) { + repo.UpdateServiceInstance = serviceInstance + return +} diff --git a/cf/api/fakes/fake_user_repo.go b/cf/api/fakes/fake_user_repo.go new file mode 100644 index 00000000000..180485e3c4c --- /dev/null +++ b/cf/api/fakes/fake_user_repo.go @@ -0,0 +1,109 @@ +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeUserRepository struct { + FindByUsernameUsername string + FindByUsernameUserFields models.UserFields + FindByUsernameNotFound bool + + ListUsersOrganizationGuid string + ListUsersSpaceGuid string + ListUsersByRole map[string][]models.UserFields + + CreateUserUsername string + CreateUserPassword string + CreateUserExists bool + CreateUserReturnsHttpError bool + + DeleteUserGuid string + + SetOrgRoleUserGuid string + SetOrgRoleOrganizationGuid string + SetOrgRoleRole string + + UnsetOrgRoleUserGuid string + UnsetOrgRoleOrganizationGuid string + UnsetOrgRoleRole string + + SetSpaceRoleUserGuid string + SetSpaceRoleOrgGuid string + SetSpaceRoleSpaceGuid string + SetSpaceRoleRole string + + UnsetSpaceRoleUserGuid string + UnsetSpaceRoleSpaceGuid string + UnsetSpaceRoleRole string +} + +func (repo *FakeUserRepository) FindByUsername(username string) (user models.UserFields, apiErr error) { + repo.FindByUsernameUsername = username + user = repo.FindByUsernameUserFields + + if repo.FindByUsernameNotFound { + apiErr = errors.NewModelNotFoundError("User", "") + } + + return +} + +func (repo *FakeUserRepository) ListUsersInOrgForRole(orgGuid string, roleName string) ([]models.UserFields, error) { + repo.ListUsersOrganizationGuid = orgGuid + return repo.ListUsersByRole[roleName], nil +} + +func (repo *FakeUserRepository) ListUsersInSpaceForRole(spaceGuid string, roleName string) ([]models.UserFields, error) { + repo.ListUsersSpaceGuid = spaceGuid + return repo.ListUsersByRole[roleName], nil +} + +func (repo *FakeUserRepository) Create(username, password string) (apiErr error) { + repo.CreateUserUsername = username + repo.CreateUserPassword = password + + if repo.CreateUserReturnsHttpError { + apiErr = errors.NewHttpError(403, "403", "Forbidden") + } + if repo.CreateUserExists { + apiErr = errors.NewModelAlreadyExistsError("User", username) + } + + return +} + +func (repo *FakeUserRepository) Delete(userGuid string) (apiErr error) { + repo.DeleteUserGuid = userGuid + return +} + +func (repo *FakeUserRepository) SetOrgRole(userGuid, orgGuid, role string) (apiErr error) { + repo.SetOrgRoleUserGuid = userGuid + repo.SetOrgRoleOrganizationGuid = orgGuid + repo.SetOrgRoleRole = role + return +} + +func (repo *FakeUserRepository) UnsetOrgRole(userGuid, orgGuid, role string) (apiErr error) { + repo.UnsetOrgRoleUserGuid = userGuid + repo.UnsetOrgRoleOrganizationGuid = orgGuid + repo.UnsetOrgRoleRole = role + return +} + +func (repo *FakeUserRepository) SetSpaceRole(userGuid, spaceGuid, orgGuid, role string) (apiErr error) { + repo.SetSpaceRoleUserGuid = userGuid + repo.SetSpaceRoleOrgGuid = orgGuid + repo.SetSpaceRoleSpaceGuid = spaceGuid + repo.SetSpaceRoleRole = role + return +} + +func (repo *FakeUserRepository) UnsetSpaceRole(userGuid, spaceGuid, role string) (apiErr error) { + repo.UnsetSpaceRoleUserGuid = userGuid + repo.UnsetSpaceRoleSpaceGuid = spaceGuid + repo.UnsetSpaceRoleRole = role + return +} diff --git a/cf/api/feature_flags/fakes/fake_feature_flag_repository.go b/cf/api/feature_flags/fakes/fake_feature_flag_repository.go new file mode 100644 index 00000000000..4eeff9a2fe3 --- /dev/null +++ b/cf/api/feature_flags/fakes/fake_feature_flag_repository.go @@ -0,0 +1,126 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeFeatureFlagRepository struct { + ListStub func() ([]models.FeatureFlag, error) + listMutex sync.RWMutex + listArgsForCall []struct{} + listReturns struct { + result1 []models.FeatureFlag + result2 error + } + FindByNameStub func(string) (models.FeatureFlag, error) + findByNameMutex sync.RWMutex + findByNameArgsForCall []struct { + arg1 string + } + findByNameReturns struct { + result1 models.FeatureFlag + result2 error + } + UpdateStub func(string, bool) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 string + arg2 bool + } + updateReturns struct { + result1 error + } +} + +func (fake *FakeFeatureFlagRepository) List() ([]models.FeatureFlag, error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.listArgsForCall = append(fake.listArgsForCall, struct{}{}) + if fake.ListStub != nil { + return fake.ListStub() + } else { + return fake.listReturns.result1, fake.listReturns.result2 + } +} + +func (fake *FakeFeatureFlagRepository) ListCallCount() int { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + return len(fake.listArgsForCall) +} + +func (fake *FakeFeatureFlagRepository) ListReturns(result1 []models.FeatureFlag, result2 error) { + fake.listReturns = struct { + result1 []models.FeatureFlag + result2 error + }{result1, result2} +} + +func (fake *FakeFeatureFlagRepository) FindByName(arg1 string) (models.FeatureFlag, error) { + fake.findByNameMutex.Lock() + defer fake.findByNameMutex.Unlock() + fake.findByNameArgsForCall = append(fake.findByNameArgsForCall, struct { + arg1 string + }{arg1}) + if fake.FindByNameStub != nil { + return fake.FindByNameStub(arg1) + } else { + return fake.findByNameReturns.result1, fake.findByNameReturns.result2 + } +} + +func (fake *FakeFeatureFlagRepository) FindByNameCallCount() int { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return len(fake.findByNameArgsForCall) +} + +func (fake *FakeFeatureFlagRepository) FindByNameArgsForCall(i int) string { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return fake.findByNameArgsForCall[i].arg1 +} + +func (fake *FakeFeatureFlagRepository) FindByNameReturns(result1 models.FeatureFlag, result2 error) { + fake.findByNameReturns = struct { + result1 models.FeatureFlag + result2 error + }{result1, result2} +} + +func (fake *FakeFeatureFlagRepository) Update(arg1 string, arg2 bool) error { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 string + arg2 bool + }{arg1, arg2}) + if fake.UpdateStub != nil { + return fake.UpdateStub(arg1, arg2) + } else { + return fake.updateReturns.result1 + } +} + +func (fake *FakeFeatureFlagRepository) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeFeatureFlagRepository) UpdateArgsForCall(i int) (string, bool) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return fake.updateArgsForCall[i].arg1, fake.updateArgsForCall[i].arg2 +} + +func (fake *FakeFeatureFlagRepository) UpdateReturns(result1 error) { + fake.updateReturns = struct { + result1 error + }{result1} +} + +var _ FeatureFlagRepository = new(FakeFeatureFlagRepository) diff --git a/cf/api/feature_flags/feature_flags.go b/cf/api/feature_flags/feature_flags.go new file mode 100644 index 00000000000..29f5c428ea7 --- /dev/null +++ b/cf/api/feature_flags/feature_flags.go @@ -0,0 +1,61 @@ +package feature_flags + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type FeatureFlagRepository interface { + List() ([]models.FeatureFlag, error) + FindByName(string) (models.FeatureFlag, error) + Update(string, bool) error +} + +type CloudControllerFeatureFlagRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerFeatureFlagRepository(config core_config.Reader, gateway net.Gateway) CloudControllerFeatureFlagRepository { + return CloudControllerFeatureFlagRepository{ + config: config, + gateway: gateway, + } +} + +func (repo CloudControllerFeatureFlagRepository) List() ([]models.FeatureFlag, error) { + flags := []models.FeatureFlag{} + apiError := repo.gateway.GetResource( + fmt.Sprintf("%s/v2/config/feature_flags", repo.config.ApiEndpoint()), + &flags) + + if apiError != nil { + return nil, apiError + } + + return flags, nil +} + +func (repo CloudControllerFeatureFlagRepository) FindByName(name string) (models.FeatureFlag, error) { + flag := models.FeatureFlag{} + apiError := repo.gateway.GetResource( + fmt.Sprintf("%s/v2/config/feature_flags/%s", repo.config.ApiEndpoint(), name), + &flag) + + if apiError != nil { + return models.FeatureFlag{}, apiError + } + + return flag, nil +} + +func (repo CloudControllerFeatureFlagRepository) Update(flag string, set bool) error { + url := fmt.Sprintf("/v2/config/feature_flags/%s", flag) + body := fmt.Sprintf(`{"enabled": %v}`, set) + + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), url, strings.NewReader(body)) +} diff --git a/cf/api/feature_flags/feature_flags_suite_test.go b/cf/api/feature_flags/feature_flags_suite_test.go new file mode 100644 index 00000000000..f89804d460a --- /dev/null +++ b/cf/api/feature_flags/feature_flags_suite_test.go @@ -0,0 +1,19 @@ +package feature_flags_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFeatureFlags(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "FeatureFlags Suite") +} diff --git a/cf/api/feature_flags/feature_flags_test.go b/cf/api/feature_flags/feature_flags_test.go new file mode 100644 index 00000000000..f8738e4a77a --- /dev/null +++ b/cf/api/feature_flags/feature_flags_test.go @@ -0,0 +1,186 @@ +package feature_flags_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/feature_flags" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Feature Flags Repository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerFeatureFlagRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerFeatureFlagRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".List", func() { + BeforeEach(func() { + setupTestServer(featureFlagsGetAllRequest) + }) + + It("returns all of the feature flags", func() { + featureFlagModels, err := repo.List() + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(featureFlagModels)).To(Equal(5)) + Expect(featureFlagModels[0].Name).To(Equal("user_org_creation")) + Expect(featureFlagModels[0].Enabled).To(BeFalse()) + Expect(featureFlagModels[1].Name).To(Equal("private_domain_creation")) + Expect(featureFlagModels[1].Enabled).To(BeFalse()) + Expect(featureFlagModels[2].Name).To(Equal("app_bits_upload")) + Expect(featureFlagModels[2].Enabled).To(BeTrue()) + Expect(featureFlagModels[3].Name).To(Equal("app_scaling")) + Expect(featureFlagModels[3].Enabled).To(BeTrue()) + Expect(featureFlagModels[4].Name).To(Equal("route_creation")) + Expect(featureFlagModels[4].Enabled).To(BeTrue()) + }) + }) + + Describe(".FindByName", func() { + BeforeEach(func() { + setupTestServer(featureFlagRequest) + }) + + It("returns the requested", func() { + featureFlagModel, err := repo.FindByName("user_org_creation") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + + Expect(featureFlagModel.Name).To(Equal("user_org_creation")) + Expect(featureFlagModel.Enabled).To(BeFalse()) + }) + }) + + Describe(".Update", func() { + BeforeEach(func() { + setupTestServer(featureFlagsUpdateRequest) + }) + + It("updates the given feature flag with the specified value", func() { + err := repo.Update("app_scaling", true) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("when given a non-existent feature flag", func() { + BeforeEach(func() { + setupTestServer(featureFlagsUpdateErrorRequest) + }) + + It("returns an error", func() { + err := repo.Update("i_dont_exist", true) + Expect(err).To(HaveOccurred()) + }) + }) + }) +}) + +var featureFlagsGetAllRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/feature_flags", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `[ + { + "name": "user_org_creation", + "enabled": false, + "error_message": null, + "url": "/v2/config/feature_flags/user_org_creation" + }, + { + "name": "private_domain_creation", + "enabled": false, + "error_message": "foobar", + "url": "/v2/config/feature_flags/private_domain_creation" + }, + { + "name": "app_bits_upload", + "enabled": true, + "error_message": null, + "url": "/v2/config/feature_flags/app_bits_upload" + }, + { + "name": "app_scaling", + "enabled": true, + "error_message": null, + "url": "/v2/config/feature_flags/app_scaling" + }, + { + "name": "route_creation", + "enabled": true, + "error_message": null, + "url": "/v2/config/feature_flags/route_creation" + } +]`, + }, +}) + +var featureFlagRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/feature_flags/user_org_creation", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "name": "user_org_creation", + "enabled": false, + "error_message": null, + "url": "/v2/config/feature_flags/user_org_creation" +}`, + }, +}) + +var featureFlagsUpdateErrorRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/feature_flags/i_dont_exist", + Response: testnet.TestResponse{ + Status: http.StatusNotFound, + Body: `{ + "code": 330000, + "description": "The feature flag could not be found: i_dont_exist", + "error_code": "CF-FeatureFlagNotFound" + }`, + }, +}) + +var featureFlagsUpdateRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/feature_flags/app_scaling", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "name": "app_scaling", + "enabled": true, + "error_message": null, + "url": "/v2/config/feature_flags/app_scaling" + }`, + }, +}) diff --git a/cf/api/log_message_queue.go b/cf/api/log_message_queue.go new file mode 100644 index 00000000000..1b72bc8206d --- /dev/null +++ b/cf/api/log_message_queue.go @@ -0,0 +1,81 @@ +package api + +import ( + "sort" + "sync" + "time" + + "github.com/cloudfoundry/loggregatorlib/logmessage" +) + +const MAX_INT64 int64 = 1<<63 - 1 + +type item struct { + message *logmessage.LogMessage + timestampWhenOutputtable int64 +} + +type SortedMessageQueue struct { + clock func() time.Time + printTimeBuffer time.Duration + items []*item + + mutex sync.Mutex +} + +func NewSortedMessageQueue(printTimeBuffer time.Duration, clock func() time.Time) *SortedMessageQueue { + return &SortedMessageQueue{ + clock: clock, + printTimeBuffer: printTimeBuffer, + } +} + +func (pq *SortedMessageQueue) PushMessage(message *logmessage.LogMessage) { + pq.mutex.Lock() + defer pq.mutex.Unlock() + + item := &item{message: message, timestampWhenOutputtable: pq.clock().Add(pq.printTimeBuffer).UnixNano()} + pq.items = append(pq.items, item) + sort.Stable(pq) +} + +func (pq *SortedMessageQueue) PopMessage() *logmessage.LogMessage { + pq.mutex.Lock() + defer pq.mutex.Unlock() + + if len(pq.items) == 0 { + return nil + } + + var item *item + item = pq.items[0] + pq.items = pq.items[1:len(pq.items)] + + return item.message +} + +func (pq *SortedMessageQueue) NextTimestamp() int64 { + pq.mutex.Lock() + defer pq.mutex.Unlock() + + currentQueue := pq.items + n := len(currentQueue) + if n == 0 { + return MAX_INT64 + } + item := currentQueue[0] + return item.timestampWhenOutputtable +} + +// implement sort interface so we can sort messages as we receive them in PushMessage +func (pq *SortedMessageQueue) Less(i, j int) bool { + return *pq.items[i].message.Timestamp < *pq.items[j].message.Timestamp +} + +func (pq *SortedMessageQueue) Swap(i, j int) { + pq.items[i], pq.items[j] = pq.items[j], pq.items[i] +} + +func (pq *SortedMessageQueue) Len() int { + return len(pq.items) +} diff --git a/cf/api/log_message_queue_test.go b/cf/api/log_message_queue_test.go new file mode 100644 index 00000000000..3aa512d4d30 --- /dev/null +++ b/cf/api/log_message_queue_test.go @@ -0,0 +1,106 @@ +package api_test + +import ( + "fmt" + "time" + + "code.google.com/p/gogoprotobuf/proto" + "github.com/cloudfoundry/loggregatorlib/logmessage" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("is a priority queue used to sort loggregator messages", func() { + It("PriorityQueue returns a new queue", func() { + pq := NewSortedMessageQueue(10*time.Millisecond, time.Now) + + msg3 := logMessageWithTime("message 3", 130) + pq.PushMessage(msg3) + msg2 := logMessageWithTime("message 2", 120) + pq.PushMessage(msg2) + msg4 := logMessageWithTime("message 4", 140) + pq.PushMessage(msg4) + msg1 := logMessageWithTime("message 1", 110) + pq.PushMessage(msg1) + + Expect(getMsgString(pq.PopMessage())).To(Equal(getMsgString(msg1))) + Expect(getMsgString(pq.PopMessage())).To(Equal(getMsgString(msg2))) + Expect(getMsgString(pq.PopMessage())).To(Equal(getMsgString(msg3))) + Expect(getMsgString(pq.PopMessage())).To(Equal(getMsgString(msg4))) + }) + + It("pops on empty queue", func() { + pq := NewSortedMessageQueue(10*time.Millisecond, time.Now) + Expect(pq.PopMessage()).To(BeNil()) + }) + + It("NextTimeStamp returns the timestamp of the log message at the head of the queue", func() { + currentTime := time.Unix(5, 0) + clock := func() time.Time { + return currentTime + } + + pq := NewSortedMessageQueue(5*time.Second, clock) + Expect(pq.NextTimestamp()).To(Equal(MAX_INT64)) + + msg2 := logMessageWithTime("message 2", 130) + pq.PushMessage(msg2) + + currentTime = time.Unix(6, 0) + msg1 := logMessageWithTime("message 1", 100) + pq.PushMessage(msg1) + Expect(pq.NextTimestamp()).To(Equal(time.Unix(11, 0).UnixNano())) + + readMessage := pq.PopMessage() + Expect(readMessage.GetTimestamp()).To(Equal(int64(100))) + Expect(pq.NextTimestamp()).To(Equal(time.Unix(10, 0).UnixNano())) + + readMessage = pq.PopMessage() + Expect(readMessage.GetTimestamp()).To(Equal(int64(130))) + Expect(pq.NextTimestamp()).To(Equal(MAX_INT64)) + }) + + It("sorts messages based on their timestamp", func() { + pq := NewSortedMessageQueue(10*time.Millisecond, time.Now) + + msg1 := logMessageWithTime("message first", 109) + pq.PushMessage(msg1) + + for i := 1; i < 1000; i++ { + msg := logMessageWithTime(fmt.Sprintf("message %d", i), 110) + pq.PushMessage(msg) + } + msg2 := logMessageWithTime("message last", 111) + pq.PushMessage(msg2) + + Expect(getMsgString(pq.PopMessage())).To(Equal("message first")) + + for i := 1; i < 1000; i++ { + Expect(getMsgString(pq.PopMessage())).To(Equal(fmt.Sprintf("message %d", i))) + } + + Expect(getMsgString(pq.PopMessage())).To(Equal("message last")) + }) +}) + +func logMessageWithTime(messageString string, timestamp int) *logmessage.LogMessage { + return generateMessage(messageString, int64(timestamp)) +} + +func generateMessage(messageString string, timestamp int64) *logmessage.LogMessage { + messageType := logmessage.LogMessage_OUT + sourceName := "DEA" + return &logmessage.LogMessage{ + Message: []byte(messageString), + AppId: proto.String("my-app-guid"), + MessageType: &messageType, + SourceName: &sourceName, + Timestamp: proto.Int64(timestamp), + } +} + +func getMsgString(message *logmessage.LogMessage) string { + return string(message.GetMessage()) +} diff --git a/cf/api/logs.go b/cf/api/logs.go new file mode 100644 index 00000000000..4ad070c9511 --- /dev/null +++ b/cf/api/logs.go @@ -0,0 +1,130 @@ +package api + +import ( + "crypto/tls" + "errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "time" + + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + consumer "github.com/cloudfoundry/loggregator_consumer" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/cloudfoundry/loggregatorlib/logmessage" +) + +type LogsRepository interface { + RecentLogsFor(appGuid string) ([]*logmessage.LogMessage, error) + TailLogsFor(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error + Close() +} + +type LoggregatorLogsRepository struct { + consumer consumer.LoggregatorConsumer + config core_config.Reader + TrustedCerts []tls.Certificate + tokenRefresher authentication.TokenRefresher + messageQueue *SortedMessageQueue + + onMessage func(*logmessage.LogMessage) +} + +var BufferTime time.Duration = 5 * time.Second + +func NewLoggregatorLogsRepository(config core_config.Reader, consumer consumer.LoggregatorConsumer, refresher authentication.TokenRefresher) LogsRepository { + return &LoggregatorLogsRepository{ + config: config, + consumer: consumer, + tokenRefresher: refresher, + messageQueue: NewSortedMessageQueue(BufferTime, time.Now), + } +} + +func (repo *LoggregatorLogsRepository) Close() { + repo.consumer.Close() + repo.flushMessageQueue() +} + +func (repo *LoggregatorLogsRepository) RecentLogsFor(appGuid string) ([]*logmessage.LogMessage, error) { + messages, err := repo.consumer.Recent(appGuid, repo.config.AccessToken()) + + switch err.(type) { + case nil: // do nothing + case *noaa_errors.UnauthorizedError: + repo.tokenRefresher.RefreshAuthToken() + messages, err = repo.consumer.Recent(appGuid, repo.config.AccessToken()) + default: + return messages, err + } + + consumer.SortRecent(messages) + return messages, err +} + +func (repo *LoggregatorLogsRepository) TailLogsFor(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error { + repo.onMessage = onMessage + + endpoint := repo.config.LoggregatorEndpoint() + if endpoint == "" { + return errors.New(T("Loggregator endpoint missing from config file")) + } + + repo.consumer.SetOnConnectCallback(onConnect) + logChan, err := repo.consumer.Tail(appGuid, repo.config.AccessToken()) + switch err.(type) { + case nil: // do nothing + case *noaa_errors.UnauthorizedError: + repo.tokenRefresher.RefreshAuthToken() + logChan, err = repo.consumer.Tail(appGuid, repo.config.AccessToken()) + default: + return err + } + + if err != nil { + return err + } + + repo.bufferMessages(logChan, onMessage) + return nil +} + +func (repo *LoggregatorLogsRepository) bufferMessages(logChan <-chan *logmessage.LogMessage, onMessage func(*logmessage.LogMessage)) { + + for { + sendMessages(repo.messageQueue, onMessage) + + select { + case msg, ok := <-logChan: + if !ok { + return + } + repo.messageQueue.PushMessage(msg) + default: + time.Sleep(1 * time.Millisecond) + } + } +} + +func (repo *LoggregatorLogsRepository) flushMessageQueue() { + if repo.onMessage == nil { + return + } + + for { + message := repo.messageQueue.PopMessage() + if message == nil { + break + } + + repo.onMessage(message) + } + + repo.onMessage = nil +} + +func sendMessages(queue *SortedMessageQueue, onMessage func(*logmessage.LogMessage)) { + for queue.NextTimestamp() < time.Now().UnixNano() { + msg := queue.PopMessage() + onMessage(msg) + } +} diff --git a/cf/api/logs_test.go b/cf/api/logs_test.go new file mode 100644 index 00000000000..cfe06d7addd --- /dev/null +++ b/cf/api/logs_test.go @@ -0,0 +1,255 @@ +package api_test + +import ( + "code.google.com/p/gogoprotobuf/proto" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/loggregator_consumer/noaa_errors" + "github.com/cloudfoundry/loggregatorlib/logmessage" + + "time" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("loggregator logs repository", func() { + var ( + fakeConsumer *testapi.FakeLoggregatorConsumer + logsRepo LogsRepository + configRepo core_config.ReadWriter + fakeTokenRefresher *testapi.FakeAuthenticationRepository + ) + + BeforeEach(func() { + BufferTime = 1 * time.Millisecond + fakeConsumer = testapi.NewFakeLoggregatorConsumer() + configRepo = testconfig.NewRepositoryWithDefaults() + configRepo.SetLoggregatorEndpoint("loggregator-server.test.com") + configRepo.SetAccessToken("the-access-token") + fakeTokenRefresher = &testapi.FakeAuthenticationRepository{} + }) + + JustBeforeEach(func() { + logsRepo = NewLoggregatorLogsRepository(configRepo, fakeConsumer, fakeTokenRefresher) + }) + + Describe("RecentLogsFor", func() { + Context("when a noaa_errors.UnauthorizedError occurs", func() { + BeforeEach(func() { + fakeConsumer.RecentReturns.Err = []error{ + noaa_errors.NewUnauthorizedError("i'm sorry dave"), + nil, + } + }) + + It("refreshes the access token", func() { + _, err := logsRepo.RecentLogsFor("app-guid") + Expect(err).ToNot(HaveOccurred()) + Expect(fakeTokenRefresher.RefreshTokenCalled).To(BeTrue()) + }) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + fakeConsumer.RecentReturns.Err = []error{errors.New("oops")} + }) + + It("returns the error", func() { + _, err := logsRepo.RecentLogsFor("app-guid") + Expect(err).To(Equal(errors.New("oops"))) + }) + }) + + Context("when an error does not occur", func() { + BeforeEach(func() { + fakeConsumer.RecentReturns.Messages = []*logmessage.LogMessage{ + makeLogMessage("My message 2", int64(2000)), + makeLogMessage("My message 1", int64(1000)), + } + }) + + It("gets the logs for the requested app", func() { + logsRepo.RecentLogsFor("app-guid") + Expect(fakeConsumer.RecentCalledWith.AppGuid).To(Equal("app-guid")) + }) + + It("writes the sorted log messages onto the provided channel", func() { + messages, err := logsRepo.RecentLogsFor("app-guid") + Expect(err).NotTo(HaveOccurred()) + + Expect(string(messages[0].Message)).To(Equal("My message 1")) + Expect(string(messages[1].Message)).To(Equal("My message 2")) + }) + }) + }) + + Describe("tailing logs", func() { + Context("when an error occurs", func() { + BeforeEach(func() { + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + return nil, errors.New("oops") + } + }) + + It("returns an error", func() { + err := logsRepo.TailLogsFor("app-guid", func() {}, func(*logmessage.LogMessage) {}) + Expect(err).To(Equal(errors.New("oops"))) + }) + }) + + Context("when a LoggregatorConsumer.UnauthorizedError occurs", func() { + + It("refreshes the access token", func(done Done) { + calledOnce := false + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + if !calledOnce { + calledOnce = true + return nil, noaa_errors.NewUnauthorizedError("i'm sorry dave") + } else { + close(done) + return nil, nil + } + } + + err := logsRepo.TailLogsFor("app-guid", func() {}, func(*logmessage.LogMessage) {}) + Expect(err).ToNot(HaveOccurred()) + Expect(fakeTokenRefresher.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when LoggregatorConsumer.UnauthorizedError occurs again", func() { + It("returns an error", func(done Done) { + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + return nil, noaa_errors.NewUnauthorizedError("All the errors") + } + + err := logsRepo.TailLogsFor("app-guid", func() {}, func(*logmessage.LogMessage) {}) + Expect(err).To(HaveOccurred()) + close(done) + }) + }) + }) + + Context("when no error occurs", func() { + It("asks for the logs for the given app", func(done Done) { + fakeConsumer.TailFunc = func(appGuid, token string) (<-chan *logmessage.LogMessage, error) { + Expect(appGuid).To(Equal("app-guid")) + Expect(token).To(Equal("the-access-token")) + close(done) + return nil, nil + } + + logsRepo.TailLogsFor("app-guid", func() {}, func(msg *logmessage.LogMessage) {}) + }) + + It("sets the on connect callback", func(done Done) { + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + close(done) + return nil, nil + } + + called := false + logsRepo.TailLogsFor("app-guid", func() { called = true }, func(msg *logmessage.LogMessage) {}) + fakeConsumer.OnConnectCallback() + Expect(called).To(BeTrue()) + }) + + Context("and the buffer time is sufficient for sorting", func() { + BeforeEach(func() { + BufferTime = 250 * time.Millisecond + }) + + It("sorts the messages before yielding them", func(done Done) { + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + logChan := make(chan *logmessage.LogMessage) + go func() { + logChan <- makeLogMessage("hello3", 300) + logChan <- makeLogMessage("hello2", 200) + logChan <- makeLogMessage("hello1", 100) + fakeConsumer.WaitForClose() + close(logChan) + }() + + return logChan, nil + } + + receivedMessages := []*logmessage.LogMessage{} + err := logsRepo.TailLogsFor("app-guid", func() {}, func(msg *logmessage.LogMessage) { + receivedMessages = append(receivedMessages, msg) + if len(receivedMessages) >= 3 { + logsRepo.Close() + } + }) + + Expect(err).NotTo(HaveOccurred()) + + Expect(receivedMessages).To(Equal([]*logmessage.LogMessage{ + makeLogMessage("hello1", 100), + makeLogMessage("hello2", 200), + makeLogMessage("hello3", 300), + })) + + close(done) + }) + }) + + Context("and the buffer time is very long", func() { + BeforeEach(func() { + BufferTime = 30 * time.Second + }) + + It("flushes remaining log messages when Close is called", func(done Done) { + synchronizationChannel := make(chan (bool)) + + fakeConsumer.TailFunc = func(_, _ string) (<-chan *logmessage.LogMessage, error) { + fakeConsumer.OnConnectCallback() + logChan := make(chan *logmessage.LogMessage) + go func() { + logChan <- makeLogMessage("One does not simply consume a log message", 1000) + synchronizationChannel <- true + fakeConsumer.WaitForClose() + close(logChan) + }() + + return logChan, nil + } + + receivedMessages := []*logmessage.LogMessage{} + + go func() { + defer GinkgoRecover() + + <-synchronizationChannel + + Expect(receivedMessages).To(BeEmpty()) + logsRepo.Close() + Expect(receivedMessages).ToNot(BeEmpty()) + + done <- true + }() + + err := logsRepo.TailLogsFor("app-guid", func() {}, func(msg *logmessage.LogMessage) { + receivedMessages = append(receivedMessages, msg) + }) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + }) +}) + +func makeLogMessage(message string, timestamp int64) *logmessage.LogMessage { + messageType := logmessage.LogMessage_OUT + sourceName := "DEA" + return &logmessage.LogMessage{ + Message: []byte(message), + AppId: proto.String("my-app-guid"), + MessageType: &messageType, + SourceName: &sourceName, + Timestamp: proto.Int64(timestamp), + } +} diff --git a/cf/api/organizations/fakes/fake_organization_repository.go b/cf/api/organizations/fakes/fake_organization_repository.go new file mode 100644 index 00000000000..d354f071c83 --- /dev/null +++ b/cf/api/organizations/fakes/fake_organization_repository.go @@ -0,0 +1,209 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeOrganizationRepository struct { + ListOrgsStub func() (orgs []models.Organization, apiErr error) + listOrgsMutex sync.RWMutex + listOrgsArgsForCall []struct{} + listOrgsReturns struct { + result1 []models.Organization + result2 error + } + FindByNameStub func(name string) (org models.Organization, apiErr error) + findByNameMutex sync.RWMutex + findByNameArgsForCall []struct { + name string + } + findByNameReturns struct { + result1 models.Organization + result2 error + } + CreateStub func(org models.Organization) (apiErr error) + createMutex sync.RWMutex + createArgsForCall []struct { + org models.Organization + } + createReturns struct { + result1 error + } + RenameStub func(orgGuid string, name string) (apiErr error) + renameMutex sync.RWMutex + renameArgsForCall []struct { + orgGuid string + name string + } + renameReturns struct { + result1 error + } + DeleteStub func(orgGuid string) (apiErr error) + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + orgGuid string + } + deleteReturns struct { + result1 error + } +} + +func (fake *FakeOrganizationRepository) ListOrgs() (orgs []models.Organization, apiErr error) { + fake.listOrgsMutex.Lock() + defer fake.listOrgsMutex.Unlock() + fake.listOrgsArgsForCall = append(fake.listOrgsArgsForCall, struct{}{}) + if fake.ListOrgsStub != nil { + return fake.ListOrgsStub() + } else { + return fake.listOrgsReturns.result1, fake.listOrgsReturns.result2 + } +} + +func (fake *FakeOrganizationRepository) ListOrgsCallCount() int { + fake.listOrgsMutex.RLock() + defer fake.listOrgsMutex.RUnlock() + return len(fake.listOrgsArgsForCall) +} + +func (fake *FakeOrganizationRepository) ListOrgsReturns(result1 []models.Organization, result2 error) { + fake.ListOrgsStub = nil + fake.listOrgsReturns = struct { + result1 []models.Organization + result2 error + }{result1, result2} +} + +func (fake *FakeOrganizationRepository) FindByName(name string) (org models.Organization, apiErr error) { + fake.findByNameMutex.Lock() + defer fake.findByNameMutex.Unlock() + fake.findByNameArgsForCall = append(fake.findByNameArgsForCall, struct { + name string + }{name}) + if fake.FindByNameStub != nil { + return fake.FindByNameStub(name) + } else { + return fake.findByNameReturns.result1, fake.findByNameReturns.result2 + } +} + +func (fake *FakeOrganizationRepository) FindByNameCallCount() int { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return len(fake.findByNameArgsForCall) +} + +func (fake *FakeOrganizationRepository) FindByNameArgsForCall(i int) string { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return fake.findByNameArgsForCall[i].name +} + +func (fake *FakeOrganizationRepository) FindByNameReturns(result1 models.Organization, result2 error) { + fake.FindByNameStub = nil + fake.findByNameReturns = struct { + result1 models.Organization + result2 error + }{result1, result2} +} + +func (fake *FakeOrganizationRepository) Create(org models.Organization) (apiErr error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + org models.Organization + }{org}) + if fake.CreateStub != nil { + return fake.CreateStub(org) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeOrganizationRepository) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeOrganizationRepository) CreateArgsForCall(i int) models.Organization { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].org +} + +func (fake *FakeOrganizationRepository) CreateReturns(result1 error) { + fake.CreateStub = nil + fake.createReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeOrganizationRepository) Rename(orgGuid string, name string) (apiErr error) { + fake.renameMutex.Lock() + defer fake.renameMutex.Unlock() + fake.renameArgsForCall = append(fake.renameArgsForCall, struct { + orgGuid string + name string + }{orgGuid, name}) + if fake.RenameStub != nil { + return fake.RenameStub(orgGuid, name) + } else { + return fake.renameReturns.result1 + } +} + +func (fake *FakeOrganizationRepository) RenameCallCount() int { + fake.renameMutex.RLock() + defer fake.renameMutex.RUnlock() + return len(fake.renameArgsForCall) +} + +func (fake *FakeOrganizationRepository) RenameArgsForCall(i int) (string, string) { + fake.renameMutex.RLock() + defer fake.renameMutex.RUnlock() + return fake.renameArgsForCall[i].orgGuid, fake.renameArgsForCall[i].name +} + +func (fake *FakeOrganizationRepository) RenameReturns(result1 error) { + fake.RenameStub = nil + fake.renameReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeOrganizationRepository) Delete(orgGuid string) (apiErr error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + orgGuid string + }{orgGuid}) + if fake.DeleteStub != nil { + return fake.DeleteStub(orgGuid) + } else { + return fake.deleteReturns.result1 + } +} + +func (fake *FakeOrganizationRepository) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeOrganizationRepository) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].orgGuid +} + +func (fake *FakeOrganizationRepository) DeleteReturns(result1 error) { + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +var _ organizations.OrganizationRepository = new(FakeOrganizationRepository) diff --git a/cf/api/organizations/organizations.go b/cf/api/organizations/organizations.go new file mode 100644 index 00000000000..6a84767b5e8 --- /dev/null +++ b/cf/api/organizations/organizations.go @@ -0,0 +1,89 @@ +package organizations + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type OrganizationRepository interface { + ListOrgs() (orgs []models.Organization, apiErr error) + FindByName(name string) (org models.Organization, apiErr error) + Create(org models.Organization) (apiErr error) + Rename(orgGuid string, name string) (apiErr error) + Delete(orgGuid string) (apiErr error) +} + +type CloudControllerOrganizationRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerOrganizationRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerOrganizationRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerOrganizationRepository) ListOrgs() ([]models.Organization, error) { + orgs := []models.Organization{} + err := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + "/v2/organizations", + resources.OrganizationResource{}, + func(resource interface{}) bool { + orgResource, ok := resource.(resources.OrganizationResource) + if ok { + orgs = append(orgs, orgResource.ToModel()) + return true + } else { + return false + } + }) + return orgs, err +} + +func (repo CloudControllerOrganizationRepository) FindByName(name string) (org models.Organization, apiErr error) { + found := false + apiErr = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/organizations?q=%s&inline-relations-depth=1", url.QueryEscape("name:"+strings.ToLower(name))), + resources.OrganizationResource{}, + func(resource interface{}) bool { + org = resource.(resources.OrganizationResource).ToModel() + found = true + return false + }) + + if apiErr == nil && !found { + apiErr = errors.NewModelNotFoundError("Organization", name) + } + + return +} + +func (repo CloudControllerOrganizationRepository) Create(org models.Organization) (apiErr error) { + data := fmt.Sprintf(`{"name":"%s"`, org.Name) + if org.QuotaDefinition.Guid != "" { + data = data + fmt.Sprintf(`, "quota_definition_guid":"%s"`, org.QuotaDefinition.Guid) + } + data = data + "}" + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), "/v2/organizations", strings.NewReader(data)) +} + +func (repo CloudControllerOrganizationRepository) Rename(orgGuid string, name string) (apiErr error) { + url := fmt.Sprintf("/v2/organizations/%s", orgGuid) + data := fmt.Sprintf(`{"name":"%s"}`, name) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), url, strings.NewReader(data)) +} + +func (repo CloudControllerOrganizationRepository) Delete(orgGuid string) (apiErr error) { + url := fmt.Sprintf("/v2/organizations/%s?recursive=true", orgGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), url) +} diff --git a/cf/api/organizations/organizations_suite_test.go b/cf/api/organizations/organizations_suite_test.go new file mode 100644 index 00000000000..098a677aa4b --- /dev/null +++ b/cf/api/organizations/organizations_suite_test.go @@ -0,0 +1,19 @@ +package organizations_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestOrganizations(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Organizations Suite") +} diff --git a/cf/api/organizations/organizations_test.go b/cf/api/organizations/organizations_test.go new file mode 100644 index 00000000000..d98482cfb83 --- /dev/null +++ b/cf/api/organizations/organizations_test.go @@ -0,0 +1,263 @@ +package organizations_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/organizations" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Organization Repository", func() { + Describe("listing organizations", func() { + It("lists the orgs from the the /v2/orgs endpoint", func() { + firstPageOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ + "next_url": "/v2/organizations?page=2", + "resources": [ + { + "metadata": { "guid": "org1-guid" }, + "entity": { "name": "Org1" } + }, + { + "metadata": { "guid": "org2-guid" }, + "entity": { "name": "Org2" } + } + ]}`}, + }) + + secondPageOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations?page=2", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ + { + "metadata": { "guid": "org3-guid" }, + "entity": { "name": "Org3" } + } + ]}`}, + }) + + testserver, handler, repo := createOrganizationRepo(firstPageOrgsRequest, secondPageOrgsRequest) + defer testserver.Close() + + orgs := []models.Organization{} + orgs, apiErr := repo.ListOrgs() + + Expect(len(orgs)).To(Equal(3)) + Expect(orgs[0].Guid).To(Equal("org1-guid")) + Expect(orgs[1].Guid).To(Equal("org2-guid")) + Expect(orgs[2].Guid).To(Equal("org3-guid")) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + It("does not call the provided function when there are no orgs found", func() { + emptyOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + }) + + testserver, handler, repo := createOrganizationRepo(emptyOrgsRequest) + defer testserver.Close() + + _, apiErr := repo.ListOrgs() + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("finding organizations by name", func() { + It("returns the org with that name", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations?q=name%3Aorg1&inline-relations-depth=1", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [{ + "metadata": { "guid": "org1-guid" }, + "entity": { + "name": "Org1", + "quota_definition": { + "entity": { + "name": "not-your-average-quota", + "memory_limit": 128 + } + }, + "spaces": [{ + "metadata": { "guid": "space1-guid" }, + "entity": { "name": "Space1" } + }], + "domains": [{ + "metadata": { "guid": "domain1-guid" }, + "entity": { "name": "cfapps.io" } + }], + "space_quota_definitions":[{ + "metadata": {"guid": "space-quota1-guid"}, + "entity": {"name": "space-quota1"} + }] + } + }]}`}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + existingOrg := models.Organization{} + existingOrg.Guid = "org1-guid" + existingOrg.Name = "Org1" + + org, apiErr := repo.FindByName("Org1") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(org.Name).To(Equal(existingOrg.Name)) + Expect(org.Guid).To(Equal(existingOrg.Guid)) + Expect(org.QuotaDefinition.Name).To(Equal("not-your-average-quota")) + Expect(org.QuotaDefinition.MemoryLimit).To(Equal(int64(128))) + Expect(len(org.Spaces)).To(Equal(1)) + Expect(org.Spaces[0].Name).To(Equal("Space1")) + Expect(org.Spaces[0].Guid).To(Equal("space1-guid")) + Expect(len(org.Domains)).To(Equal(1)) + Expect(org.Domains[0].Name).To(Equal("cfapps.io")) + Expect(org.Domains[0].Guid).To(Equal("domain1-guid")) + Expect(len(org.SpaceQuotas)).To(Equal(1)) + Expect(org.SpaceQuotas[0].Name).To(Equal("space-quota1")) + Expect(org.SpaceQuotas[0].Guid).To(Equal("space-quota1-guid")) + }) + + It("returns a ModelNotFoundError when the org cannot be found", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations?q=name%3Aorg1&inline-relations-depth=1", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + + _, apiErr := repo.FindByName("org1") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + + It("returns an api error when the response is not successful", func() { + requestHandler := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations?q=name%3Aorg1&inline-relations-depth=1", + Response: testnet.TestResponse{Status: http.StatusBadGateway, Body: `{"resources": []}`}, + }) + + testserver, handler, repo := createOrganizationRepo(requestHandler) + defer testserver.Close() + + _, apiErr := repo.FindByName("org1") + _, ok := apiErr.(*errors.ModelNotFoundError) + Expect(ok).To(BeFalse()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".Create", func() { + It("creates the org and sends only the org name if the quota flag is not provided", func() { + org := models.Organization{ + OrganizationFields: models.OrganizationFields{ + Name: "my-org", + }} + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/organizations", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-org"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + + apiErr := repo.Create(org) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("creates the org with the provided quota", func() { + org := models.Organization{ + OrganizationFields: models.OrganizationFields{ + Name: "my-org", + QuotaDefinition: models.QuotaFields{ + Guid: "my-quota-guid", + }, + }} + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/organizations", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-org", "quota_definition_guid":"my-quota-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + + apiErr := repo.Create(org) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("renaming orgs", func() { + It("renames the org with the given guid", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/organizations/my-org-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-new-org"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + + apiErr := repo.Rename("my-org-guid", "my-new-org") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("deleting orgs", func() { + It("deletes the org with the given guid", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/organizations/my-org-guid?recursive=true", + Response: testnet.TestResponse{Status: http.StatusOK}, + }) + + testserver, handler, repo := createOrganizationRepo(req) + defer testserver.Close() + + apiErr := repo.Delete("my-org-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) +}) + +func createOrganizationRepo(reqs ...testnet.TestRequest) (testserver *httptest.Server, handler *testnet.TestHandler, repo OrganizationRepository) { + testserver, handler = testnet.NewServer(reqs) + + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(testserver.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerOrganizationRepository(configRepo, gateway) + return +} diff --git a/cf/api/password/password.go b/cf/api/password/password.go new file mode 100644 index 00000000000..2e5f8ee97ca --- /dev/null +++ b/cf/api/password/password.go @@ -0,0 +1,38 @@ +package password + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/net" +) + +type PasswordRepository interface { + UpdatePassword(old string, new string) error +} + +type CloudControllerPasswordRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerPasswordRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerPasswordRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerPasswordRepository) UpdatePassword(old string, new string) error { + uaaEndpoint := repo.config.UaaEndpoint() + if uaaEndpoint == "" { + return errors.New(T("UAA endpoint missing from config file")) + } + + url := fmt.Sprintf("/Users/%s/password", repo.config.UserGuid()) + body := fmt.Sprintf(`{"password":"%s","oldPassword":"%s"}`, new, old) + + return repo.gateway.UpdateResource(uaaEndpoint, url, strings.NewReader(body)) +} diff --git a/cf/api/password/password_suite_test.go b/cf/api/password/password_suite_test.go new file mode 100644 index 00000000000..f2d67b45076 --- /dev/null +++ b/cf/api/password/password_suite_test.go @@ -0,0 +1,19 @@ +package password_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPassword(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Password Suite") +} diff --git a/cf/api/password/password_test.go b/cf/api/password/password_test.go new file mode 100644 index 00000000000..6001d758d41 --- /dev/null +++ b/cf/api/password/password_test.go @@ -0,0 +1,46 @@ +package password_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/password" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerPasswordRepository", func() { + It("updates your password", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/Users/my-user-guid/password", + Matcher: testnet.RequestBodyMatcher(`{"password":"new-password","oldPassword":"old-password"}`), + Response: testnet.TestResponse{Status: http.StatusOK}, + }) + + passwordUpdateServer, handler, repo := createPasswordRepo(req) + defer passwordUpdateServer.Close() + + apiErr := repo.UpdatePassword("old-password", "new-password") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) +}) + +func createPasswordRepo(req testnet.TestRequest) (passwordServer *httptest.Server, handler *testnet.TestHandler, repo PasswordRepository) { + passwordServer, handler = testnet.NewServer([]testnet.TestRequest{req}) + + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetUaaEndpoint(passwordServer.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerPasswordRepository(configRepo, gateway) + return +} diff --git a/cf/api/quotas/fakes/fake_quota_repository.go b/cf/api/quotas/fakes/fake_quota_repository.go new file mode 100644 index 00000000000..b296f8e481e --- /dev/null +++ b/cf/api/quotas/fakes/fake_quota_repository.go @@ -0,0 +1,245 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/quotas" + + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeQuotaRepository struct { + FindAllStub func() (quotas []models.QuotaFields, apiErr error) + findAllMutex sync.RWMutex + findAllArgsForCall []struct{} + findAllReturns struct { + result1 []models.QuotaFields + result2 error + } + FindByNameStub func(name string) (quota models.QuotaFields, apiErr error) + findByNameMutex sync.RWMutex + findByNameArgsForCall []struct { + name string + } + findByNameReturns struct { + result1 models.QuotaFields + result2 error + } + AssignQuotaToOrgStub func(orgGuid, quotaGuid string) error + assignQuotaToOrgMutex sync.RWMutex + assignQuotaToOrgArgsForCall []struct { + orgGuid string + quotaGuid string + } + assignQuotaToOrgReturns struct { + result1 error + } + CreateStub func(quota models.QuotaFields) error + createMutex sync.RWMutex + createArgsForCall []struct { + quota models.QuotaFields + } + createReturns struct { + result1 error + } + UpdateStub func(quota models.QuotaFields) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + quota models.QuotaFields + } + updateReturns struct { + result1 error + } + DeleteStub func(quotaGuid string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + quotaGuid string + } + deleteReturns struct { + result1 error + } +} + +func (fake *FakeQuotaRepository) FindAll() (quotas []models.QuotaFields, apiErr error) { + fake.findAllMutex.Lock() + defer fake.findAllMutex.Unlock() + fake.findAllArgsForCall = append(fake.findAllArgsForCall, struct{}{}) + if fake.FindAllStub != nil { + return fake.FindAllStub() + } else { + return fake.findAllReturns.result1, fake.findAllReturns.result2 + } +} + +func (fake *FakeQuotaRepository) FindAllCallCount() int { + fake.findAllMutex.RLock() + defer fake.findAllMutex.RUnlock() + return len(fake.findAllArgsForCall) +} + +func (fake *FakeQuotaRepository) FindAllReturns(result1 []models.QuotaFields, result2 error) { + fake.findAllReturns = struct { + result1 []models.QuotaFields + result2 error + }{result1, result2} +} + +func (fake *FakeQuotaRepository) FindByName(name string) (quota models.QuotaFields, apiErr error) { + fake.findByNameMutex.Lock() + defer fake.findByNameMutex.Unlock() + fake.findByNameArgsForCall = append(fake.findByNameArgsForCall, struct { + name string + }{name}) + if fake.FindByNameStub != nil { + return fake.FindByNameStub(name) + } else { + return fake.findByNameReturns.result1, fake.findByNameReturns.result2 + } +} + +func (fake *FakeQuotaRepository) FindByNameCallCount() int { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return len(fake.findByNameArgsForCall) +} + +func (fake *FakeQuotaRepository) FindByNameArgsForCall(i int) string { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return fake.findByNameArgsForCall[i].name +} + +func (fake *FakeQuotaRepository) FindByNameReturns(result1 models.QuotaFields, result2 error) { + fake.findByNameReturns = struct { + result1 models.QuotaFields + result2 error + }{result1, result2} +} + +func (fake *FakeQuotaRepository) AssignQuotaToOrg(orgGuid string, quotaGuid string) error { + fake.assignQuotaToOrgMutex.Lock() + defer fake.assignQuotaToOrgMutex.Unlock() + fake.assignQuotaToOrgArgsForCall = append(fake.assignQuotaToOrgArgsForCall, struct { + orgGuid string + quotaGuid string + }{orgGuid, quotaGuid}) + if fake.AssignQuotaToOrgStub != nil { + return fake.AssignQuotaToOrgStub(orgGuid, quotaGuid) + } else { + return fake.assignQuotaToOrgReturns.result1 + } +} + +func (fake *FakeQuotaRepository) AssignQuotaToOrgCallCount() int { + fake.assignQuotaToOrgMutex.RLock() + defer fake.assignQuotaToOrgMutex.RUnlock() + return len(fake.assignQuotaToOrgArgsForCall) +} + +func (fake *FakeQuotaRepository) AssignQuotaToOrgArgsForCall(i int) (string, string) { + fake.assignQuotaToOrgMutex.RLock() + defer fake.assignQuotaToOrgMutex.RUnlock() + return fake.assignQuotaToOrgArgsForCall[i].orgGuid, fake.assignQuotaToOrgArgsForCall[i].quotaGuid +} + +func (fake *FakeQuotaRepository) AssignQuotaToOrgReturns(result1 error) { + fake.assignQuotaToOrgReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeQuotaRepository) Create(quota models.QuotaFields) error { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + quota models.QuotaFields + }{quota}) + if fake.CreateStub != nil { + return fake.CreateStub(quota) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeQuotaRepository) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeQuotaRepository) CreateArgsForCall(i int) models.QuotaFields { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].quota +} + +func (fake *FakeQuotaRepository) CreateReturns(result1 error) { + fake.createReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeQuotaRepository) Update(quota models.QuotaFields) error { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + quota models.QuotaFields + }{quota}) + if fake.UpdateStub != nil { + return fake.UpdateStub(quota) + } else { + return fake.updateReturns.result1 + } +} + +func (fake *FakeQuotaRepository) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeQuotaRepository) UpdateArgsForCall(i int) models.QuotaFields { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return fake.updateArgsForCall[i].quota +} + +func (fake *FakeQuotaRepository) UpdateReturns(result1 error) { + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeQuotaRepository) Delete(quotaGuid string) error { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + quotaGuid string + }{quotaGuid}) + if fake.DeleteStub != nil { + return fake.DeleteStub(quotaGuid) + } else { + return fake.deleteReturns.result1 + } +} + +func (fake *FakeQuotaRepository) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeQuotaRepository) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].quotaGuid +} + +func (fake *FakeQuotaRepository) DeleteReturns(result1 error) { + fake.deleteReturns = struct { + result1 error + }{result1} +} + +var _ QuotaRepository = new(FakeQuotaRepository) diff --git a/cf/api/quotas/quotas.go b/cf/api/quotas/quotas.go new file mode 100644 index 00000000000..8ac2db63fff --- /dev/null +++ b/cf/api/quotas/quotas.go @@ -0,0 +1,91 @@ +package quotas + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type QuotaRepository interface { + FindAll() (quotas []models.QuotaFields, apiErr error) + FindByName(name string) (quota models.QuotaFields, apiErr error) + + AssignQuotaToOrg(orgGuid, quotaGuid string) error + + // CRUD ahoy + Create(quota models.QuotaFields) error + Update(quota models.QuotaFields) error + Delete(quotaGuid string) error +} + +type CloudControllerQuotaRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerQuotaRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerQuotaRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerQuotaRepository) findAllWithPath(path string) ([]models.QuotaFields, error) { + var quotas []models.QuotaFields + apiErr := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.QuotaResource{}, + func(resource interface{}) bool { + if qr, ok := resource.(resources.QuotaResource); ok { + quotas = append(quotas, qr.ToFields()) + } + return true + }) + return quotas, apiErr +} + +func (repo CloudControllerQuotaRepository) FindAll() (quotas []models.QuotaFields, apiErr error) { + return repo.findAllWithPath("/v2/quota_definitions") +} + +func (repo CloudControllerQuotaRepository) FindByName(name string) (quota models.QuotaFields, apiErr error) { + path := fmt.Sprintf("/v2/quota_definitions?q=%s", url.QueryEscape("name:"+name)) + quotas, apiErr := repo.findAllWithPath(path) + if apiErr != nil { + return + } + + if len(quotas) == 0 { + apiErr = errors.NewModelNotFoundError("Quota", name) + return + } + + quota = quotas[0] + return +} + +func (repo CloudControllerQuotaRepository) Create(quota models.QuotaFields) error { + return repo.gateway.CreateResourceFromStruct(repo.config.ApiEndpoint(), "/v2/quota_definitions", quota) +} + +func (repo CloudControllerQuotaRepository) Update(quota models.QuotaFields) error { + path := fmt.Sprintf("/v2/quota_definitions/%s", quota.Guid) + return repo.gateway.UpdateResourceFromStruct(repo.config.ApiEndpoint(), path, quota) +} + +func (repo CloudControllerQuotaRepository) AssignQuotaToOrg(orgGuid, quotaGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/organizations/%s", orgGuid) + data := fmt.Sprintf(`{"quota_definition_guid":"%s"}`, quotaGuid) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(data)) +} + +func (repo CloudControllerQuotaRepository) Delete(quotaGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/quota_definitions/%s", quotaGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/quotas/quotas_suite_test.go b/cf/api/quotas/quotas_suite_test.go new file mode 100644 index 00000000000..ee4f65f6ebd --- /dev/null +++ b/cf/api/quotas/quotas_suite_test.go @@ -0,0 +1,19 @@ +package quotas_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestQuotas(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Quotas Suite") +} diff --git a/cf/api/quotas/quotas_test.go b/cf/api/quotas/quotas_test.go new file mode 100644 index 00000000000..4fb59a24d55 --- /dev/null +++ b/cf/api/quotas/quotas_test.go @@ -0,0 +1,221 @@ +package quotas_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/quotas" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerQuotaRepository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerQuotaRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerQuotaRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe("FindByName", func() { + BeforeEach(func() { + setupTestServer(firstQuotaRequest, secondQuotaRequest) + }) + + It("Finds Quota definitions by name", func() { + quota, err := repo.FindByName("my-remote-quota") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(quota).To(Equal(models.QuotaFields{ + Guid: "my-quota-guid", + Name: "my-remote-quota", + MemoryLimit: 1024, + InstanceMemoryLimit: -1, + RoutesLimit: 123, + ServicesLimit: 321, + NonBasicServicesAllowed: true, + })) + }) + }) + + Describe("FindAll", func() { + BeforeEach(func() { + setupTestServer(firstQuotaRequest, secondQuotaRequest) + }) + + It("finds all Quota definitions", func() { + quotas, err := repo.FindAll() + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(quotas)).To(Equal(3)) + Expect(quotas[0].Guid).To(Equal("my-quota-guid")) + Expect(quotas[0].Name).To(Equal("my-remote-quota")) + Expect(quotas[0].MemoryLimit).To(Equal(int64(1024))) + Expect(quotas[0].RoutesLimit).To(Equal(123)) + Expect(quotas[0].ServicesLimit).To(Equal(321)) + + Expect(quotas[1].Guid).To(Equal("my-quota-guid2")) + Expect(quotas[2].Guid).To(Equal("my-quota-guid3")) + }) + }) + + Describe("AssignQuotaToOrg", func() { + It("sets the quota for an organization", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/organizations/my-org-guid", + Matcher: testnet.RequestBodyMatcher(`{"quota_definition_guid":"my-quota-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + setupTestServer(req) + + err := repo.AssignQuotaToOrg("my-org-guid", "my-quota-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Create", func() { + It("creates a new quota with the given name", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/quota_definitions", + Matcher: testnet.RequestBodyMatcher(`{ + "name": "not-so-strict", + "non_basic_services_allowed": false, + "total_services": 1, + "total_routes": 12, + "memory_limit": 123, + "instance_memory_limit": 0 + }`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + setupTestServer(req) + + quota := models.QuotaFields{ + Name: "not-so-strict", + ServicesLimit: 1, + RoutesLimit: 12, + MemoryLimit: 123, + } + err := repo.Create(quota) + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("Update", func() { + It("updates an existing quota", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/quota_definitions/my-quota-guid", + Matcher: testnet.RequestBodyMatcher(`{ + "guid": "my-quota-guid", + "non_basic_services_allowed": false, + "name": "amazing-quota", + "total_services": 1, + "total_routes": 12, + "memory_limit": 123, + "instance_memory_limit": 0 + }`), + })) + + quota := models.QuotaFields{ + Guid: "my-quota-guid", + Name: "amazing-quota", + ServicesLimit: 1, + RoutesLimit: 12, + MemoryLimit: 123, + } + + err := repo.Update(quota) + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("Delete", func() { + It("deletes the quota with the given name", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/quota_definitions/my-quota-guid", + Response: testnet.TestResponse{Status: http.StatusNoContent}, + }) + setupTestServer(req) + + err := repo.Delete("my-quota-guid") + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) +}) + +var firstQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/quota_definitions", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/quota_definitions?page=2", + "resources": [ + { + "metadata": { "guid": "my-quota-guid" }, + "entity": { + "name": "my-remote-quota", + "memory_limit": 1024, + "instance_memory_limit": -1, + "total_routes": 123, + "total_services": 321, + "non_basic_services_allowed": true + } + } + ]}`, + }, +}) + +var secondQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/quota_definitions?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "resources": [ + { + "metadata": { "guid": "my-quota-guid2" }, + "entity": { "name": "my-remote-quota2", "memory_limit": 1024 } + }, + { + "metadata": { "guid": "my-quota-guid3" }, + "entity": { "name": "my-remote-quota3", "memory_limit": 1024 } + } + ]}`, + }, +}) diff --git a/cf/api/repository_locator.go b/cf/api/repository_locator.go new file mode 100644 index 00000000000..a960d847df3 --- /dev/null +++ b/cf/api/repository_locator.go @@ -0,0 +1,270 @@ +package api + +import ( + "crypto/tls" + "net/http" + + "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/api/organizations" + + "github.com/cloudfoundry/cli/cf/api/app_events" + api_app_files "github.com/cloudfoundry/cli/cf/api/app_files" + "github.com/cloudfoundry/cli/cf/api/app_instances" + "github.com/cloudfoundry/cli/cf/api/application_bits" + applications "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/api/copy_application_source" + "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/api/password" + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + securitygroupspaces "github.com/cloudfoundry/cli/cf/api/security_groups/spaces" + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/api/spaces" + stacks "github.com/cloudfoundry/cli/cf/api/stacks" + "github.com/cloudfoundry/cli/cf/api/strategy" + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/cli/cf/terminal" + consumer "github.com/cloudfoundry/loggregator_consumer" +) + +type RepositoryLocator struct { + authRepo authentication.AuthenticationRepository + curlRepo CurlRepository + endpointRepo RemoteEndpointRepository + organizationRepo organizations.CloudControllerOrganizationRepository + quotaRepo quotas.CloudControllerQuotaRepository + spaceRepo spaces.CloudControllerSpaceRepository + appRepo applications.CloudControllerApplicationRepository + appBitsRepo application_bits.CloudControllerApplicationBitsRepository + appSummaryRepo CloudControllerAppSummaryRepository + appInstancesRepo app_instances.CloudControllerAppInstancesRepository + appEventsRepo app_events.CloudControllerAppEventsRepository + appFilesRepo api_app_files.CloudControllerAppFilesRepository + domainRepo CloudControllerDomainRepository + routeRepo CloudControllerRouteRepository + stackRepo stacks.CloudControllerStackRepository + serviceRepo CloudControllerServiceRepository + serviceBindingRepo CloudControllerServiceBindingRepository + serviceSummaryRepo CloudControllerServiceSummaryRepository + userRepo CloudControllerUserRepository + passwordRepo password.CloudControllerPasswordRepository + logsRepo LogsRepository + authTokenRepo CloudControllerServiceAuthTokenRepository + serviceBrokerRepo CloudControllerServiceBrokerRepository + servicePlanRepo CloudControllerServicePlanRepository + servicePlanVisibilityRepo ServicePlanVisibilityRepository + userProvidedServiceInstanceRepo CCUserProvidedServiceInstanceRepository + buildpackRepo CloudControllerBuildpackRepository + buildpackBitsRepo CloudControllerBuildpackBitsRepository + securityGroupRepo security_groups.SecurityGroupRepo + stagingSecurityGroupRepo staging.StagingSecurityGroupsRepo + runningSecurityGroupRepo running.RunningSecurityGroupsRepo + securityGroupSpaceBinder securitygroupspaces.SecurityGroupSpaceBinder + spaceQuotaRepo space_quotas.SpaceQuotaRepository + featureFlagRepo feature_flags.FeatureFlagRepository + environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository + copyAppSourceRepo copy_application_source.CopyApplicationSourceRepository +} + +func NewRepositoryLocator(config core_config.ReadWriter, gatewaysByName map[string]net.Gateway) (loc RepositoryLocator) { + strategy := strategy.NewEndpointStrategy(config.ApiVersion()) + + authGateway := gatewaysByName["auth"] + cloudControllerGateway := gatewaysByName["cloud-controller"] + uaaGateway := gatewaysByName["uaa"] + loc.authRepo = authentication.NewUAAAuthenticationRepository(authGateway, config) + + // ensure gateway refreshers are set before passing them by value to repositories + cloudControllerGateway.SetTokenRefresher(loc.authRepo) + uaaGateway.SetTokenRefresher(loc.authRepo) + + tlsConfig := net.NewTLSConfig([]tls.Certificate{}, config.IsSSLDisabled()) + loggregatorConsumer := consumer.New(config.LoggregatorEndpoint(), tlsConfig, http.ProxyFromEnvironment) + loggregatorConsumer.SetDebugPrinter(terminal.DebugPrinter{}) + + loc.appBitsRepo = application_bits.NewCloudControllerApplicationBitsRepository(config, cloudControllerGateway) + loc.appEventsRepo = app_events.NewCloudControllerAppEventsRepository(config, cloudControllerGateway, strategy) + loc.appFilesRepo = api_app_files.NewCloudControllerAppFilesRepository(config, cloudControllerGateway) + loc.appRepo = applications.NewCloudControllerApplicationRepository(config, cloudControllerGateway) + loc.appSummaryRepo = NewCloudControllerAppSummaryRepository(config, cloudControllerGateway) + loc.appInstancesRepo = app_instances.NewCloudControllerAppInstancesRepository(config, cloudControllerGateway) + loc.authTokenRepo = NewCloudControllerServiceAuthTokenRepository(config, cloudControllerGateway) + loc.curlRepo = NewCloudControllerCurlRepository(config, cloudControllerGateway) + loc.domainRepo = NewCloudControllerDomainRepository(config, cloudControllerGateway, strategy) + loc.endpointRepo = NewEndpointRepository(config, cloudControllerGateway) + loc.logsRepo = NewLoggregatorLogsRepository(config, loggregatorConsumer, loc.authRepo) + loc.organizationRepo = organizations.NewCloudControllerOrganizationRepository(config, cloudControllerGateway) + loc.passwordRepo = password.NewCloudControllerPasswordRepository(config, uaaGateway) + loc.quotaRepo = quotas.NewCloudControllerQuotaRepository(config, cloudControllerGateway) + loc.routeRepo = NewCloudControllerRouteRepository(config, cloudControllerGateway) + loc.stackRepo = stacks.NewCloudControllerStackRepository(config, cloudControllerGateway) + loc.serviceRepo = NewCloudControllerServiceRepository(config, cloudControllerGateway) + loc.serviceBindingRepo = NewCloudControllerServiceBindingRepository(config, cloudControllerGateway) + loc.serviceBrokerRepo = NewCloudControllerServiceBrokerRepository(config, cloudControllerGateway) + loc.servicePlanRepo = NewCloudControllerServicePlanRepository(config, cloudControllerGateway) + loc.servicePlanVisibilityRepo = NewCloudControllerServicePlanVisibilityRepository(config, cloudControllerGateway) + loc.serviceSummaryRepo = NewCloudControllerServiceSummaryRepository(config, cloudControllerGateway) + loc.spaceRepo = spaces.NewCloudControllerSpaceRepository(config, cloudControllerGateway) + loc.userProvidedServiceInstanceRepo = NewCCUserProvidedServiceInstanceRepository(config, cloudControllerGateway) + loc.userRepo = NewCloudControllerUserRepository(config, uaaGateway, cloudControllerGateway) + loc.buildpackRepo = NewCloudControllerBuildpackRepository(config, cloudControllerGateway) + loc.buildpackBitsRepo = NewCloudControllerBuildpackBitsRepository(config, cloudControllerGateway, app_files.ApplicationZipper{}) + loc.securityGroupRepo = security_groups.NewSecurityGroupRepo(config, cloudControllerGateway) + loc.stagingSecurityGroupRepo = staging.NewStagingSecurityGroupsRepo(config, cloudControllerGateway) + loc.runningSecurityGroupRepo = running.NewRunningSecurityGroupsRepo(config, cloudControllerGateway) + loc.securityGroupSpaceBinder = securitygroupspaces.NewSecurityGroupSpaceBinder(config, cloudControllerGateway) + loc.spaceQuotaRepo = space_quotas.NewCloudControllerSpaceQuotaRepository(config, cloudControllerGateway) + loc.featureFlagRepo = feature_flags.NewCloudControllerFeatureFlagRepository(config, cloudControllerGateway) + loc.environmentVariableGroupRepo = environment_variable_groups.NewCloudControllerEnvironmentVariableGroupsRepository(config, cloudControllerGateway) + loc.copyAppSourceRepo = copy_application_source.NewCloudControllerCopyApplicationSourceRepository(config, cloudControllerGateway) + return +} + +func (locator RepositoryLocator) GetAuthenticationRepository() authentication.AuthenticationRepository { + return locator.authRepo +} + +func (locator RepositoryLocator) GetCurlRepository() CurlRepository { + return locator.curlRepo +} + +func (locator RepositoryLocator) GetEndpointRepository() EndpointRepository { + return locator.endpointRepo +} + +func (locator RepositoryLocator) GetOrganizationRepository() organizations.OrganizationRepository { + return locator.organizationRepo +} + +func (locator RepositoryLocator) GetQuotaRepository() quotas.QuotaRepository { + return locator.quotaRepo +} + +func (locator RepositoryLocator) GetSpaceRepository() spaces.SpaceRepository { + return locator.spaceRepo +} + +func (locator RepositoryLocator) GetApplicationRepository() applications.ApplicationRepository { + return locator.appRepo +} + +func (locator RepositoryLocator) GetApplicationBitsRepository() application_bits.ApplicationBitsRepository { + return locator.appBitsRepo +} + +func (locator RepositoryLocator) GetAppSummaryRepository() AppSummaryRepository { + return locator.appSummaryRepo +} + +func (locator RepositoryLocator) GetAppInstancesRepository() app_instances.AppInstancesRepository { + return locator.appInstancesRepo +} + +func (locator RepositoryLocator) GetAppEventsRepository() app_events.AppEventsRepository { + return locator.appEventsRepo +} + +func (locator RepositoryLocator) GetAppFilesRepository() api_app_files.AppFilesRepository { + return locator.appFilesRepo +} + +func (locator RepositoryLocator) GetDomainRepository() DomainRepository { + return locator.domainRepo +} + +func (locator RepositoryLocator) GetRouteRepository() RouteRepository { + return locator.routeRepo +} + +func (locator RepositoryLocator) GetStackRepository() stacks.StackRepository { + return locator.stackRepo +} + +func (locator RepositoryLocator) GetServiceRepository() ServiceRepository { + return locator.serviceRepo +} + +func (locator RepositoryLocator) GetServiceBindingRepository() ServiceBindingRepository { + return locator.serviceBindingRepo +} + +func (locator RepositoryLocator) GetServiceSummaryRepository() ServiceSummaryRepository { + return locator.serviceSummaryRepo +} + +func (locator RepositoryLocator) GetUserRepository() UserRepository { + return locator.userRepo +} + +func (locator RepositoryLocator) GetPasswordRepository() password.PasswordRepository { + return locator.passwordRepo +} + +func (locator RepositoryLocator) GetLogsRepository() LogsRepository { + return locator.logsRepo +} + +func (locator RepositoryLocator) GetServiceAuthTokenRepository() ServiceAuthTokenRepository { + return locator.authTokenRepo +} + +func (locator RepositoryLocator) GetServiceBrokerRepository() ServiceBrokerRepository { + return locator.serviceBrokerRepo +} + +func (locator RepositoryLocator) GetServicePlanRepository() ServicePlanRepository { + return locator.servicePlanRepo +} + +func (locator RepositoryLocator) GetUserProvidedServiceInstanceRepository() UserProvidedServiceInstanceRepository { + return locator.userProvidedServiceInstanceRepo +} + +func (locator RepositoryLocator) GetBuildpackRepository() BuildpackRepository { + return locator.buildpackRepo +} + +func (locator RepositoryLocator) GetBuildpackBitsRepository() BuildpackBitsRepository { + return locator.buildpackBitsRepo +} + +func (locator RepositoryLocator) GetSecurityGroupRepository() security_groups.SecurityGroupRepo { + return locator.securityGroupRepo +} + +func (locator RepositoryLocator) GetStagingSecurityGroupsRepository() staging.StagingSecurityGroupsRepo { + return locator.stagingSecurityGroupRepo +} + +func (locator RepositoryLocator) GetRunningSecurityGroupsRepository() running.RunningSecurityGroupsRepo { + return locator.runningSecurityGroupRepo +} + +func (locator RepositoryLocator) GetSecurityGroupSpaceBinder() securitygroupspaces.SecurityGroupSpaceBinder { + return locator.securityGroupSpaceBinder +} + +func (locator RepositoryLocator) GetServicePlanVisibilityRepository() ServicePlanVisibilityRepository { + return locator.servicePlanVisibilityRepo +} + +func (locator RepositoryLocator) GetSpaceQuotaRepository() space_quotas.SpaceQuotaRepository { + return locator.spaceQuotaRepo +} + +func (locator RepositoryLocator) GetFeatureFlagRepository() feature_flags.FeatureFlagRepository { + return locator.featureFlagRepo +} + +func (locator RepositoryLocator) GetEnvironmentVariableGroupsRepository() environment_variable_groups.EnvironmentVariableGroupsRepository { + return locator.environmentVariableGroupRepo +} + +func (locator RepositoryLocator) GetCopyApplicationSourceRepository() copy_application_source.CopyApplicationSourceRepository { + return locator.copyAppSourceRepo +} diff --git a/cf/api/resource.go b/cf/api/resource.go new file mode 100644 index 00000000000..778f64ec17c --- /dev/null +++ b/cf/api/resource.go @@ -0,0 +1 @@ +package api diff --git a/cf/api/resources/applications.go b/cf/api/resources/applications.go new file mode 100644 index 00000000000..e8fbed7548f --- /dev/null +++ b/cf/api/resources/applications.go @@ -0,0 +1,141 @@ +package resources + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/models" +) + +type PaginatedApplicationResources struct { + Resources []ApplicationResource +} + +type AppRouteEntity struct { + Host string + Domain struct { + Resource + Entity struct { + Name string + } + } +} + +type AppRouteResource struct { + Resource + Entity AppRouteEntity +} + +type AppFileResource struct { + Path string `json:"fn"` + Sha1 string `json:"sha1"` + Size int64 `json:"size"` +} + +type ApplicationResource struct { + Resource + Entity ApplicationEntity +} + +type ApplicationEntity struct { + Name *string `json:"name,omitempty"` + Command *string `json:"command,omitempty"` + DetectedStartCommand *string `json:"detected_start_command,omitempty"` + State *string `json:"state,omitempty"` + SpaceGuid *string `json:"space_guid,omitempty"` + Instances *int `json:"instances,omitempty"` + Memory *int64 `json:"memory,omitempty"` + DiskQuota *int64 `json:"disk_quota,omitempty"` + StackGuid *string `json:"stack_guid,omitempty"` + Stack *StackResource `json:"stack,omitempty"` + Routes *[]AppRouteResource `json:"routes,omitempty"` + Buildpack *string `json:"buildpack,omitempty"` + EnvironmentJson *map[string]interface{} `json:"environment_json,omitempty"` + HealthCheckTimeout *int `json:"health_check_timeout,omitempty"` +} + +func (resource AppRouteResource) ToFields() (route models.RouteSummary) { + route.Guid = resource.Metadata.Guid + route.Host = resource.Entity.Host + return +} + +func (resource AppRouteResource) ToModel() (route models.RouteSummary) { + route.Guid = resource.Metadata.Guid + route.Host = resource.Entity.Host + route.Domain.Guid = resource.Entity.Domain.Metadata.Guid + route.Domain.Name = resource.Entity.Domain.Entity.Name + return +} + +func NewApplicationEntityFromAppParams(app models.AppParams) ApplicationEntity { + entity := ApplicationEntity{ + Buildpack: app.BuildpackUrl, + Name: app.Name, + SpaceGuid: app.SpaceGuid, + Instances: app.InstanceCount, + Memory: app.Memory, + DiskQuota: app.DiskQuota, + StackGuid: app.StackGuid, + Command: app.Command, + HealthCheckTimeout: app.HealthCheckTimeout, + } + if app.State != nil { + state := strings.ToUpper(*app.State) + entity.State = &state + } + if app.EnvironmentVars != nil && *app.EnvironmentVars != nil { + entity.EnvironmentJson = app.EnvironmentVars + } + return entity +} + +func (resource ApplicationResource) ToFields() (app models.ApplicationFields) { + entity := resource.Entity + app.Guid = resource.Metadata.Guid + + if entity.Name != nil { + app.Name = *entity.Name + } + if entity.Memory != nil { + app.Memory = *entity.Memory + } + if entity.DiskQuota != nil { + app.DiskQuota = *entity.DiskQuota + } + if entity.Instances != nil { + app.InstanceCount = *entity.Instances + } + if entity.State != nil { + app.State = strings.ToLower(*entity.State) + } + if entity.EnvironmentJson != nil { + app.EnvironmentVars = *entity.EnvironmentJson + } + if entity.SpaceGuid != nil { + app.SpaceGuid = *entity.SpaceGuid + } + if entity.DetectedStartCommand != nil { + app.DetectedStartCommand = *entity.DetectedStartCommand + } + if entity.Command != nil { + app.Command = *entity.Command + } + return +} + +func (resource ApplicationResource) ToModel() (app models.Application) { + app.ApplicationFields = resource.ToFields() + + entity := resource.Entity + if entity.Stack != nil { + app.Stack = entity.Stack.ToFields() + } + + if entity.Routes != nil { + for _, routeResource := range *entity.Routes { + app.Routes = append(app.Routes, routeResource.ToModel()) + } + } + + return +} diff --git a/cf/api/resources/auth_tokens.go b/cf/api/resources/auth_tokens.go new file mode 100644 index 00000000000..236e07ad88f --- /dev/null +++ b/cf/api/resources/auth_tokens.go @@ -0,0 +1,24 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedAuthTokenResources struct { + Resources []AuthTokenResource +} + +type AuthTokenResource struct { + Resource + Entity AuthTokenEntity +} + +type AuthTokenEntity struct { + Label string + Provider string +} + +func (resource AuthTokenResource) ToFields() (authToken models.ServiceAuthTokenFields) { + authToken.Guid = resource.Metadata.Guid + authToken.Label = resource.Entity.Label + authToken.Provider = resource.Entity.Provider + return +} diff --git a/cf/api/resources/buildpacks.go b/cf/api/resources/buildpacks.go new file mode 100644 index 00000000000..3c4e87a591a --- /dev/null +++ b/cf/api/resources/buildpacks.go @@ -0,0 +1,29 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type BuildpackResource struct { + Resource + Entity BuildpackEntity +} + +type BuildpackEntity struct { + Name string `json:"name"` + Position *int `json:"position,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Key string `json:"key,omitempty"` + Filename string `json:"filename,omitempty"` + Locked *bool `json:"locked,omitempty"` +} + +func (resource BuildpackResource) ToFields() models.Buildpack { + return models.Buildpack{ + Guid: resource.Metadata.Guid, + Name: resource.Entity.Name, + Position: resource.Entity.Position, + Enabled: resource.Entity.Enabled, + Key: resource.Entity.Key, + Filename: resource.Entity.Filename, + Locked: resource.Entity.Locked, + } +} diff --git a/cf/api/resources/domains.go b/cf/api/resources/domains.go new file mode 100644 index 00000000000..a4011b2fb1c --- /dev/null +++ b/cf/api/resources/domains.go @@ -0,0 +1,24 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type DomainResource struct { + Resource + Entity DomainEntity +} + +type DomainEntity struct { + Name string `json:"name"` + OwningOrganizationGuid string `json:"owning_organization_guid,omitempty"` + Wildcard bool `json:"wildcard"` +} + +func (resource DomainResource) ToFields() models.DomainFields { + owningOrganizationGuid := resource.Entity.OwningOrganizationGuid + return models.DomainFields{ + Name: resource.Entity.Name, + Guid: resource.Metadata.Guid, + OwningOrganizationGuid: owningOrganizationGuid, + Shared: owningOrganizationGuid == "", + } +} diff --git a/cf/api/resources/events.go b/cf/api/resources/events.go new file mode 100644 index 00000000000..ae29f4bfbf1 --- /dev/null +++ b/cf/api/resources/events.go @@ -0,0 +1,107 @@ +package resources + +import ( + "fmt" + "strconv" + "strings" + "time" + + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/generic" +) + +type EventResource interface { + ToFields() models.EventFields +} + +type EventResourceNewV2 struct { + Resource + Entity struct { + Timestamp time.Time + Type string + ActorName string `json:"actor_name"` + Metadata map[string]interface{} + } +} + +type EventResourceOldV2 struct { + Resource + Entity struct { + Timestamp time.Time + ExitDescription string `json:"exit_description"` + ExitStatus int `json:"exit_status"` + InstanceIndex int `json:"instance_index"` + } +} + +func (resource EventResourceNewV2) ToFields() models.EventFields { + metadata := generic.NewMap(resource.Entity.Metadata) + if metadata.Has("request") { + metadata = generic.NewMap(metadata.Get("request")) + } + + return models.EventFields{ + Guid: resource.Metadata.Guid, + Name: resource.Entity.Type, + Timestamp: resource.Entity.Timestamp, + Description: formatDescription(metadata, knownMetadataKeys), + ActorName: resource.Entity.ActorName, + } +} + +func (resource EventResourceOldV2) ToFields() models.EventFields { + return models.EventFields{ + Guid: resource.Metadata.Guid, + Name: T("app crashed"), + Timestamp: resource.Entity.Timestamp, + Description: fmt.Sprintf(T("instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + map[string]interface{}{ + "InstanceIndex": resource.Entity.InstanceIndex, + "ExitDescription": resource.Entity.ExitDescription, + "ExitStatus": strconv.Itoa(resource.Entity.ExitStatus), + })), + } +} + +var knownMetadataKeys = []string{ + "index", + "reason", + "exit_description", + "exit_status", + "recursive", + "disk_quota", + "instances", + "memory", + "state", + "command", + "environment_json", +} + +func formatDescription(metadata generic.Map, keys []string) string { + parts := []string{} + for _, key := range keys { + value := metadata.Get(key) + if value != nil { + parts = append(parts, fmt.Sprintf("%s: %s", key, formatDescriptionPart(value))) + } + } + return strings.Join(parts, ", ") +} + +func formatDescriptionPart(val interface{}) string { + switch val := val.(type) { + case string: + return val + case float64: + return strconv.FormatFloat(val, byte('f'), -1, 64) + case bool: + if val { + return "true" + } else { + return "false" + } + default: + return fmt.Sprintf("%s", val) + } +} diff --git a/cf/api/resources/events_test.go b/cf/api/resources/events_test.go new file mode 100644 index 00000000000..bbca40c45d5 --- /dev/null +++ b/cf/api/resources/events_test.go @@ -0,0 +1,169 @@ +package resources_test + +import ( + "encoding/json" + + . "github.com/cloudfoundry/cli/cf/api/resources" + testtime "github.com/cloudfoundry/cli/testhelpers/time" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Event resources", func() { + var resource EventResource + + Describe("New V2 resources", func() { + BeforeEach(func() { + resource = new(EventResourceNewV2) + }) + + It("unmarshals app crash events", func() { + err := json.Unmarshal([]byte(` + { + "metadata": { + "guid":"event-1-guid" + }, + "entity": { + "timestamp": "2013-10-07T16:51:07+00:00", + "type": "app.crash", + "metadata": { + "instance": "50dd66d3f8874b35988d23a25d19bfa0", + "index": 3, + "exit_status": -1, + "exit_description": "unknown", + "reason": "CRASHED" + } + } + }`), &resource) + + Expect(err).NotTo(HaveOccurred()) + + eventFields := resource.ToFields() + Expect(eventFields.Guid).To(Equal("event-1-guid")) + Expect(eventFields.Name).To(Equal("app.crash")) + Expect(eventFields.Timestamp).To(Equal(testtime.MustParse(eventTimestampFormat, "2013-10-07T16:51:07+00:00"))) + Expect(eventFields.Description).To(Equal(`index: 3, reason: CRASHED, exit_description: unknown, exit_status: -1`)) + }) + + It("unmarshals app update events", func() { + err := json.Unmarshal([]byte(` + { + "metadata": { + "guid": "event-1-guid" + }, + "entity": { + "type": "audit.app.update", + "timestamp": "2014-01-21T00:20:11+00:00", + "metadata": { + "request": { + "state": "STOPPED", + "command": "PRIVATE DATA HIDDEN", + "instances": 1, + "memory": 256, + "environment_json": "PRIVATE DATA HIDDEN" + } + } + } + }`), &resource) + + Expect(err).NotTo(HaveOccurred()) + + eventFields := resource.ToFields() + Expect(eventFields.Guid).To(Equal("event-1-guid")) + Expect(eventFields.Name).To(Equal("audit.app.update")) + Expect(eventFields.Timestamp).To(Equal(testtime.MustParse(eventTimestampFormat, "2014-01-21T00:20:11+00:00"))) + Expect(eventFields.Description).To(Equal("instances: 1, memory: 256, state: STOPPED, command: PRIVATE DATA HIDDEN, environment_json: PRIVATE DATA HIDDEN")) + }) + + It("unmarshals app delete events", func() { + resource := new(EventResourceNewV2) + err := json.Unmarshal([]byte(` + { + "metadata": { + "guid": "event-2-guid" + }, + "entity": { + "type": "audit.app.delete-request", + "timestamp": "2014-01-21T18:39:09+00:00", + "metadata": { + "request": { + "recursive": true + } + } + } + }`), &resource) + + Expect(err).NotTo(HaveOccurred()) + + eventFields := resource.ToFields() + Expect(eventFields.Guid).To(Equal("event-2-guid")) + Expect(eventFields.Name).To(Equal("audit.app.delete-request")) + Expect(eventFields.Timestamp).To(Equal(testtime.MustParse(eventTimestampFormat, "2014-01-21T18:39:09+00:00"))) + Expect(eventFields.Description).To(Equal("recursive: true")) + }) + + It("unmarshals the new v2 app create event", func() { + resource := new(EventResourceNewV2) + err := json.Unmarshal([]byte(` + { + "metadata": { + "guid": "event-1-guid" + }, + "entity": { + "type": "audit.app.create", + "timestamp": "2014-01-22T19:34:16+00:00", + "metadata": { + "request": { + "name": "java-warz", + "space_guid": "6cc20fec-0dee-4843-b875-b124bfee791a", + "production": false, + "environment_json": "PRIVATE DATA HIDDEN", + "instances": 1, + "disk_quota": 1024, + "state": "STOPPED", + "console": false + } + } + } + }`), &resource) + + Expect(err).NotTo(HaveOccurred()) + + eventFields := resource.ToFields() + Expect(eventFields.Guid).To(Equal("event-1-guid")) + Expect(eventFields.Name).To(Equal("audit.app.create")) + Expect(eventFields.Timestamp).To(Equal(testtime.MustParse(eventTimestampFormat, "2014-01-22T19:34:16+00:00"))) + Expect(eventFields.Description).To(Equal("disk_quota: 1024, instances: 1, state: STOPPED, environment_json: PRIVATE DATA HIDDEN")) + }) + }) + + Describe("Old V2 Resources", func() { + BeforeEach(func() { + resource = new(EventResourceOldV2) + }) + + It("unmarshals app crashed events", func() { + err := json.Unmarshal([]byte(` + { + "metadata": { + "guid": "event-1-guid" + }, + "entity": { + "timestamp": "2014-01-22T19:34:16+00:00", + "exit_status": 3, + "instance_index": 4, + "exit_description": "the exit description" + } + }`), &resource) + + Expect(err).NotTo(HaveOccurred()) + eventFields := resource.ToFields() + Expect(eventFields.Guid).To(Equal("event-1-guid")) + Expect(eventFields.Name).To(Equal("app crashed")) + Expect(eventFields.Timestamp).To(Equal(testtime.MustParse(eventTimestampFormat, "2014-01-22T19:34:16+00:00"))) + Expect(eventFields.Description).To(Equal("instance: 4, reason: the exit description, exit_status: 3")) + }) + }) +}) + +const eventTimestampFormat = "2006-01-02T15:04:05-07:00" diff --git a/cf/api/resources/feature_flags.go b/cf/api/resources/feature_flags.go new file mode 100644 index 00000000000..d9a0fed2efd --- /dev/null +++ b/cf/api/resources/feature_flags.go @@ -0,0 +1,14 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type FeatureFlagResource struct { + Entity models.FeatureFlag +} + +func (resource FeatureFlagResource) ToFields() (flag models.FeatureFlag) { + flag.Name = resource.Entity.Name + flag.Enabled = resource.Entity.Enabled + flag.ErrorMessage = resource.Entity.ErrorMessage + return +} diff --git a/cf/api/resources/organizations.go b/cf/api/resources/organizations.go new file mode 100644 index 00000000000..862aec90f97 --- /dev/null +++ b/cf/api/resources/organizations.go @@ -0,0 +1,47 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type OrganizationResource struct { + Resource + Entity OrganizationEntity +} + +type OrganizationEntity struct { + Name string + QuotaDefinition QuotaResource `json:"quota_definition"` + Spaces []SpaceResource + Domains []DomainResource + SpaceQuotas []SpaceQuotaResource `json:"space_quota_definitions"` +} + +func (resource OrganizationResource) ToFields() (fields models.OrganizationFields) { + fields.Name = resource.Entity.Name + fields.Guid = resource.Metadata.Guid + + fields.QuotaDefinition = resource.Entity.QuotaDefinition.ToFields() + return +} + +func (resource OrganizationResource) ToModel() (org models.Organization) { + org.OrganizationFields = resource.ToFields() + + spaces := []models.SpaceFields{} + for _, s := range resource.Entity.Spaces { + spaces = append(spaces, s.ToFields()) + } + org.Spaces = spaces + + domains := []models.DomainFields{} + for _, d := range resource.Entity.Domains { + domains = append(domains, d.ToFields()) + } + org.Domains = domains + + spaceQuotas := []models.SpaceQuota{} + for _, sq := range resource.Entity.SpaceQuotas { + spaceQuotas = append(spaceQuotas, sq.ToModel()) + } + org.SpaceQuotas = spaceQuotas + return +} diff --git a/cf/api/resources/quotas.go b/cf/api/resources/quotas.go new file mode 100644 index 00000000000..5c253e7b254 --- /dev/null +++ b/cf/api/resources/quotas.go @@ -0,0 +1,23 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedQuotaResources struct { + Resources []QuotaResource +} + +type QuotaResource struct { + Resource + Entity models.QuotaFields +} + +func (resource QuotaResource) ToFields() (quota models.QuotaFields) { + quota.Guid = resource.Metadata.Guid + quota.Name = resource.Entity.Name + quota.MemoryLimit = resource.Entity.MemoryLimit + quota.InstanceMemoryLimit = resource.Entity.InstanceMemoryLimit + quota.RoutesLimit = resource.Entity.RoutesLimit + quota.ServicesLimit = resource.Entity.ServicesLimit + quota.NonBasicServicesAllowed = resource.Entity.NonBasicServicesAllowed + return +} diff --git a/cf/api/resources/resources.go b/cf/api/resources/resources.go new file mode 100644 index 00000000000..2a124bed78d --- /dev/null +++ b/cf/api/resources/resources.go @@ -0,0 +1,10 @@ +package resources + +type Metadata struct { + Guid string `json:"guid"` + Url string `json:"url,omitempty"` +} + +type Resource struct { + Metadata Metadata +} diff --git a/cf/api/resources/resources_test.go b/cf/api/resources/resources_test.go new file mode 100644 index 00000000000..cc1fc16426a --- /dev/null +++ b/cf/api/resources/resources_test.go @@ -0,0 +1,19 @@ +package resources_test + +import ( + "testing" + + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestResources(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Resources Suite") +} diff --git a/cf/api/resources/routes.go b/cf/api/resources/routes.go new file mode 100644 index 00000000000..873caecf44b --- /dev/null +++ b/cf/api/resources/routes.go @@ -0,0 +1,31 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type RouteResource struct { + Resource + Entity RouteEntity +} + +type RouteEntity struct { + Host string + Domain DomainResource + Space SpaceResource + Apps []ApplicationResource +} + +func (resource RouteResource) ToFields() (fields models.Route) { + fields.Guid = resource.Metadata.Guid + fields.Host = resource.Entity.Host + return +} +func (resource RouteResource) ToModel() (route models.Route) { + route.Host = resource.Entity.Host + route.Guid = resource.Metadata.Guid + route.Domain = resource.Entity.Domain.ToFields() + route.Space = resource.Entity.Space.ToFields() + for _, appResource := range resource.Entity.Apps { + route.Apps = append(route.Apps, appResource.ToFields()) + } + return +} diff --git a/cf/api/resources/security_groups.go b/cf/api/resources/security_groups.go new file mode 100644 index 00000000000..41662bded45 --- /dev/null +++ b/cf/api/resources/security_groups.go @@ -0,0 +1,37 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedSecurityGroupResources struct { + Resources []SecurityGroupResource +} + +type SecurityGroupResource struct { + Resource + Entity SecurityGroup +} + +type SecurityGroup struct { + models.SecurityGroupFields + Spaces []SpaceResource +} + +func (resource SecurityGroupResource) ToFields() (fields models.SecurityGroupFields) { + fields.Name = resource.Entity.Name + fields.Rules = resource.Entity.Rules + fields.Guid = resource.Metadata.Guid + + return +} + +func (resource SecurityGroupResource) ToModel() (asg models.SecurityGroup) { + asg.SecurityGroupFields = resource.ToFields() + + spaces := []models.Space{} + for _, s := range resource.Entity.Spaces { + spaces = append(spaces, s.ToModel()) + } + asg.Spaces = spaces + + return +} diff --git a/cf/api/resources/service_bindings.go b/cf/api/resources/service_bindings.go new file mode 100644 index 00000000000..d8a116312d6 --- /dev/null +++ b/cf/api/resources/service_bindings.go @@ -0,0 +1,19 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type ServiceBindingResource struct { + Resource + Entity ServiceBindingEntity +} + +type ServiceBindingEntity struct { + AppGuid string `json:"app_guid"` +} + +func (resource ServiceBindingResource) ToFields() (fields models.ServiceBindingFields) { + fields.Url = resource.Metadata.Url + fields.Guid = resource.Metadata.Guid + fields.AppGuid = resource.Entity.AppGuid + return +} diff --git a/cf/api/resources/service_brokers.go b/cf/api/resources/service_brokers.go new file mode 100644 index 00000000000..b83b3d703b2 --- /dev/null +++ b/cf/api/resources/service_brokers.go @@ -0,0 +1,25 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type ServiceBrokerResource struct { + Resource + Entity ServiceBrokerEntity +} + +type ServiceBrokerEntity struct { + Guid string + Name string + Password string `json:"auth_password"` + Username string `json:"auth_username"` + Url string `json:"broker_url"` +} + +func (resource ServiceBrokerResource) ToFields() (fields models.ServiceBroker) { + fields.Name = resource.Entity.Name + fields.Guid = resource.Metadata.Guid + fields.Url = resource.Entity.Url + fields.Username = resource.Entity.Username + fields.Password = resource.Entity.Password + return +} diff --git a/cf/api/resources/service_instances.go b/cf/api/resources/service_instances.go new file mode 100644 index 00000000000..ac1c4bc839b --- /dev/null +++ b/cf/api/resources/service_instances.go @@ -0,0 +1,36 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedServiceInstanceResources struct { + TotalResults int `json:"total_results"` + Resources []ServiceInstanceResource +} + +type ServiceInstanceResource struct { + Resource + Entity ServiceInstanceEntity +} + +type ServiceInstanceEntity struct { + Name string + ServiceBindings []ServiceBindingResource `json:"service_bindings"` + ServicePlan ServicePlanResource `json:"service_plan"` +} + +func (resource ServiceInstanceResource) ToFields() (fields models.ServiceInstanceFields) { + fields.Guid = resource.Metadata.Guid + fields.Name = resource.Entity.Name + return +} + +func (resource ServiceInstanceResource) ToModel() (instance models.ServiceInstance) { + instance.ServiceInstanceFields = resource.ToFields() + instance.ServicePlan = resource.Entity.ServicePlan.ToFields() + + instance.ServiceBindings = []models.ServiceBindingFields{} + for _, bindingResource := range resource.Entity.ServiceBindings { + instance.ServiceBindings = append(instance.ServiceBindings, bindingResource.ToFields()) + } + return +} diff --git a/cf/api/resources/service_offerings.go b/cf/api/resources/service_offerings.go new file mode 100644 index 00000000000..e94a6c98183 --- /dev/null +++ b/cf/api/resources/service_offerings.go @@ -0,0 +1,44 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedServiceOfferingResources struct { + Resources []ServiceOfferingResource +} + +type ServiceOfferingResource struct { + Resource + Entity ServiceOfferingEntity +} + +type ServiceOfferingEntity struct { + Label string `json:"label"` + Version string `json:"version"` + Description string `json:"description"` + DocumentationUrl string `json:"documentation_url"` + Provider string `json:"provider"` + BrokerGuid string `json:"service_broker_guid"` + ServicePlans []ServicePlanResource `json:"service_plans"` +} + +func (resource ServiceOfferingResource) ToFields() (fields models.ServiceOfferingFields) { + fields.Label = resource.Entity.Label + fields.Version = resource.Entity.Version + fields.Provider = resource.Entity.Provider + fields.Description = resource.Entity.Description + fields.BrokerGuid = resource.Entity.BrokerGuid + fields.Guid = resource.Metadata.Guid + fields.DocumentationUrl = resource.Entity.DocumentationUrl + return +} + +func (resource ServiceOfferingResource) ToModel() (offering models.ServiceOffering) { + offering.ServiceOfferingFields = resource.ToFields() + for _, p := range resource.Entity.ServicePlans { + servicePlan := models.ServicePlanFields{} + servicePlan.Name = p.Entity.Name + servicePlan.Guid = p.Metadata.Guid + offering.Plans = append(offering.Plans, servicePlan) + } + return offering +} diff --git a/cf/api/resources/service_plan_visibility.go b/cf/api/resources/service_plan_visibility.go new file mode 100644 index 00000000000..9889105bee2 --- /dev/null +++ b/cf/api/resources/service_plan_visibility.go @@ -0,0 +1,15 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type ServicePlanVisibilityResource struct { + Resource + Entity models.ServicePlanVisibilityFields +} + +func (resource ServicePlanVisibilityResource) ToFields() (fields models.ServicePlanVisibilityFields) { + fields.Guid = resource.Metadata.Guid + fields.ServicePlanGuid = resource.Entity.ServicePlanGuid + fields.OrganizationGuid = resource.Entity.OrganizationGuid + return +} diff --git a/cf/api/resources/service_plans.go b/cf/api/resources/service_plans.go new file mode 100644 index 00000000000..5f667707217 --- /dev/null +++ b/cf/api/resources/service_plans.go @@ -0,0 +1,51 @@ +package resources + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/models" +) + +type ServicePlanResource struct { + Resource + Entity ServicePlanEntity +} + +type ServicePlanEntity struct { + Name string + Free bool + Public bool + Active bool + Description string `json:"description"` + ServiceOfferingGuid string `json:"service_guid"` + ServiceOffering ServiceOfferingResource `json:"service"` +} + +type ServicePlanDescription struct { + ServiceLabel string + ServicePlanName string + ServiceProvider string +} + +func (resource ServicePlanResource) ToFields() (fields models.ServicePlanFields) { + fields.Guid = resource.Metadata.Guid + fields.Name = resource.Entity.Name + fields.Free = resource.Entity.Free + fields.Description = resource.Entity.Description + fields.Public = resource.Entity.Public + fields.Active = resource.Entity.Active + fields.ServiceOfferingGuid = resource.Entity.ServiceOfferingGuid + return +} + +func (planDesc ServicePlanDescription) String() string { + if planDesc.ServiceProvider == "" { + return fmt.Sprintf("%s %s", planDesc.ServiceLabel, planDesc.ServicePlanName) // v2 plan + } else { + return fmt.Sprintf("%s %s %s", planDesc.ServiceLabel, planDesc.ServiceProvider, planDesc.ServicePlanName) // v1 plan + } +} + +type ServiceMigrateV1ToV2Response struct { + ChangedCount int `json:"changed_count"` +} diff --git a/cf/api/resources/space_quotas.go b/cf/api/resources/space_quotas.go new file mode 100644 index 00000000000..95fd03f1c14 --- /dev/null +++ b/cf/api/resources/space_quotas.go @@ -0,0 +1,27 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedSpaceQuotaResources struct { + Resources []SpaceQuotaResource +} + +type SpaceQuotaResource struct { + Resource + Entity models.SpaceQuota +} + +func (resource SpaceQuotaResource) ToModel() models.SpaceQuota { + entity := resource.Entity + + return models.SpaceQuota{ + Guid: resource.Metadata.Guid, + Name: entity.Name, + MemoryLimit: entity.MemoryLimit, + InstanceMemoryLimit: entity.InstanceMemoryLimit, + RoutesLimit: entity.RoutesLimit, + ServicesLimit: entity.ServicesLimit, + NonBasicServicesAllowed: entity.NonBasicServicesAllowed, + OrgGuid: entity.OrgGuid, + } +} diff --git a/cf/api/resources/spaces.go b/cf/api/resources/spaces.go new file mode 100644 index 00000000000..6c268887937 --- /dev/null +++ b/cf/api/resources/spaces.go @@ -0,0 +1,47 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type SpaceResource struct { + Resource + Entity SpaceEntity +} + +type SpaceEntity struct { + Name string + Organization OrganizationResource + Applications []ApplicationResource `json:"apps"` + Domains []DomainResource + ServiceInstances []ServiceInstanceResource `json:"service_instances"` + SecurityGroups []SecurityGroupResource `json:"security_groups"` + SpaceQuotaGuid string `json:"space_quota_definition_guid"` +} + +func (resource SpaceResource) ToFields() (fields models.SpaceFields) { + fields.Guid = resource.Metadata.Guid + fields.Name = resource.Entity.Name + return +} + +func (resource SpaceResource) ToModel() (space models.Space) { + space.SpaceFields = resource.ToFields() + for _, app := range resource.Entity.Applications { + space.Applications = append(space.Applications, app.ToFields()) + } + + for _, domainResource := range resource.Entity.Domains { + space.Domains = append(space.Domains, domainResource.ToFields()) + } + + for _, serviceResource := range resource.Entity.ServiceInstances { + space.ServiceInstances = append(space.ServiceInstances, serviceResource.ToFields()) + } + + for _, securityGroupResource := range resource.Entity.SecurityGroups { + space.SecurityGroups = append(space.SecurityGroups, securityGroupResource.ToFields()) + } + + space.Organization = resource.Entity.Organization.ToFields() + space.SpaceQuotaGuid = resource.Entity.SpaceQuotaGuid + return +} diff --git a/cf/api/resources/stacks.go b/cf/api/resources/stacks.go new file mode 100644 index 00000000000..b1bc46a3bc9 --- /dev/null +++ b/cf/api/resources/stacks.go @@ -0,0 +1,25 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type PaginatedStackResources struct { + Resources []StackResource +} + +type StackResource struct { + Resource + Entity StackEntity +} + +type StackEntity struct { + Name string + Description string +} + +func (resource StackResource) ToFields() *models.Stack { + return &models.Stack{ + Guid: resource.Metadata.Guid, + Name: resource.Entity.Name, + Description: resource.Entity.Description, + } +} diff --git a/cf/api/resources/users.go b/cf/api/resources/users.go new file mode 100644 index 00000000000..010195c45b8 --- /dev/null +++ b/cf/api/resources/users.go @@ -0,0 +1,59 @@ +package resources + +import "github.com/cloudfoundry/cli/cf/models" + +type UserResource struct { + Resource + Entity UserEntity +} + +type UserEntity struct { + Name string + Admin bool +} + +type UAAUserResources struct { + Resources []struct { + Id string + Username string + } +} + +func (resource UserResource) ToFields() models.UserFields { + return models.UserFields{ + Guid: resource.Metadata.Guid, + IsAdmin: resource.Entity.Admin, + } +} + +type UAAUserResourceEmail struct { + Value string `json:"value"` +} + +type UAAUserResourceName struct { + GivenName string `json:"givenName"` + FamilyName string `json:"familyName"` +} + +type UAAUserResource struct { + Username string `json:"userName"` + Emails []UAAUserResourceEmail `json:"emails"` + Password string `json:"password"` + Name UAAUserResourceName `json:"name"` +} + +func NewUAAUserResource(username, password string) UAAUserResource { + return UAAUserResource{ + Username: username, + Emails: []UAAUserResourceEmail{{Value: username}}, + Password: password, + Name: UAAUserResourceName{ + GivenName: username, + FamilyName: username, + }, + } +} + +type UAAUserFields struct { + Id string +} diff --git a/cf/api/routes.go b/cf/api/routes.go new file mode 100644 index 00000000000..ebad551c2e1 --- /dev/null +++ b/cf/api/routes.go @@ -0,0 +1,112 @@ +package api + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type RouteRepository interface { + ListRoutes(cb func(models.Route) bool) (apiErr error) + FindByHostAndDomain(host string, domain models.DomainFields) (route models.Route, apiErr error) + Create(host string, domain models.DomainFields) (createdRoute models.Route, apiErr error) + CheckIfExists(host string, domain models.DomainFields) (found bool, apiErr error) + CreateInSpace(host, domainGuid, spaceGuid string) (createdRoute models.Route, apiErr error) + Bind(routeGuid, appGuid string) (apiErr error) + Unbind(routeGuid, appGuid string) (apiErr error) + Delete(routeGuid string) (apiErr error) +} + +type CloudControllerRouteRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerRouteRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerRouteRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerRouteRepository) ListRoutes(cb func(models.Route) bool) (apiErr error) { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/spaces/%s/routes?inline-relations-depth=1", repo.config.SpaceFields().Guid), + resources.RouteResource{}, + func(resource interface{}) bool { + return cb(resource.(resources.RouteResource).ToModel()) + }) +} + +func (repo CloudControllerRouteRepository) FindByHostAndDomain(host string, domain models.DomainFields) (route models.Route, apiErr error) { + found := false + apiErr = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/routes?inline-relations-depth=1&q=%s", url.QueryEscape("host:"+host+";domain_guid:"+domain.Guid)), + resources.RouteResource{}, + func(resource interface{}) bool { + route = resource.(resources.RouteResource).ToModel() + found = true + return false + }) + + if apiErr == nil && !found { + apiErr = errors.NewModelNotFoundError("Route", host) + } + + return +} + +func (repo CloudControllerRouteRepository) Create(host string, domain models.DomainFields) (createdRoute models.Route, apiErr error) { + return repo.CreateInSpace(host, domain.Guid, repo.config.SpaceFields().Guid) +} + +func (repo CloudControllerRouteRepository) CheckIfExists(host string, domain models.DomainFields) (found bool, apiErr error) { + var raw_response interface{} + apiErr = repo.gateway.GetResource(fmt.Sprintf("%s/v2/routes/reserved/domain/%s/host/%s", repo.config.ApiEndpoint(), domain.Guid, host), &raw_response) + + switch apiErr.(type) { + case nil: + found = true + case *errors.HttpNotFoundError: + found = false + apiErr = nil + default: + return + } + return +} + +func (repo CloudControllerRouteRepository) CreateInSpace(host, domainGuid, spaceGuid string) (createdRoute models.Route, apiErr error) { + data := fmt.Sprintf(`{"host":"%s","domain_guid":"%s","space_guid":"%s"}`, host, domainGuid, spaceGuid) + + resource := new(resources.RouteResource) + apiErr = repo.gateway.CreateResource(repo.config.ApiEndpoint(), "/v2/routes?inline-relations-depth=1", strings.NewReader(data), resource) + if apiErr != nil { + return + } + + createdRoute = resource.ToModel() + return +} + +func (repo CloudControllerRouteRepository) Bind(routeGuid, appGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/apps/%s/routes/%s", appGuid, routeGuid) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, nil) +} + +func (repo CloudControllerRouteRepository) Unbind(routeGuid, appGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/apps/%s/routes/%s", appGuid, routeGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerRouteRepository) Delete(routeGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/routes/%s", routeGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/routes_test.go b/cf/api/routes_test.go new file mode 100644 index 00000000000..6e22aca568d --- /dev/null +++ b/cf/api/routes_test.go @@ -0,0 +1,387 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("route repository", func() { + + var ( + ts *httptest.Server + handler *testnet.TestHandler + configRepo core_config.Repository + repo CloudControllerRouteRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + configRepo.SetSpaceFields(models.SpaceFields{ + Guid: "the-space-guid", + Name: "the-space-name", + }) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerRouteRepository(configRepo, gateway) + }) + + AfterEach(func() { + ts.Close() + }) + + Describe("List routes", func() { + It("lists routes in the current space", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/the-space-guid/routes?inline-relations-depth=1", + Response: firstPageRoutesResponse, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/the-space-guid/routes?inline-relations-depth=1&page=2", + Response: secondPageRoutesResponse, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + routes := []models.Route{} + apiErr := repo.ListRoutes(func(route models.Route) bool { + routes = append(routes, route) + return true + }) + + Expect(len(routes)).To(Equal(2)) + Expect(routes[0].Guid).To(Equal("route-1-guid")) + Expect(routes[1].Guid).To(Equal("route-2-guid")) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("finds a route by host and domain", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/routes?q=host%3Amy-cool-app%3Bdomain_guid%3Amy-domain-guid", + Response: findRouteByHostResponse, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + domain := models.DomainFields{} + domain.Guid = "my-domain-guid" + + route, apiErr := repo.FindByHostAndDomain("my-cool-app", domain) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(route.Host).To(Equal("my-cool-app")) + Expect(route.Guid).To(Equal("my-route-guid")) + Expect(route.Domain.Guid).To(Equal(domain.Guid)) + }) + + It("returns 'not found' response when there is no route w/ the given domain and host", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/routes?q=host%3Amy-cool-app%3Bdomain_guid%3Amy-domain-guid", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [ ] }`}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + domain := models.DomainFields{} + domain.Guid = "my-domain-guid" + + _, apiErr := repo.FindByHostAndDomain("my-cool-app", domain) + + Expect(handler).To(HaveAllRequestsCalled()) + + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe("Create routes", func() { + It("creates routes in a given space", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/routes?inline-relations-depth=1", + Matcher: testnet.RequestBodyMatcher(`{"host":"my-cool-app","domain_guid":"my-domain-guid","space_guid":"my-space-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "my-route-guid" }, + "entity": { "host": "my-cool-app" } + } + `}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + createdRoute, apiErr := repo.CreateInSpace("my-cool-app", "my-domain-guid", "my-space-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(createdRoute.Guid).To(Equal("my-route-guid")) + }) + + It("creates routes", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/routes?inline-relations-depth=1", + Matcher: testnet.RequestBodyMatcher(`{"host":"my-cool-app","domain_guid":"my-domain-guid","space_guid":"the-space-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { "guid": "my-route-guid" }, + "entity": { "host": "my-cool-app" } + } + `}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + createdRoute, apiErr := repo.Create("my-cool-app", models.DomainFields{Guid: "my-domain-guid"}) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(createdRoute.Guid).To(Equal("my-route-guid")) + }) + + }) + + Describe("Check routes", func() { + It("checks if a route exists", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/routes/reserved/domain/domain-guid/host/my-host", + Response: testnet.TestResponse{Status: http.StatusNoContent}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + domain := models.DomainFields{} + domain.Guid = "domain-guid" + + found, apiErr := repo.CheckIfExists("my-host", domain) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + }) + Context("when the route is not found", func() { + It("does not return the error", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/routes/reserved/domain/domain-guid/host/my-host", + Response: testnet.TestResponse{Status: http.StatusNotFound}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + domain := models.DomainFields{} + domain.Guid = "domain-guid" + + found, apiErr := repo.CheckIfExists("my-host", domain) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(found).To(BeFalse()) + }) + }) + Context("when there is a random httpError", func() { + It("returns false and the error", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/routes/reserved/domain/domain-guid/host/my-host", + Response: testnet.TestResponse{Status: http.StatusForbidden}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + domain := models.DomainFields{} + domain.Guid = "domain-guid" + + found, apiErr := repo.CheckIfExists("my-host", domain) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + Expect(found).To(BeFalse()) + }) + }) + }) + + Describe("Bind routes", func() { + It("binds routes", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/apps/my-cool-app-guid/routes/my-cool-route-guid", + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + apiErr := repo.Bind("my-cool-route-guid", "my-cool-app-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("unbinds routes", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/apps/my-cool-app-guid/routes/my-cool-route-guid", + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + apiErr := repo.Unbind("my-cool-route-guid", "my-cool-app-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + }) + + Describe("Delete routes", func() { + It("deletes routes", func() { + ts, handler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/routes/my-cool-route-guid", + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, + }), + }) + configRepo.SetApiEndpoint(ts.URL) + + apiErr := repo.Delete("my-cool-route-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + +}) + +var firstPageRoutesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/spaces/the-space-guid/routes?inline-relations-depth=1&page=2", + "resources": [ + { + "metadata": { + "guid": "route-1-guid" + }, + "entity": { + "host": "route-1-host", + "domain": { + "metadata": { + "guid": "domain-1-guid" + }, + "entity": { + "name": "cfapps.io" + } + }, + "space": { + "metadata": { + "guid": "space-1-guid" + }, + "entity": { + "name": "space-1" + } + }, + "apps": [ + { + "metadata": { + "guid": "app-1-guid" + }, + "entity": { + "name": "app-1" + } + } + ] + } + } + ] +}`} + +var secondPageRoutesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "route-2-guid" + }, + "entity": { + "host": "route-2-host", + "domain": { + "metadata": { + "guid": "domain-2-guid" + }, + "entity": { + "name": "example.com" + } + }, + "space": { + "metadata": { + "guid": "space-2-guid" + }, + "entity": { + "name": "space-2" + } + }, + "apps": [ + { + "metadata": { + "guid": "app-2-guid" + }, + "entity": { + "name": "app-2" + } + }, + { + "metadata": { + "guid": "app-3-guid" + }, + "entity": { + "name": "app-3" + } + } + ] + } + } + ] +}`} + +var findRouteByHostResponse = testnet.TestResponse{Status: http.StatusCreated, Body: ` +{ "resources": [ + { + "metadata": { + "guid": "my-route-guid" + }, + "entity": { + "host": "my-cool-app", + "domain": { + "metadata": { + "guid": "my-domain-guid" + } + } + } + } +]}`} diff --git a/cf/api/security_groups/defaults/defaults.go b/cf/api/security_groups/defaults/defaults.go new file mode 100644 index 00000000000..5fd0cb5ed12 --- /dev/null +++ b/cf/api/security_groups/defaults/defaults.go @@ -0,0 +1,44 @@ +package defaults + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type DefaultSecurityGroupsRepoBase struct { + ConfigRepo core_config.Reader + Gateway net.Gateway +} + +func (repo *DefaultSecurityGroupsRepoBase) Bind(groupGuid string, path string) error { + updatedPath := fmt.Sprintf("%s/%s", path, groupGuid) + return repo.Gateway.UpdateResourceFromStruct(repo.ConfigRepo.ApiEndpoint(), updatedPath, "") +} + +func (repo *DefaultSecurityGroupsRepoBase) List(path string) ([]models.SecurityGroupFields, error) { + groups := []models.SecurityGroupFields{} + + err := repo.Gateway.ListPaginatedResources( + repo.ConfigRepo.ApiEndpoint(), + path, + resources.SecurityGroupResource{}, + func(resource interface{}) bool { + if securityGroupResource, ok := resource.(resources.SecurityGroupResource); ok { + groups = append(groups, securityGroupResource.ToFields()) + } + + return true + }, + ) + + return groups, err +} + +func (repo *DefaultSecurityGroupsRepoBase) Delete(groupGuid string, path string) error { + updatedPath := fmt.Sprintf("%s/%s", path, groupGuid) + return repo.Gateway.DeleteResource(repo.ConfigRepo.ApiEndpoint(), updatedPath) +} diff --git a/cf/api/security_groups/defaults/running/fakes/fake_running_security_groups_repo.go b/cf/api/security_groups/defaults/running/fakes/fake_running_security_groups_repo.go new file mode 100644 index 00000000000..834d72e85d8 --- /dev/null +++ b/cf/api/security_groups/defaults/running/fakes/fake_running_security_groups_repo.go @@ -0,0 +1,124 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults" + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeRunningSecurityGroupsRepo struct { + BindToRunningSetStub func(string) error + bindToRunningSetMutex sync.RWMutex + bindToRunningSetArgsForCall []struct { + arg1 string + } + bindToRunningSetReturns struct { + result1 error + } + ListStub func() ([]models.SecurityGroupFields, error) + listMutex sync.RWMutex + listArgsForCall []struct{} + listReturns struct { + result1 []models.SecurityGroupFields + result2 error + } + UnbindFromRunningSetStub func(string) error + unbindFromRunningSetMutex sync.RWMutex + unbindFromRunningSetArgsForCall []struct { + arg1 string + } + unbindFromRunningSetReturns struct { + result1 error + } +} + +func (fake *FakeRunningSecurityGroupsRepo) BindToRunningSet(arg1 string) error { + fake.bindToRunningSetMutex.Lock() + defer fake.bindToRunningSetMutex.Unlock() + fake.bindToRunningSetArgsForCall = append(fake.bindToRunningSetArgsForCall, struct { + arg1 string + }{arg1}) + if fake.BindToRunningSetStub != nil { + return fake.BindToRunningSetStub(arg1) + } else { + return fake.bindToRunningSetReturns.result1 + } +} + +func (fake *FakeRunningSecurityGroupsRepo) BindToRunningSetCallCount() int { + fake.bindToRunningSetMutex.RLock() + defer fake.bindToRunningSetMutex.RUnlock() + return len(fake.bindToRunningSetArgsForCall) +} + +func (fake *FakeRunningSecurityGroupsRepo) BindToRunningSetArgsForCall(i int) string { + fake.bindToRunningSetMutex.RLock() + defer fake.bindToRunningSetMutex.RUnlock() + return fake.bindToRunningSetArgsForCall[i].arg1 +} + +func (fake *FakeRunningSecurityGroupsRepo) BindToRunningSetReturns(result1 error) { + fake.bindToRunningSetReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeRunningSecurityGroupsRepo) List() ([]models.SecurityGroupFields, error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.listArgsForCall = append(fake.listArgsForCall, struct{}{}) + if fake.ListStub != nil { + return fake.ListStub() + } else { + return fake.listReturns.result1, fake.listReturns.result2 + } +} + +func (fake *FakeRunningSecurityGroupsRepo) ListCallCount() int { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + return len(fake.listArgsForCall) +} + +func (fake *FakeRunningSecurityGroupsRepo) ListReturns(result1 []models.SecurityGroupFields, result2 error) { + fake.listReturns = struct { + result1 []models.SecurityGroupFields + result2 error + }{result1, result2} +} + +func (fake *FakeRunningSecurityGroupsRepo) UnbindFromRunningSet(arg1 string) error { + fake.unbindFromRunningSetMutex.Lock() + defer fake.unbindFromRunningSetMutex.Unlock() + fake.unbindFromRunningSetArgsForCall = append(fake.unbindFromRunningSetArgsForCall, struct { + arg1 string + }{arg1}) + if fake.UnbindFromRunningSetStub != nil { + return fake.UnbindFromRunningSetStub(arg1) + } else { + return fake.unbindFromRunningSetReturns.result1 + } +} + +func (fake *FakeRunningSecurityGroupsRepo) UnbindFromRunningSetCallCount() int { + fake.unbindFromRunningSetMutex.RLock() + defer fake.unbindFromRunningSetMutex.RUnlock() + return len(fake.unbindFromRunningSetArgsForCall) +} + +func (fake *FakeRunningSecurityGroupsRepo) UnbindFromRunningSetArgsForCall(i int) string { + fake.unbindFromRunningSetMutex.RLock() + defer fake.unbindFromRunningSetMutex.RUnlock() + return fake.unbindFromRunningSetArgsForCall[i].arg1 +} + +func (fake *FakeRunningSecurityGroupsRepo) UnbindFromRunningSetReturns(result1 error) { + fake.unbindFromRunningSetReturns = struct { + result1 error + }{result1} +} + +var _ RunningSecurityGroupsRepo = new(FakeRunningSecurityGroupsRepo) diff --git a/cf/api/security_groups/defaults/running/running.go b/cf/api/security_groups/defaults/running/running.go new file mode 100644 index 00000000000..d516ad178ab --- /dev/null +++ b/cf/api/security_groups/defaults/running/running.go @@ -0,0 +1,42 @@ +package running + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults" +) + +const urlPath = "/v2/config/running_security_groups" + +type RunningSecurityGroupsRepo interface { + BindToRunningSet(string) error + List() ([]models.SecurityGroupFields, error) + UnbindFromRunningSet(string) error +} + +type cloudControllerRunningSecurityGroupRepo struct { + repoBase DefaultSecurityGroupsRepoBase +} + +func NewRunningSecurityGroupsRepo(configRepo core_config.Reader, gateway net.Gateway) RunningSecurityGroupsRepo { + return &cloudControllerRunningSecurityGroupRepo{ + repoBase: DefaultSecurityGroupsRepoBase{ + ConfigRepo: configRepo, + Gateway: gateway, + }, + } +} + +func (repo *cloudControllerRunningSecurityGroupRepo) BindToRunningSet(groupGuid string) error { + return repo.repoBase.Bind(groupGuid, urlPath) +} + +func (repo *cloudControllerRunningSecurityGroupRepo) List() ([]models.SecurityGroupFields, error) { + return repo.repoBase.List(urlPath) +} + +func (repo *cloudControllerRunningSecurityGroupRepo) UnbindFromRunningSet(groupGuid string) error { + return repo.repoBase.Delete(groupGuid, urlPath) +} diff --git a/cf/api/security_groups/defaults/running/running_suite_test.go b/cf/api/security_groups/defaults/running/running_suite_test.go new file mode 100644 index 00000000000..d9bdc2106db --- /dev/null +++ b/cf/api/security_groups/defaults/running/running_suite_test.go @@ -0,0 +1,19 @@ +package running_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRunning(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Running Suite") +} diff --git a/cf/api/security_groups/defaults/running/running_test.go b/cf/api/security_groups/defaults/running/running_test.go new file mode 100644 index 00000000000..24c01373103 --- /dev/null +++ b/cf/api/security_groups/defaults/running/running_test.go @@ -0,0 +1,193 @@ +package running_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RunningSecurityGroupsRepo", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo RunningSecurityGroupsRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewRunningSecurityGroupsRepo(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".BindToRunningSet", func() { + It("makes a correct request", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/running_security_groups/a-real-guid", + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: bindRunningResponse, + }, + }), + ) + + err := repo.BindToRunningSet("a-real-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".UnbindFromRunningSet", func() { + It("makes a correct request", func() { + testServer, testHandler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/config/running_security_groups/my-guid", + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }, + }), + }) + + configRepo.SetApiEndpoint(testServer.URL) + err := repo.UnbindFromRunningSet("my-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".List", func() { + It("returns a list of security groups that are the defaults for running", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/running_security_groups", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: firstRunningListItem, + }, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/running_security_groups", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: secondRunningListItem, + }, + }), + ) + + defaults, err := repo.List() + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(defaults).To(ConsistOf([]models.SecurityGroupFields{ + { + Name: "name-71", + Guid: "cd186158-b356-474d-9861-724f34f48502", + Rules: []map[string]interface{}{{ + "protocol": "udp", + }}, + }, + { + Name: "name-72", + Guid: "d3374b62-7eac-4823-afbd-460d2bf44c67", + Rules: []map[string]interface{}{{ + "destination": "198.41.191.47/1", + }}, + }, + })) + }) + }) +}) + +var bindRunningResponse string = `{ + "metadata": { + "guid": "897341eb-ef31-406f-b57b-414f51583a3a", + "url": "/v2/config/running_security_groups/897341eb-ef31-406f-b57b-414f51583a3a", + "created_at": "2014-06-23T21:43:30+00:00", + "updated_at": "2014-06-23T21:43:30+00:00" + }, + "entity": { + "name": "name-904", + "rules": [ + { + "protocol": "udp", + "ports": "8080", + "destination": "198.41.191.47/1" + } + ] + } +}` + +var firstRunningListItem string = `{ + "next_url": "/v2/config/running_security_groups?page=2", + "resources": [ + { + "metadata": { + "guid": "cd186158-b356-474d-9861-724f34f48502", + "url": "/v2/security_groups/cd186158-b356-474d-9861-724f34f48502", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-71", + "rules": [ + { + "protocol": "udp" + } + ], + "spaces_url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67/spaces" + } + } + ] +}` + +var secondRunningListItem string = `{ + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "d3374b62-7eac-4823-afbd-460d2bf44c67", + "url": "/v2/config/running_security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-72", + "rules": [ + { + "destination": "198.41.191.47/1" + } + ], + "spaces_url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67/spaces" + } + } + ] +}` diff --git a/cf/api/security_groups/defaults/staging/fakes/fake_staging_security_groups_repo.go b/cf/api/security_groups/defaults/staging/fakes/fake_staging_security_groups_repo.go new file mode 100644 index 00000000000..4815bf70eea --- /dev/null +++ b/cf/api/security_groups/defaults/staging/fakes/fake_staging_security_groups_repo.go @@ -0,0 +1,124 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults" + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeStagingSecurityGroupsRepo struct { + BindToStagingSetStub func(string) error + bindToStagingSetMutex sync.RWMutex + bindToStagingSetArgsForCall []struct { + arg1 string + } + bindToStagingSetReturns struct { + result1 error + } + ListStub func() ([]models.SecurityGroupFields, error) + listMutex sync.RWMutex + listArgsForCall []struct{} + listReturns struct { + result1 []models.SecurityGroupFields + result2 error + } + UnbindFromStagingSetStub func(string) error + unbindFromStagingSetMutex sync.RWMutex + unbindFromStagingSetArgsForCall []struct { + arg1 string + } + unbindFromStagingSetReturns struct { + result1 error + } +} + +func (fake *FakeStagingSecurityGroupsRepo) BindToStagingSet(arg1 string) error { + fake.bindToStagingSetMutex.Lock() + defer fake.bindToStagingSetMutex.Unlock() + fake.bindToStagingSetArgsForCall = append(fake.bindToStagingSetArgsForCall, struct { + arg1 string + }{arg1}) + if fake.BindToStagingSetStub != nil { + return fake.BindToStagingSetStub(arg1) + } else { + return fake.bindToStagingSetReturns.result1 + } +} + +func (fake *FakeStagingSecurityGroupsRepo) BindToStagingSetCallCount() int { + fake.bindToStagingSetMutex.RLock() + defer fake.bindToStagingSetMutex.RUnlock() + return len(fake.bindToStagingSetArgsForCall) +} + +func (fake *FakeStagingSecurityGroupsRepo) BindToStagingSetArgsForCall(i int) string { + fake.bindToStagingSetMutex.RLock() + defer fake.bindToStagingSetMutex.RUnlock() + return fake.bindToStagingSetArgsForCall[i].arg1 +} + +func (fake *FakeStagingSecurityGroupsRepo) BindToStagingSetReturns(result1 error) { + fake.bindToStagingSetReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeStagingSecurityGroupsRepo) List() ([]models.SecurityGroupFields, error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.listArgsForCall = append(fake.listArgsForCall, struct{}{}) + if fake.ListStub != nil { + return fake.ListStub() + } else { + return fake.listReturns.result1, fake.listReturns.result2 + } +} + +func (fake *FakeStagingSecurityGroupsRepo) ListCallCount() int { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + return len(fake.listArgsForCall) +} + +func (fake *FakeStagingSecurityGroupsRepo) ListReturns(result1 []models.SecurityGroupFields, result2 error) { + fake.listReturns = struct { + result1 []models.SecurityGroupFields + result2 error + }{result1, result2} +} + +func (fake *FakeStagingSecurityGroupsRepo) UnbindFromStagingSet(arg1 string) error { + fake.unbindFromStagingSetMutex.Lock() + defer fake.unbindFromStagingSetMutex.Unlock() + fake.unbindFromStagingSetArgsForCall = append(fake.unbindFromStagingSetArgsForCall, struct { + arg1 string + }{arg1}) + if fake.UnbindFromStagingSetStub != nil { + return fake.UnbindFromStagingSetStub(arg1) + } else { + return fake.unbindFromStagingSetReturns.result1 + } +} + +func (fake *FakeStagingSecurityGroupsRepo) UnbindFromStagingSetCallCount() int { + fake.unbindFromStagingSetMutex.RLock() + defer fake.unbindFromStagingSetMutex.RUnlock() + return len(fake.unbindFromStagingSetArgsForCall) +} + +func (fake *FakeStagingSecurityGroupsRepo) UnbindFromStagingSetArgsForCall(i int) string { + fake.unbindFromStagingSetMutex.RLock() + defer fake.unbindFromStagingSetMutex.RUnlock() + return fake.unbindFromStagingSetArgsForCall[i].arg1 +} + +func (fake *FakeStagingSecurityGroupsRepo) UnbindFromStagingSetReturns(result1 error) { + fake.unbindFromStagingSetReturns = struct { + result1 error + }{result1} +} + +var _ StagingSecurityGroupsRepo = new(FakeStagingSecurityGroupsRepo) diff --git a/cf/api/security_groups/defaults/staging/staging.go b/cf/api/security_groups/defaults/staging/staging.go new file mode 100644 index 00000000000..6b6a225caf7 --- /dev/null +++ b/cf/api/security_groups/defaults/staging/staging.go @@ -0,0 +1,42 @@ +package staging + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults" +) + +const urlPath = "/v2/config/staging_security_groups" + +type StagingSecurityGroupsRepo interface { + BindToStagingSet(string) error + List() ([]models.SecurityGroupFields, error) + UnbindFromStagingSet(string) error +} + +type cloudControllerStagingSecurityGroupRepo struct { + repoBase DefaultSecurityGroupsRepoBase +} + +func NewStagingSecurityGroupsRepo(configRepo core_config.Reader, gateway net.Gateway) StagingSecurityGroupsRepo { + return &cloudControllerStagingSecurityGroupRepo{ + repoBase: DefaultSecurityGroupsRepoBase{ + ConfigRepo: configRepo, + Gateway: gateway, + }, + } +} + +func (repo *cloudControllerStagingSecurityGroupRepo) BindToStagingSet(groupGuid string) error { + return repo.repoBase.Bind(groupGuid, urlPath) +} + +func (repo *cloudControllerStagingSecurityGroupRepo) List() ([]models.SecurityGroupFields, error) { + return repo.repoBase.List(urlPath) +} + +func (repo *cloudControllerStagingSecurityGroupRepo) UnbindFromStagingSet(groupGuid string) error { + return repo.repoBase.Delete(groupGuid, urlPath) +} diff --git a/cf/api/security_groups/defaults/staging/staging_suite_test.go b/cf/api/security_groups/defaults/staging/staging_suite_test.go new file mode 100644 index 00000000000..d02a47b6791 --- /dev/null +++ b/cf/api/security_groups/defaults/staging/staging_suite_test.go @@ -0,0 +1,19 @@ +package staging_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestStaging(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Staging Suite") +} diff --git a/cf/api/security_groups/defaults/staging/staging_test.go b/cf/api/security_groups/defaults/staging/staging_test.go new file mode 100644 index 00000000000..026553e56a2 --- /dev/null +++ b/cf/api/security_groups/defaults/staging/staging_test.go @@ -0,0 +1,192 @@ +package staging_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("StagingSecurityGroupsRepo", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo StagingSecurityGroupsRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewStagingSecurityGroupsRepo(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe("BindToStagingSet", func() { + It("makes a correct request", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/config/staging_security_groups/a-real-guid", + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: bindStagingResponse, + }, + }), + ) + + err := repo.BindToStagingSet("a-real-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".List", func() { + It("returns a list of security groups that are the defaults for staging", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/staging_security_groups", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: firstStagingListItem, + }, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/config/staging_security_groups", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: secondStagingListItem, + }, + }), + ) + + defaults, err := repo.List() + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(defaults).To(ConsistOf([]models.SecurityGroupFields{ + { + Name: "name-71", + Guid: "cd186158-b356-474d-9861-724f34f48502", + Rules: []map[string]interface{}{{ + "protocol": "udp", + }}, + }, + { + Name: "name-72", + Guid: "d3374b62-7eac-4823-afbd-460d2bf44c67", + Rules: []map[string]interface{}{{ + "destination": "198.41.191.47/1", + }}, + }, + })) + }) + }) + + Describe("UnbindFromStagingSet", func() { + It("makes a correct request", func() { + testServer, testHandler = testnet.NewServer([]testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/config/staging_security_groups/my-guid", + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }, + }), + }) + + configRepo.SetApiEndpoint(testServer.URL) + err := repo.UnbindFromStagingSet("my-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) +}) + +var bindStagingResponse string = `{ + "metadata": { + "guid": "897341eb-ef31-406f-b57b-414f51583a3a", + "url": "/v2/config/staging_security_groups/897341eb-ef31-406f-b57b-414f51583a3a", + "created_at": "2014-06-23T21:43:30+00:00", + "updated_at": "2014-06-23T21:43:30+00:00" + }, + "entity": { + "name": "name-904", + "rules": [ + { + "protocol": "udp", + "ports": "8080", + "destination": "198.41.191.47/1" + } + ] + } +}` + +var firstStagingListItem string = `{ + "next_url": "/v2/config/staging_security_groups?page=2", + "resources": [ + { + "metadata": { + "guid": "cd186158-b356-474d-9861-724f34f48502", + "url": "/v2/security_groups/cd186158-b356-474d-9861-724f34f48502", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-71", + "rules": [ + { + "protocol": "udp" + } + ], + "spaces_url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67/spaces" + } + } + ] +}` + +var secondStagingListItem string = `{ + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "d3374b62-7eac-4823-afbd-460d2bf44c67", + "url": "/v2/config/staging_security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-72", + "rules": [ + { + "destination": "198.41.191.47/1" + } + ], + "spaces_url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67/spaces" + } + } + ] +}` diff --git a/cf/api/security_groups/fakes/fake_security_group.go b/cf/api/security_groups/fakes/fake_security_group.go new file mode 100644 index 00000000000..a5a7d7e47a8 --- /dev/null +++ b/cf/api/security_groups/fakes/fake_security_group.go @@ -0,0 +1,208 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/models" + + . "github.com/cloudfoundry/cli/cf/api/security_groups" +) + +type FakeSecurityGroupRepo struct { + CreateStub func(name string, rules []map[string]interface{}) error + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 string + arg2 []map[string]interface{} + } + createReturns struct { + result1 error + } + UpdateStub func(guid string, rules []map[string]interface{}) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 string + arg2 []map[string]interface{} + } + updateReturns struct { + result1 error + } + ReadStub func(string) (models.SecurityGroup, error) + readMutex sync.RWMutex + readArgsForCall []struct { + arg1 string + } + readReturns struct { + result1 models.SecurityGroup + result2 error + } + DeleteStub func(string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 string + } + deleteReturns struct { + result1 error + } + FindAllStub func() ([]models.SecurityGroup, error) + findAllMutex sync.RWMutex + findAllArgsForCall []struct{} + findAllReturns struct { + result1 []models.SecurityGroup + result2 error + } +} + +func (fake *FakeSecurityGroupRepo) Create(arg1 string, arg2 []map[string]interface{}) error { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 string + arg2 []map[string]interface{} + }{arg1, arg2}) + if fake.CreateStub != nil { + return fake.CreateStub(arg1, arg2) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeSecurityGroupRepo) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeSecurityGroupRepo) CreateArgsForCall(i int) (string, []map[string]interface{}) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].arg1, fake.createArgsForCall[i].arg2 +} + +func (fake *FakeSecurityGroupRepo) CreateReturns(result1 error) { + fake.createReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSecurityGroupRepo) Update(arg1 string, arg2 []map[string]interface{}) error { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 string + arg2 []map[string]interface{} + }{arg1, arg2}) + if fake.UpdateStub != nil { + return fake.UpdateStub(arg1, arg2) + } else { + return fake.updateReturns.result1 + } +} + +func (fake *FakeSecurityGroupRepo) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeSecurityGroupRepo) UpdateArgsForCall(i int) (string, []map[string]interface{}) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return fake.updateArgsForCall[i].arg1, fake.updateArgsForCall[i].arg2 +} + +func (fake *FakeSecurityGroupRepo) UpdateReturns(result1 error) { + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSecurityGroupRepo) Read(arg1 string) (models.SecurityGroup, error) { + fake.readMutex.Lock() + defer fake.readMutex.Unlock() + fake.readArgsForCall = append(fake.readArgsForCall, struct { + arg1 string + }{arg1}) + if fake.ReadStub != nil { + return fake.ReadStub(arg1) + } else { + return fake.readReturns.result1, fake.readReturns.result2 + } +} + +func (fake *FakeSecurityGroupRepo) ReadCallCount() int { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + return len(fake.readArgsForCall) +} + +func (fake *FakeSecurityGroupRepo) ReadArgsForCall(i int) string { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + return fake.readArgsForCall[i].arg1 +} + +func (fake *FakeSecurityGroupRepo) ReadReturns(result1 models.SecurityGroup, result2 error) { + fake.readReturns = struct { + result1 models.SecurityGroup + result2 error + }{result1, result2} +} + +func (fake *FakeSecurityGroupRepo) Delete(arg1 string) error { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 string + }{arg1}) + if fake.DeleteStub != nil { + return fake.DeleteStub(arg1) + } else { + return fake.deleteReturns.result1 + } +} + +func (fake *FakeSecurityGroupRepo) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeSecurityGroupRepo) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].arg1 +} + +func (fake *FakeSecurityGroupRepo) DeleteReturns(result1 error) { + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSecurityGroupRepo) FindAll() ([]models.SecurityGroup, error) { + fake.findAllMutex.Lock() + defer fake.findAllMutex.Unlock() + fake.findAllArgsForCall = append(fake.findAllArgsForCall, struct{}{}) + if fake.FindAllStub != nil { + return fake.FindAllStub() + } else { + return fake.findAllReturns.result1, fake.findAllReturns.result2 + } +} + +func (fake *FakeSecurityGroupRepo) FindAllCallCount() int { + fake.findAllMutex.RLock() + defer fake.findAllMutex.RUnlock() + return len(fake.findAllArgsForCall) +} + +func (fake *FakeSecurityGroupRepo) FindAllReturns(result1 []models.SecurityGroup, result2 error) { + fake.findAllReturns = struct { + result1 []models.SecurityGroup + result2 error + }{result1, result2} +} + +var _ SecurityGroupRepo = new(FakeSecurityGroupRepo) diff --git a/cf/api/security_groups/security_groups.go b/cf/api/security_groups/security_groups.go new file mode 100644 index 00000000000..36224774a89 --- /dev/null +++ b/cf/api/security_groups/security_groups.go @@ -0,0 +1,104 @@ +package security_groups + +import ( + "fmt" + "net/url" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type SecurityGroupRepo interface { + Create(name string, rules []map[string]interface{}) error + Update(guid string, rules []map[string]interface{}) error + Read(string) (models.SecurityGroup, error) + Delete(string) error + FindAll() ([]models.SecurityGroup, error) +} + +type cloudControllerSecurityGroupRepo struct { + gateway net.Gateway + config core_config.Reader +} + +func NewSecurityGroupRepo(config core_config.Reader, gateway net.Gateway) SecurityGroupRepo { + return cloudControllerSecurityGroupRepo{ + config: config, + gateway: gateway, + } +} + +func (repo cloudControllerSecurityGroupRepo) Create(name string, rules []map[string]interface{}) error { + path := "/v2/security_groups" + params := models.SecurityGroupParams{ + Name: name, + Rules: rules, + } + return repo.gateway.CreateResourceFromStruct(repo.config.ApiEndpoint(), path, params) +} + +func (repo cloudControllerSecurityGroupRepo) Read(name string) (models.SecurityGroup, error) { + path := fmt.Sprintf("/v2/security_groups?q=%s&inline-relations-depth=2", url.QueryEscape("name:"+name)) + group := models.SecurityGroup{} + foundGroup := false + + err := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.SecurityGroupResource{}, + func(resource interface{}) bool { + if asgr, ok := resource.(resources.SecurityGroupResource); ok { + group = asgr.ToModel() + foundGroup = true + } + + return false + }, + ) + if err != nil { + return group, err + } + + if !foundGroup { + err = errors.NewModelNotFoundError("security group", name) + } + + return group, err +} + +func (repo cloudControllerSecurityGroupRepo) Update(guid string, rules []map[string]interface{}) error { + url := fmt.Sprintf("/v2/security_groups/%s", guid) + return repo.gateway.UpdateResourceFromStruct(repo.config.ApiEndpoint(), url, models.SecurityGroupParams{Rules: rules}) +} + +func (repo cloudControllerSecurityGroupRepo) FindAll() ([]models.SecurityGroup, error) { + path := "/v2/security_groups?inline-relations-depth=2" + securityGroups := []models.SecurityGroup{} + + err := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.SecurityGroupResource{}, + func(resource interface{}) bool { + if securityGroupResource, ok := resource.(resources.SecurityGroupResource); ok { + securityGroups = append(securityGroups, securityGroupResource.ToModel()) + } + + return true + }, + ) + + if err != nil { + return nil, err + } + + return securityGroups, err +} + +func (repo cloudControllerSecurityGroupRepo) Delete(securityGroupGuid string) error { + path := fmt.Sprintf("/v2/security_groups/%s", securityGroupGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/security_groups/security_groups_suite_test.go b/cf/api/security_groups/security_groups_suite_test.go new file mode 100644 index 00000000000..f443e4f8346 --- /dev/null +++ b/cf/api/security_groups/security_groups_suite_test.go @@ -0,0 +1,19 @@ +package security_groups_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSecurityGroups(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "SecurityGroups Suite") +} diff --git a/cf/api/security_groups/security_groups_test.go b/cf/api/security_groups/security_groups_test.go new file mode 100644 index 00000000000..839fb4e0a01 --- /dev/null +++ b/cf/api/security_groups/security_groups_test.go @@ -0,0 +1,280 @@ +package security_groups_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/security_groups" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("app security group api", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo SecurityGroupRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewSecurityGroupRepo(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".Create", func() { + It("can create an app security group, given some attributes", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/security_groups", + // FIXME: this matcher depend on the order of the key/value pairs in the map + Matcher: testnet.RequestBodyMatcher(`{ + "name": "mygroup", + "rules": [{"my-house": "my-rules"}] + }`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + setupTestServer(req) + + err := repo.Create( + "mygroup", + []map[string]interface{}{{"my-house": "my-rules"}}, + ) + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".Read", func() { + It("returns the app security group with the given name", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/security_groups?q=name:the-name&inline-relations-depth=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "the-group-guid" + }, + "entity": { + "name": "the-name", + "rules": [{"key": "value"}], + "spaces": [ + { + "metadata":{ + "guid": "my-space-guid" + }, + "entity": { + "name": "my-space", + "organization": { + "metadata": { + "guid": "my-org-guid" + }, + "entity": { + "name": "my-org" + } + } + } + } + ] + } + } + ] +} + `, + }, + })) + + group, err := repo.Read("the-name") + + Expect(err).ToNot(HaveOccurred()) + Expect(group).To(Equal(models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "the-name", + Guid: "the-group-guid", + Rules: []map[string]interface{}{{"key": "value"}}, + }, + Spaces: []models.Space{ + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid", Name: "my-space"}, + Organization: models.OrganizationFields{Guid: "my-org-guid", Name: "my-org"}, + }, + }, + })) + }) + + It("returns a ModelNotFound error if the security group cannot be found", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/security_groups?q=name:the-name&inline-relations-depth=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": []}`, + }, + })) + + _, err := repo.Read("the-name") + + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(errors.NewModelNotFoundError("model-type", "description"))) + }) + }) + + Describe(".Delete", func() { + It("deletes the security group", func() { + securityGroupGuid := "the-security-group-guid" + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/security_groups/" + securityGroupGuid, + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }, + })) + + err := repo.Delete(securityGroupGuid) + + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe(".FindAll", func() { + It("returns all the security groups", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/security_groups?inline-relations-depth=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: firstListItem(), + }, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/security_groups?inline-relations-depth=2&page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: secondListItem(), + }, + }), + ) + + groups, err := repo.FindAll() + + Expect(err).ToNot(HaveOccurred()) + Expect(groups[0]).To(Equal(models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "name-71", + Guid: "cd186158-b356-474d-9861-724f34f48502", + Rules: []map[string]interface{}{{"protocol": "udp"}}, + }, + Spaces: []models.Space{}, + })) + Expect(groups[1]).To(Equal(models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "name-72", + Guid: "d3374b62-7eac-4823-afbd-460d2bf44c67", + Rules: []map[string]interface{}{{"destination": "198.41.191.47/1"}}, + }, + Spaces: []models.Space{ + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid", Name: "my-space"}, + Organization: models.OrganizationFields{Guid: "my-org-guid", Name: "my-org"}, + }, + }, + })) + }) + }) +}) + +func firstListItem() string { + return `{ + "next_url": "/v2/security_groups?inline-relations-depth=2&page=2", + "resources": [ + { + "metadata": { + "guid": "cd186158-b356-474d-9861-724f34f48502", + "url": "/v2/security_groups/cd186158-b356-474d-9861-724f34f48502", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-71", + "rules": [ + { + "protocol": "udp" + } + ], + "spaces_url": "/v2/security_groups/cd186158-b356-474d-9861-724f34f48502/spaces" + } + } + ] +}` +} + +func secondListItem() string { + return `{ + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "d3374b62-7eac-4823-afbd-460d2bf44c67", + "url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67", + "created_at": "2014-06-23T22:55:30+00:00", + "updated_at": null + }, + "entity": { + "name": "name-72", + "rules": [ + { + "destination": "198.41.191.47/1" + } + ], + "spaces": [ + { + "metadata":{ + "guid": "my-space-guid" + }, + "entity": { + "name": "my-space", + "organization": { + "metadata": { + "guid": "my-org-guid" + }, + "entity": { + "name": "my-org" + } + } + } + } + ], + "spaces_url": "/v2/security_groups/d3374b62-7eac-4823-afbd-460d2bf44c67/spaces" + } + } + ] +}` +} diff --git a/cf/api/security_groups/spaces/fakes/fake_security_group_space_binder.go b/cf/api/security_groups/spaces/fakes/fake_security_group_space_binder.go new file mode 100644 index 00000000000..c2ebeaf9954 --- /dev/null +++ b/cf/api/security_groups/spaces/fakes/fake_security_group_space_binder.go @@ -0,0 +1,95 @@ +// This file was generated by counterfeiter +package tmp + +import ( + . "github.com/cloudfoundry/cli/cf/api/security_groups/spaces" + "sync" +) + +type FakeSecurityGroupSpaceBinder struct { + BindSpaceStub func(securityGroupGuid string, spaceGuid string) error + bindSpaceMutex sync.RWMutex + bindSpaceArgsForCall []struct { + arg1 string + arg2 string + } + bindSpaceReturns struct { + result1 error + } + + UnbindSpaceStub func(securityGroupGuid string, spaceGuid string) error + unbindSpaceMutex sync.RWMutex + unbindSpaceArgsForCall []struct { + arg1 string + arg2 string + } + unbindSpaceReturns struct { + result1 error + } +} + +func (fake *FakeSecurityGroupSpaceBinder) BindSpace(arg1 string, arg2 string) error { + fake.bindSpaceMutex.Lock() + defer fake.bindSpaceMutex.Unlock() + fake.bindSpaceArgsForCall = append(fake.bindSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.BindSpaceStub != nil { + return fake.BindSpaceStub(arg1, arg2) + } else { + return fake.bindSpaceReturns.result1 + } +} + +func (fake *FakeSecurityGroupSpaceBinder) BindSpaceCallCount() int { + fake.bindSpaceMutex.RLock() + defer fake.bindSpaceMutex.RUnlock() + return len(fake.bindSpaceArgsForCall) +} + +func (fake *FakeSecurityGroupSpaceBinder) BindSpaceArgsForCall(i int) (string, string) { + fake.bindSpaceMutex.RLock() + defer fake.bindSpaceMutex.RUnlock() + return fake.bindSpaceArgsForCall[i].arg1, fake.bindSpaceArgsForCall[i].arg2 +} + +func (fake *FakeSecurityGroupSpaceBinder) BindSpaceReturns(result1 error) { + fake.bindSpaceReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSecurityGroupSpaceBinder) UnbindSpace(arg1 string, arg2 string) error { + fake.unbindSpaceMutex.Lock() + defer fake.unbindSpaceMutex.Unlock() + fake.unbindSpaceArgsForCall = append(fake.unbindSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.UnbindSpaceStub != nil { + return fake.UnbindSpaceStub(arg1, arg2) + } else { + return fake.unbindSpaceReturns.result1 + } +} + +func (fake *FakeSecurityGroupSpaceBinder) UnbindSpaceCallCount() int { + fake.unbindSpaceMutex.RLock() + defer fake.unbindSpaceMutex.RUnlock() + return len(fake.unbindSpaceArgsForCall) +} + +func (fake *FakeSecurityGroupSpaceBinder) UnbindSpaceArgsForCall(i int) (string, string) { + fake.unbindSpaceMutex.RLock() + defer fake.unbindSpaceMutex.RUnlock() + return fake.unbindSpaceArgsForCall[i].arg1, fake.unbindSpaceArgsForCall[i].arg2 +} + +func (fake *FakeSecurityGroupSpaceBinder) UnbindSpaceReturns(result1 error) { + fake.unbindSpaceReturns = struct { + result1 error + }{result1} +} + +var _ SecurityGroupSpaceBinder = new(FakeSecurityGroupSpaceBinder) diff --git a/cf/api/security_groups/spaces/space_binder.go b/cf/api/security_groups/spaces/space_binder.go new file mode 100644 index 00000000000..8afc06d0239 --- /dev/null +++ b/cf/api/security_groups/spaces/space_binder.go @@ -0,0 +1,44 @@ +package spaces + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type SecurityGroupSpaceBinder interface { + BindSpace(securityGroupGuid string, spaceGuid string) error + UnbindSpace(securityGroupGuid string, spaceGuid string) error +} + +type securityGroupSpaceBinder struct { + configRepo core_config.Reader + gateway net.Gateway +} + +func NewSecurityGroupSpaceBinder(configRepo core_config.Reader, gateway net.Gateway) (binder securityGroupSpaceBinder) { + return securityGroupSpaceBinder{ + configRepo: configRepo, + gateway: gateway, + } +} + +func (repo securityGroupSpaceBinder) BindSpace(securityGroupGuid string, spaceGuid string) error { + url := fmt.Sprintf("/v2/security_groups/%s/spaces/%s", + securityGroupGuid, + spaceGuid, + ) + + return repo.gateway.UpdateResourceFromStruct(repo.configRepo.ApiEndpoint(), url, models.SecurityGroupParams{}) +} + +func (repo securityGroupSpaceBinder) UnbindSpace(securityGroupGuid string, spaceGuid string) error { + url := fmt.Sprintf("/v2/security_groups/%s/spaces/%s", + securityGroupGuid, + spaceGuid, + ) + + return repo.gateway.DeleteResource(repo.configRepo.ApiEndpoint(), url) +} diff --git a/cf/api/security_groups/spaces/space_binder_test.go b/cf/api/security_groups/spaces/space_binder_test.go new file mode 100644 index 00000000000..0f6d8478a5d --- /dev/null +++ b/cf/api/security_groups/spaces/space_binder_test.go @@ -0,0 +1,83 @@ +package spaces_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/security_groups/spaces" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SecurityGroupSpaceBinder", func() { + var ( + repo SecurityGroupSpaceBinder + gateway net.Gateway + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway = net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewSecurityGroupSpaceBinder(configRepo, gateway) + }) + + AfterEach(func() { testServer.Close() }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".BindSpace", func() { + It("associates the security group with the space", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/security_groups/this-is-a-security-group-guid/spaces/yes-its-a-space-guid", + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: ` +{ + "metadata": {"guid": "fb6fdf81-ce1b-448f-ada9-09bbb8807812"}, + "entity": {"name": "dummy1", "rules": [] } +}`, + }, + })) + + err := repo.BindSpace("this-is-a-security-group-guid", "yes-its-a-space-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe(".UnbindSpace", func() { + It("removes the associated security group from the space", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/security_groups/this-is-a-security-group-guid/spaces/yes-its-a-space-guid", + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }, + })) + + err := repo.UnbindSpace("this-is-a-security-group-guid", "yes-its-a-space-guid") + + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) +}) diff --git a/cf/api/security_groups/spaces/suite_test.go b/cf/api/security_groups/spaces/suite_test.go new file mode 100644 index 00000000000..c5ad4d2caf0 --- /dev/null +++ b/cf/api/security_groups/spaces/suite_test.go @@ -0,0 +1,19 @@ +package spaces_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSecurityGroupSpaces(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "SecurityGroupSpaces Suite") +} diff --git a/cf/api/service_auth_tokens.go b/cf/api/service_auth_tokens.go new file mode 100644 index 00000000000..40819676c13 --- /dev/null +++ b/cf/api/service_auth_tokens.go @@ -0,0 +1,85 @@ +package api + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServiceAuthTokenRepository interface { + FindAll() (authTokens []models.ServiceAuthTokenFields, apiErr error) + FindByLabelAndProvider(label, provider string) (authToken models.ServiceAuthTokenFields, apiErr error) + Create(authToken models.ServiceAuthTokenFields) (apiErr error) + Update(authToken models.ServiceAuthTokenFields) (apiErr error) + Delete(authToken models.ServiceAuthTokenFields) (apiErr error) +} + +type CloudControllerServiceAuthTokenRepository struct { + gateway net.Gateway + config core_config.Reader +} + +func NewCloudControllerServiceAuthTokenRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerServiceAuthTokenRepository) { + repo.gateway = gateway + repo.config = config + return +} + +func (repo CloudControllerServiceAuthTokenRepository) FindAll() (authTokens []models.ServiceAuthTokenFields, apiErr error) { + return repo.findAllWithPath("/v2/service_auth_tokens") +} + +func (repo CloudControllerServiceAuthTokenRepository) FindByLabelAndProvider(label, provider string) (authToken models.ServiceAuthTokenFields, apiErr error) { + path := fmt.Sprintf("/v2/service_auth_tokens?q=%s", url.QueryEscape("label:"+label+";provider:"+provider)) + authTokens, apiErr := repo.findAllWithPath(path) + if apiErr != nil { + return + } + + if len(authTokens) == 0 { + apiErr = errors.NewModelNotFoundError("Service Auth Token", label+" "+provider) + return + } + + authToken = authTokens[0] + return +} + +func (repo CloudControllerServiceAuthTokenRepository) findAllWithPath(path string) ([]models.ServiceAuthTokenFields, error) { + var authTokens []models.ServiceAuthTokenFields + apiErr := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.AuthTokenResource{}, + func(resource interface{}) bool { + if at, ok := resource.(resources.AuthTokenResource); ok { + authTokens = append(authTokens, at.ToFields()) + } + return true + }) + + return authTokens, apiErr +} + +func (repo CloudControllerServiceAuthTokenRepository) Create(authToken models.ServiceAuthTokenFields) (apiErr error) { + body := fmt.Sprintf(`{"label":"%s","provider":"%s","token":"%s"}`, authToken.Label, authToken.Provider, authToken.Token) + path := "/v2/service_auth_tokens" + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceAuthTokenRepository) Delete(authToken models.ServiceAuthTokenFields) (apiErr error) { + path := fmt.Sprintf("/v2/service_auth_tokens/%s", authToken.Guid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerServiceAuthTokenRepository) Update(authToken models.ServiceAuthTokenFields) (apiErr error) { + body := fmt.Sprintf(`{"token":"%s"}`, authToken.Token) + path := fmt.Sprintf("/v2/service_auth_tokens/%s", authToken.Guid) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} diff --git a/cf/api/service_auth_tokens_test.go b/cf/api/service_auth_tokens_test.go new file mode 100644 index 00000000000..1ec3681449c --- /dev/null +++ b/cf/api/service_auth_tokens_test.go @@ -0,0 +1,233 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServiceAuthTokensRepo", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerServiceAuthTokenRepository + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServiceAuthTokenRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("Create", func() { + It("creates a service auth token", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_auth_tokens", + Matcher: testnet.RequestBodyMatcher(`{"label":"a label","provider":"a provider","token":"a token"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + + err := repo.Create(models.ServiceAuthTokenFields{ + Label: "a label", + Provider: "a provider", + Token: "a token", + }) + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("FindAll", func() { + var firstServiceAuthTokenRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_auth_tokens", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "next_url": "/v2/service_auth_tokens?page=2", + "resources": [ + { + "metadata": { + "guid": "mongodb-core-guid" + }, + "entity": { + "label": "mongodb", + "provider": "mongodb-core" + } + } + ] + }`, + }, + }) + + var secondServiceAuthTokenRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_auth_tokens", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { + "metadata": { + "guid": "mysql-core-guid" + }, + "entity": { + "label": "mysql", + "provider": "mysql-core" + } + }, + { + "metadata": { + "guid": "postgres-core-guid" + }, + "entity": { + "label": "postgres", + "provider": "postgres-core" + } + } + ] + }`, + }, + }) + + BeforeEach(func() { + setupTestServer(firstServiceAuthTokenRequest, secondServiceAuthTokenRequest) + }) + + It("finds all service auth tokens", func() { + authTokens, err := repo.FindAll() + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(authTokens)).To(Equal(3)) + + Expect(authTokens[0].Label).To(Equal("mongodb")) + Expect(authTokens[0].Provider).To(Equal("mongodb-core")) + Expect(authTokens[0].Guid).To(Equal("mongodb-core-guid")) + + Expect(authTokens[1].Label).To(Equal("mysql")) + Expect(authTokens[1].Provider).To(Equal("mysql-core")) + Expect(authTokens[1].Guid).To(Equal("mysql-core-guid")) + }) + }) + + Describe("FindByLabelAndProvider", func() { + Context("when the auth token exists", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_auth_tokens?q=label%3Aa-label%3Bprovider%3Aa-provider", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "resources": [{ + "metadata": { "guid": "mysql-core-guid" }, + "entity": { + "label": "mysql", + "provider": "mysql-core" + } + }]}`, + }, + })) + }) + + It("returns the auth token", func() { + serviceAuthToken, err := repo.FindByLabelAndProvider("a-label", "a-provider") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(serviceAuthToken).To(Equal(models.ServiceAuthTokenFields{ + Guid: "mysql-core-guid", + Label: "mysql", + Provider: "mysql-core", + })) + }) + }) + + Context("when the auth token does not exist", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_auth_tokens?q=label%3Aa-label%3Bprovider%3Aa-provider", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": []}`}, + })) + }) + + It("returns a ModelNotFoundError", func() { + _, err := repo.FindByLabelAndProvider("a-label", "a-provider") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + }) + }) + }) + + Describe("Update", func() { + It("updates the service auth token", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_auth_tokens/mysql-core-guid", + Matcher: testnet.RequestBodyMatcher(`{"token":"a value"}`), + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.Update(models.ServiceAuthTokenFields{ + Guid: "mysql-core-guid", + Token: "a value", + }) + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Delete", func() { + It("deletes the service auth token", func() { + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/service_auth_tokens/mysql-core-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.Delete(models.ServiceAuthTokenFields{ + Guid: "mysql-core-guid", + }) + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/cf/api/service_bindings.go b/cf/api/service_bindings.go new file mode 100644 index 00000000000..86f81c262e3 --- /dev/null +++ b/cf/api/service_bindings.go @@ -0,0 +1,55 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServiceBindingRepository interface { + Create(instanceGuid, appGuid string) (apiErr error) + Delete(instance models.ServiceInstance, appGuid string) (found bool, apiErr error) +} + +type CloudControllerServiceBindingRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServiceBindingRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerServiceBindingRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerServiceBindingRepository) Create(instanceGuid, appGuid string) (apiErr error) { + path := "/v2/service_bindings" + body := fmt.Sprintf( + `{"app_guid":"%s","service_instance_guid":"%s","async":true}`, + appGuid, instanceGuid, + ) + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceBindingRepository) Delete(instance models.ServiceInstance, appGuid string) (found bool, apiErr error) { + var path string + + for _, binding := range instance.ServiceBindings { + if binding.AppGuid == appGuid { + path = binding.Url + break + } + } + + if path == "" { + return + } else { + found = true + } + + apiErr = repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) + return +} diff --git a/cf/api/service_bindings_test.go b/cf/api/service_bindings_test.go new file mode 100644 index 00000000000..39c94bb69e6 --- /dev/null +++ b/cf/api/service_bindings_test.go @@ -0,0 +1,137 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServiceBindingsRepository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerServiceBindingRepository + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServiceBindingRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("Create", func() { + Context("when the service binding can be created", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_bindings", + Matcher: testnet.RequestBodyMatcher(`{"app_guid":"my-app-guid","service_instance_guid":"my-service-instance-guid","async":true}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + }) + + It("creates the service binding", func() { + apiErr := repo.Create("my-service-instance-guid", "my-app-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_bindings", + Matcher: testnet.RequestBodyMatcher(`{"app_guid":"my-app-guid","service_instance_guid":"my-service-instance-guid","async":true}`), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{"code":90003,"description":"The app space binding to service is taken: 7b959018-110a-4913-ac0a-d663e613cdea 346bf237-7eef-41a7-b892-68fb08068f09"}`, + }, + })) + }) + + It("returns an error", func() { + apiErr := repo.Create("my-service-instance-guid", "my-app-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(Equal("90003")) + }) + }) + }) + + Describe("Delete", func() { + Context("when binding does exist", func() { + var serviceInstance models.ServiceInstance + + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/service_bindings/service-binding-2-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + serviceInstance.Guid = "my-service-instance-guid" + + binding := models.ServiceBindingFields{} + binding.Url = "/v2/service_bindings/service-binding-1-guid" + binding.AppGuid = "app-1-guid" + binding2 := models.ServiceBindingFields{} + binding2.Url = "/v2/service_bindings/service-binding-2-guid" + binding2.AppGuid = "app-2-guid" + serviceInstance.ServiceBindings = []models.ServiceBindingFields{binding, binding2} + }) + + It("deletes the service binding with the given guid", func() { + found, apiErr := repo.Delete(serviceInstance, "app-2-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + }) + }) + + Context("when binding does not exist", func() { + var serviceInstance models.ServiceInstance + + BeforeEach(func() { + setupTestServer() + serviceInstance.Guid = "my-service-instance-guid" + }) + + It("does not return an error", func() { + found, apiErr := repo.Delete(serviceInstance, "app-2-guid") + + Expect(testHandler.CallCount).To(Equal(0)) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(found).To(BeFalse()) + }) + }) + }) +}) diff --git a/cf/api/service_brokers.go b/cf/api/service_brokers.go new file mode 100644 index 00000000000..6733f9ecae2 --- /dev/null +++ b/cf/api/service_brokers.go @@ -0,0 +1,98 @@ +package api + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServiceBrokerRepository interface { + ListServiceBrokers(callback func(models.ServiceBroker) bool) error + FindByName(name string) (serviceBroker models.ServiceBroker, apiErr error) + FindByGuid(guid string) (serviceBroker models.ServiceBroker, apiErr error) + Create(name, url, username, password string) (apiErr error) + Update(serviceBroker models.ServiceBroker) (apiErr error) + Rename(guid, name string) (apiErr error) + Delete(guid string) (apiErr error) +} + +type CloudControllerServiceBrokerRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServiceBrokerRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerServiceBrokerRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerServiceBrokerRepository) ListServiceBrokers(callback func(models.ServiceBroker) bool) error { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + "/v2/service_brokers", + resources.ServiceBrokerResource{}, + func(resource interface{}) bool { + callback(resource.(resources.ServiceBrokerResource).ToFields()) + return true + }) +} + +func (repo CloudControllerServiceBrokerRepository) FindByName(name string) (serviceBroker models.ServiceBroker, apiErr error) { + foundBroker := false + apiErr = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/service_brokers?q=%s", url.QueryEscape("name:"+name)), + resources.ServiceBrokerResource{}, + func(resource interface{}) bool { + serviceBroker = resource.(resources.ServiceBrokerResource).ToFields() + foundBroker = true + return false + }) + + if !foundBroker { + apiErr = errors.NewModelNotFoundError("Service Broker", name) + } + + return +} +func (repo CloudControllerServiceBrokerRepository) FindByGuid(guid string) (serviceBroker models.ServiceBroker, apiErr error) { + broker := new(resources.ServiceBrokerResource) + apiErr = repo.gateway.GetResource(repo.config.ApiEndpoint()+fmt.Sprintf("/v2/service_brokers/%s", guid), broker) + serviceBroker = broker.ToFields() + return +} + +func (repo CloudControllerServiceBrokerRepository) Create(name, url, username, password string) (apiErr error) { + path := "/v2/service_brokers" + body := fmt.Sprintf( + `{"name":"%s","broker_url":"%s","auth_username":"%s","auth_password":"%s"}`, name, url, username, password, + ) + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceBrokerRepository) Update(serviceBroker models.ServiceBroker) (apiErr error) { + path := fmt.Sprintf("/v2/service_brokers/%s", serviceBroker.Guid) + body := fmt.Sprintf( + `{"broker_url":"%s","auth_username":"%s","auth_password":"%s"}`, + serviceBroker.Url, serviceBroker.Username, serviceBroker.Password, + ) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceBrokerRepository) Rename(guid, name string) (apiErr error) { + path := fmt.Sprintf("/v2/service_brokers/%s", guid) + body := fmt.Sprintf(`{"name":"%s"}`, name) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceBrokerRepository) Delete(guid string) (apiErr error) { + path := fmt.Sprintf("/v2/service_brokers/%s", guid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/service_brokers_test.go b/cf/api/service_brokers_test.go new file mode 100644 index 00000000000..d88072ba5c8 --- /dev/null +++ b/cf/api/service_brokers_test.go @@ -0,0 +1,291 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service Brokers Repo", func() { + It("lists services brokers", func() { + firstRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/service_brokers?page=2", + "resources": [ + { + "metadata": { + "guid":"found-guid-1" + }, + "entity": { + "name": "found-name-1", + "broker_url": "http://found.example.com-1", + "auth_username": "found-username-1", + "auth_password": "found-password-1" + } + } + ] + }`, + }, + }) + + secondRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { + "metadata": { + "guid":"found-guid-2" + }, + "entity": { + "name": "found-name-2", + "broker_url": "http://found.example.com-2", + "auth_username": "found-username-2", + "auth_password": "found-password-2" + } + } + ] + }`, + }, + }) + + ts, handler, repo := createServiceBrokerRepo(firstRequest, secondRequest) + defer ts.Close() + + serviceBrokers := []models.ServiceBroker{} + apiErr := repo.ListServiceBrokers(func(broker models.ServiceBroker) bool { + serviceBrokers = append(serviceBrokers, broker) + return true + }) + + Expect(len(serviceBrokers)).To(Equal(2)) + Expect(serviceBrokers[0].Guid).To(Equal("found-guid-1")) + Expect(serviceBrokers[1].Guid).To(Equal("found-guid-2")) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + Describe("FindByName", func() { + It("returns the service broker with the given name", func() { + responseBody := ` +{"resources": [{ + "metadata": {"guid":"found-guid"}, + "entity": { + "name": "found-name", + "broker_url": "http://found.example.com", + "auth_username": "found-username", + "auth_password": "found-password" + } +}]}` + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers?q=name%3Amy-broker", + Response: testnet.TestResponse{Status: http.StatusOK, Body: responseBody}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + foundBroker, apiErr := repo.FindByName("my-broker") + expectedBroker := models.ServiceBroker{} + expectedBroker.Name = "found-name" + expectedBroker.Url = "http://found.example.com" + expectedBroker.Username = "found-username" + expectedBroker.Password = "found-password" + expectedBroker.Guid = "found-guid" + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(foundBroker).To(Equal(expectedBroker)) + }) + + It("returns an error when the service broker cannot be found", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers?q=name%3Amy-broker", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [ ] }`}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + _, apiErr := repo.FindByName("my-broker") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.Error()).To(Equal("Service Broker my-broker not found")) + }) + }) + + Describe("FindByGuid", func() { + It("returns the service broker with the given guid", func() { + responseBody := ` +{ + "metadata": { + "guid": "found-guid", + "url": "/v2/service_brokers/found-guid", + "created_at": "2014-07-24T21:21:54+00:00", + "updated_at": "2014-07-25T17:03:40+00:00" + }, + "entity": { + "name": "found-name", + "broker_url": "http://found.example.com", + "auth_username": "found-username" + } +} +` + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers/found-guid", + Response: testnet.TestResponse{Status: http.StatusOK, Body: responseBody}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + foundBroker, apiErr := repo.FindByGuid("found-guid") + expectedBroker := models.ServiceBroker{} + expectedBroker.Name = "found-name" + expectedBroker.Url = "http://found.example.com" + expectedBroker.Username = "found-username" + expectedBroker.Guid = "found-guid" + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(foundBroker).To(Equal(expectedBroker)) + }) + + It("returns an error when the service broker cannot be found", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_brokers/bogus-guid", + //This error code may not reflect reality. Check it, change the code to match, and remove this comment. + Response: testnet.TestResponse{Status: http.StatusNotFound, Body: `{"error_code":"ServiceBrokerNotFound","description":"Service Broker bogus-guid not found","code":270042}`}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + _, apiErr := repo.FindByGuid("bogus-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.Error()).To(Equal("Server error, status code: 404, error code: 270042, message: Service Broker bogus-guid not found")) + }) + }) + + Describe("Create", func() { + It("creates the service broker with the given name, URL, username and password", func() { + expectedReqBody := `{"name":"foobroker","broker_url":"http://example.com","auth_username":"foouser","auth_password":"password"}` + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_brokers", + Matcher: testnet.RequestBodyMatcher(expectedReqBody), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + apiErr := repo.Create("foobroker", "http://example.com", "foouser", "password") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("Update", func() { + It("updates the service broker with the given guid", func() { + expectedReqBody := `{"broker_url":"http://update.example.com","auth_username":"update-foouser","auth_password":"update-password"}` + + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_brokers/my-guid", + Matcher: testnet.RequestBodyMatcher(expectedReqBody), + Response: testnet.TestResponse{Status: http.StatusOK}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + serviceBroker := models.ServiceBroker{} + serviceBroker.Guid = "my-guid" + serviceBroker.Name = "foobroker" + serviceBroker.Url = "http://update.example.com" + serviceBroker.Username = "update-foouser" + serviceBroker.Password = "update-password" + + apiErr := repo.Update(serviceBroker) + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("Rename", func() { + It("renames the service broker with the given guid", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_brokers/my-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"update-foobroker"}`), + Response: testnet.TestResponse{Status: http.StatusOK}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + apiErr := repo.Rename("my-guid", "update-foobroker") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + Describe("Delete", func() { + It("deletes the service broker with the given guid", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/service_brokers/my-guid", + Response: testnet.TestResponse{Status: http.StatusNoContent}, + }) + + ts, handler, repo := createServiceBrokerRepo(req) + defer ts.Close() + + apiErr := repo.Delete("my-guid") + + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) +}) + +func createServiceBrokerRepo(requests ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceBrokerRepository) { + ts, handler = testnet.NewServer(requests) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServiceBrokerRepository(configRepo, gateway) + return +} diff --git a/cf/api/service_plan.go b/cf/api/service_plan.go new file mode 100644 index 00000000000..2e5cd62641c --- /dev/null +++ b/cf/api/service_plan.go @@ -0,0 +1,71 @@ +package api + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServicePlanRepository interface { + Search(searchParameters map[string]string) ([]models.ServicePlanFields, error) + Update(models.ServicePlanFields, string, bool) error +} + +type CloudControllerServicePlanRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServicePlanRepository(config core_config.Reader, gateway net.Gateway) CloudControllerServicePlanRepository { + return CloudControllerServicePlanRepository{ + config: config, + gateway: gateway, + } +} + +func (repo CloudControllerServicePlanRepository) Update(servicePlan models.ServicePlanFields, serviceGuid string, public bool) error { + var body string + + body = fmt.Sprintf(`{"name":"%s", "free":%t, "description":"%s", "public":%t, "service_guid":"%s"}`, + servicePlan.Name, + servicePlan.Free, + servicePlan.Description, + public, + serviceGuid, + ) + + url := fmt.Sprintf("/v2/service_plans/%s", servicePlan.Guid) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), url, strings.NewReader(body)) +} + +func (repo CloudControllerServicePlanRepository) Search(queryParams map[string]string) (plans []models.ServicePlanFields, err error) { + err = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + combineQueryParametersWithUri("/v2/service_plans", queryParams), + resources.ServicePlanResource{}, + func(resource interface{}) bool { + if sp, ok := resource.(resources.ServicePlanResource); ok { + plans = append(plans, sp.ToFields()) + } + return true + }) + return +} + +func combineQueryParametersWithUri(uri string, queryParams map[string]string) string { + if len(queryParams) == 0 { + return uri + } + + params := []string{} + for key, value := range queryParams { + params = append(params, url.QueryEscape(key+":"+value)) + } + + return uri + "?q=" + strings.Join(params, "%3B") +} diff --git a/cf/api/service_plan_test.go b/cf/api/service_plan_test.go new file mode 100644 index 00000000000..434f55106f1 --- /dev/null +++ b/cf/api/service_plan_test.go @@ -0,0 +1,220 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service Plan Repository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerServicePlanRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServicePlanRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".Search", func() { + Context("No query parameters", func() { + BeforeEach(func() { + setupTestServer(firstPlanRequest, secondPlanRequest) + }) + + It("returns service plans", func() { + servicePlansFields, err := repo.Search(map[string]string{}) + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(servicePlansFields)).To(Equal(2)) + Expect(servicePlansFields[0].Name).To(Equal("The big one")) + Expect(servicePlansFields[0].Guid).To(Equal("the-big-guid")) + Expect(servicePlansFields[0].Free).To(BeTrue()) + Expect(servicePlansFields[0].Public).To(BeTrue()) + Expect(servicePlansFields[0].Active).To(BeTrue()) + Expect(servicePlansFields[1].Name).To(Equal("The small second")) + Expect(servicePlansFields[1].Guid).To(Equal("the-small-second")) + Expect(servicePlansFields[1].Free).To(BeTrue()) + Expect(servicePlansFields[1].Public).To(BeFalse()) + Expect(servicePlansFields[1].Active).To(BeFalse()) + }) + }) + Context("With query parameters", func() { + BeforeEach(func() { + setupTestServer(firstPlanRequestWithParams, secondPlanRequestWithParams) + }) + + It("returns service plans", func() { + servicePlansFields, err := repo.Search(map[string]string{"service_guid": "Foo"}) + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(servicePlansFields)).To(Equal(2)) + Expect(servicePlansFields[0].Name).To(Equal("The big one")) + Expect(servicePlansFields[0].Guid).To(Equal("the-big-guid")) + Expect(servicePlansFields[0].Free).To(BeTrue()) + Expect(servicePlansFields[0].Public).To(BeTrue()) + Expect(servicePlansFields[0].Active).To(BeTrue()) + Expect(servicePlansFields[1].Name).To(Equal("The small second")) + Expect(servicePlansFields[1].Guid).To(Equal("the-small-second")) + Expect(servicePlansFields[1].Free).To(BeTrue()) + Expect(servicePlansFields[1].Public).To(BeFalse()) + Expect(servicePlansFields[1].Active).To(BeFalse()) + }) + }) + }) + + Describe(".Update", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_plans/my-service-plan-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-service-plan", "free":true, "description":"descriptive text", "public":true, "service_guid":"service-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + }) + + It("Updates the service to public", func() { + servicePlan := models.ServicePlanFields{ + Name: "my-service-plan", + Guid: "my-service-plan-guid", + Description: "descriptive text", + Free: true, + Public: false, + } + + err := repo.Update(servicePlan, "service-guid", true) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +var firstPlanRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plans", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "next_url": "/v2/service_plans?page=2", + "resources": [ + { + "metadata": { + "guid": "the-big-guid" + }, + "entity": { + "name": "The big one", + "free": true, + "public": true, + "active": true + } + } + ] +}`, + }, +}) + +var secondPlanRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plans?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "resources": [ + { + "metadata": { + "guid": "the-small-second" + }, + "entity": { + "name": "The small second", + "free": true, + "public": false, + "active": false + } + } + ] +}`, + }, +}) + +var firstPlanRequestWithParams = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plans?q=service_guid%3AFoo", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "next_url": "/v2/service_plans?q=service_guid%3AFoo&page=2", + "resources": [ + { + "metadata": { + "guid": "the-big-guid" + }, + "entity": { + "name": "The big one", + "free": true, + "public": true, + "active": true + } + } + ] +}`, + }, +}) + +var secondPlanRequestWithParams = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plans?q=service_guid%3AFoo&page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "resources": [ + { + "metadata": { + "guid": "the-small-second" + }, + "entity": { + "name": "The small second", + "free": true, + "public": false, + "active": false + } + } + ] +}`, + }, +}) diff --git a/cf/api/service_plan_visibility.go b/cf/api/service_plan_visibility.go new file mode 100644 index 00000000000..9e503a7ce6a --- /dev/null +++ b/cf/api/service_plan_visibility.go @@ -0,0 +1,70 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServicePlanVisibilityRepository interface { + Create(string, string) error + List() ([]models.ServicePlanVisibilityFields, error) + Delete(string) error + Search(map[string]string) ([]models.ServicePlanVisibilityFields, error) +} + +type CloudControllerServicePlanVisibilityRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServicePlanVisibilityRepository(config core_config.Reader, gateway net.Gateway) CloudControllerServicePlanVisibilityRepository { + return CloudControllerServicePlanVisibilityRepository{ + config: config, + gateway: gateway, + } +} + +func (repo CloudControllerServicePlanVisibilityRepository) Create(serviceGuid, orgGuid string) error { + url := "/v2/service_plan_visibilities" + data := fmt.Sprintf(`{"service_plan_guid":"%s", "organization_guid":"%s"}`, serviceGuid, orgGuid) + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), url, strings.NewReader(data)) +} + +func (repo CloudControllerServicePlanVisibilityRepository) List() (visibilities []models.ServicePlanVisibilityFields, err error) { + err = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + "/v2/service_plan_visibilities", + resources.ServicePlanVisibilityResource{}, + func(resource interface{}) bool { + if spv, ok := resource.(resources.ServicePlanVisibilityResource); ok { + visibilities = append(visibilities, spv.ToFields()) + } + return true + }) + return +} + +func (repo CloudControllerServicePlanVisibilityRepository) Delete(servicePlanGuid string) error { + path := fmt.Sprintf("/v2/service_plan_visibilities/%s", servicePlanGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerServicePlanVisibilityRepository) Search(queryParams map[string]string) ([]models.ServicePlanVisibilityFields, error) { + var visibilities []models.ServicePlanVisibilityFields + err := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + combineQueryParametersWithUri("/v2/service_plan_visibilities", queryParams), + resources.ServicePlanVisibilityResource{}, + func(resource interface{}) bool { + if sp, ok := resource.(resources.ServicePlanVisibilityResource); ok { + visibilities = append(visibilities, sp.ToFields()) + } + return true + }) + return visibilities, err +} diff --git a/cf/api/service_plan_visibility_test.go b/cf/api/service_plan_visibility_test.go new file mode 100644 index 00000000000..c4fd3c75613 --- /dev/null +++ b/cf/api/service_plan_visibility_test.go @@ -0,0 +1,181 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service Plan Visibility Repository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerServicePlanVisibilityRepository + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServicePlanVisibilityRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + Describe(".Create", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_plan_visibilities", + Matcher: testnet.RequestBodyMatcher(`{"service_plan_guid":"service_plan_guid", "organization_guid":"org_guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + }) + + It("creates a service plan visibility", func() { + err := repo.Create("service_plan_guid", "org_guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe(".List", func() { + BeforeEach(func() { + setupTestServer(firstPlanVisibilityRequest, secondPlanVisibilityRequest) + }) + + It("returns service plans", func() { + servicePlansVisibilitiesFields, err := repo.List() + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(servicePlansVisibilitiesFields)).To(Equal(2)) + Expect(servicePlansVisibilitiesFields[0].Guid).To(Equal("request-guid-1")) + Expect(servicePlansVisibilitiesFields[0].ServicePlanGuid).To(Equal("service-plan-guid-1")) + Expect(servicePlansVisibilitiesFields[0].OrganizationGuid).To(Equal("org-guid-1")) + Expect(servicePlansVisibilitiesFields[1].Guid).To(Equal("request-guid-2")) + Expect(servicePlansVisibilitiesFields[1].ServicePlanGuid).To(Equal("service-plan-guid-2")) + Expect(servicePlansVisibilitiesFields[1].OrganizationGuid).To(Equal("org-guid-2")) + }) + }) + + Describe(".Delete", func() { + It("deletes a service plan visibility", func() { + servicePlanVisibilityGuid := "the-service-plan-visibility-guid" + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/service_plan_visibilities/" + servicePlanVisibilityGuid, + Response: testnet.TestResponse{ + Status: http.StatusNoContent, + }, + })) + + err := repo.Delete(servicePlanVisibilityGuid) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe(".Search", func() { + It("finds the service plan visibilities that match the given query parameters", func() { + setupTestServer(searchPlanVisibilityRequest) + + servicePlansVisibilitiesFields, err := repo.Search(map[string]string{"service_plan_guid": "service-plan-guid-1", "organization_guid": "org-guid-1"}) + Expect(err).ToNot(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(servicePlansVisibilitiesFields)).To(Equal(1)) + Expect(servicePlansVisibilitiesFields[0].Guid).To(Equal("request-guid-1")) + Expect(servicePlansVisibilitiesFields[0].ServicePlanGuid).To(Equal("service-plan-guid-1")) + Expect(servicePlansVisibilitiesFields[0].OrganizationGuid).To(Equal("org-guid-1")) + }) + }) +}) + +var firstPlanVisibilityRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plan_visibilities", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "next_url": "/v2/service_plan_visibilities?page=2", + "resources": [ + { + "metadata": { + "guid": "request-guid-1" + }, + "entity": { + "service_plan_guid": "service-plan-guid-1", + "organization_guid": "org-guid-1" + } + } + ] +}`, + }, +}) + +var secondPlanVisibilityRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plan_visibilities?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 2, + "total_pages": 2, + "resources": [ + { + "metadata": { + "guid": "request-guid-2" + }, + "entity": { + "service_plan_guid": "service-plan-guid-2", + "organization_guid": "org-guid-2" + } + } + ] +}`, + }, +}) + +var searchPlanVisibilityRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/service_plan_visibilities?q=service_plan_guid%3Aservice-plan-guid-1%3Borganization_guid%3Aorg-guid-1", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "total_results": 1, + "total_pages": 1, + "resources": [ + { + "metadata": { + "guid": "request-guid-1" + }, + "entity": { + "service_plan_guid": "service-plan-guid-1", + "organization_guid": "org-guid-1" + } + } + ] +}`, + }, +}) diff --git a/cf/api/service_summary.go b/cf/api/service_summary.go new file mode 100644 index 00000000000..1e64b7632dc --- /dev/null +++ b/cf/api/service_summary.go @@ -0,0 +1,103 @@ +package api + +import ( + "fmt" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServiceInstancesSummaries struct { + Apps []ServiceInstanceSummaryApp + ServiceInstances []ServiceInstanceSummary `json:"services"` +} + +func (resource ServiceInstancesSummaries) ToModels() (instances []models.ServiceInstance) { + for _, instanceSummary := range resource.ServiceInstances { + applicationNames := resource.findApplicationNamesForInstance(instanceSummary.Name) + + planSummary := instanceSummary.ServicePlan + servicePlan := models.ServicePlanFields{} + servicePlan.Name = planSummary.Name + servicePlan.Guid = planSummary.Guid + + offeringSummary := planSummary.ServiceOffering + serviceOffering := models.ServiceOfferingFields{} + serviceOffering.Label = offeringSummary.Label + serviceOffering.Provider = offeringSummary.Provider + serviceOffering.Version = offeringSummary.Version + + instance := models.ServiceInstance{} + instance.Name = instanceSummary.Name + instance.ApplicationNames = applicationNames + instance.ServicePlan = servicePlan + instance.ServiceOffering = serviceOffering + + instances = append(instances, instance) + } + + return +} + +func (resource ServiceInstancesSummaries) findApplicationNamesForInstance(instanceName string) (applicationNames []string) { + for _, app := range resource.Apps { + for _, name := range app.ServiceNames { + if name == instanceName { + applicationNames = append(applicationNames, app.Name) + } + } + } + + return +} + +type ServiceInstanceSummaryApp struct { + Name string + ServiceNames []string `json:"service_names"` +} + +type ServiceInstanceSummary struct { + Name string + ServicePlan ServicePlanSummary `json:"service_plan"` +} + +type ServicePlanSummary struct { + Name string + Guid string + ServiceOffering ServiceOfferingSummary `json:"service"` +} + +type ServiceOfferingSummary struct { + Label string + Provider string + Version string +} + +type ServiceSummaryRepository interface { + GetSummariesInCurrentSpace() (instances []models.ServiceInstance, apiErr error) +} + +type CloudControllerServiceSummaryRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServiceSummaryRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerServiceSummaryRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerServiceSummaryRepository) GetSummariesInCurrentSpace() (instances []models.ServiceInstance, apiErr error) { + path := fmt.Sprintf("%s/v2/spaces/%s/summary", repo.config.ApiEndpoint(), repo.config.SpaceFields().Guid) + resource := new(ServiceInstancesSummaries) + + apiErr = repo.gateway.GetResource(path, resource) + if apiErr != nil { + return + } + + instances = resource.ToModels() + + return +} diff --git a/cf/api/service_summary_test.go b/cf/api/service_summary_test.go new file mode 100644 index 00000000000..5088e13b93f --- /dev/null +++ b/cf/api/service_summary_test.go @@ -0,0 +1,96 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServiceSummaryRepository", func() { + var serviceInstanceSummariesResponse testnet.TestResponse + + BeforeEach(func() { + serviceInstanceSummariesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "apps":[ + { + "name":"app1", + "service_names":[ + "my-service-instance" + ] + },{ + "name":"app2", + "service_names":[ + "my-service-instance" + ] + } + ], + "services": [ + { + "guid": "my-service-instance-guid", + "name": "my-service-instance", + "bound_app_count": 2, + "service_plan": { + "guid": "service-plan-guid", + "name": "spark", + "service": { + "guid": "service-offering-guid", + "label": "cleardb", + "provider": "cleardb-provider", + "version": "n/a" + } + } + } + ] + }`, + } + }) + + It("gets a summary of services in the given space", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/summary", + Response: serviceInstanceSummariesResponse, + }) + + ts, handler, repo := createServiceSummaryRepo(req) + defer ts.Close() + + serviceInstances, apiErr := repo.GetSummariesInCurrentSpace() + Expect(handler).To(HaveAllRequestsCalled()) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(1).To(Equal(len(serviceInstances))) + + instance1 := serviceInstances[0] + Expect(instance1.Name).To(Equal("my-service-instance")) + Expect(instance1.ServicePlan.Name).To(Equal("spark")) + Expect(instance1.ServiceOffering.Label).To(Equal("cleardb")) + Expect(instance1.ServiceOffering.Label).To(Equal("cleardb")) + Expect(instance1.ServiceOffering.Provider).To(Equal("cleardb-provider")) + Expect(instance1.ServiceOffering.Version).To(Equal("n/a")) + Expect(len(instance1.ApplicationNames)).To(Equal(2)) + Expect(instance1.ApplicationNames[0]).To(Equal("app1")) + Expect(instance1.ApplicationNames[1]).To(Equal("app2")) + }) +}) + +func createServiceSummaryRepo(req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceSummaryRepository) { + ts, handler = testnet.NewServer([]testnet.TestRequest{req}) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServiceSummaryRepository(configRepo, gateway) + return +} diff --git a/cf/api/services.go b/cf/api/services.go new file mode 100644 index 00000000000..63bf8dd8b0d --- /dev/null +++ b/cf/api/services.go @@ -0,0 +1,273 @@ +package api + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type ServiceRepository interface { + PurgeServiceOffering(offering models.ServiceOffering) error + GetServiceOfferingByGuid(serviceGuid string) (offering models.ServiceOffering, apiErr error) + FindServiceOfferingsByLabel(name string) (offering models.ServiceOfferings, apiErr error) + FindServiceOfferingByLabelAndProvider(name, provider string) (offering models.ServiceOffering, apiErr error) + + FindServiceOfferingsForSpaceByLabel(spaceGuid, name string) (offering models.ServiceOfferings, apiErr error) + + GetAllServiceOfferings() (offerings models.ServiceOfferings, apiErr error) + GetServiceOfferingsForSpace(spaceGuid string) (offerings models.ServiceOfferings, apiErr error) + FindInstanceByName(name string) (instance models.ServiceInstance, apiErr error) + CreateServiceInstance(name, planGuid string) (apiErr error) + UpdateServiceInstance(instanceGuid, planGuid string) (apiErr error) + RenameService(instance models.ServiceInstance, newName string) (apiErr error) + DeleteService(instance models.ServiceInstance) (apiErr error) + FindServicePlanByDescription(planDescription resources.ServicePlanDescription) (planGuid string, apiErr error) + ListServicesFromBroker(brokerGuid string) (services []models.ServiceOffering, err error) + GetServiceInstanceCountForServicePlan(v1PlanGuid string) (count int, apiErr error) + MigrateServicePlanFromV1ToV2(v1PlanGuid, v2PlanGuid string) (changedCount int, apiErr error) +} + +type CloudControllerServiceRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerServiceRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerServiceRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerServiceRepository) GetServiceOfferingByGuid(serviceGuid string) (models.ServiceOffering, error) { + offering := new(resources.ServiceOfferingResource) + apiErr := repo.gateway.GetResource(repo.config.ApiEndpoint()+fmt.Sprintf("/v2/services/%s", serviceGuid), offering) + serviceOffering := offering.ToFields() + return models.ServiceOffering{ServiceOfferingFields: serviceOffering}, apiErr +} + +func (repo CloudControllerServiceRepository) GetServiceOfferingsForSpace(spaceGuid string) (models.ServiceOfferings, error) { + return repo.getServiceOfferings(fmt.Sprintf("/v2/spaces/%s/services", spaceGuid)) +} + +func (repo CloudControllerServiceRepository) FindServiceOfferingsForSpaceByLabel(spaceGuid, name string) (offerings models.ServiceOfferings, err error) { + offerings, err = repo.getServiceOfferings(fmt.Sprintf("/v2/spaces/%s/services?q=%s", spaceGuid, url.QueryEscape("label:"+name))) + + if httpErr, ok := err.(errors.HttpError); ok && httpErr.ErrorCode() == errors.BAD_QUERY_PARAM { + offerings, err = repo.findServiceOfferingsByPaginating(spaceGuid, name) + } + + if err == nil && len(offerings) == 0 { + err = errors.NewModelNotFoundError("Service offering", name) + } + + return +} + +func (repo CloudControllerServiceRepository) findServiceOfferingsByPaginating(spaceGuid, label string) (offerings models.ServiceOfferings, apiErr error) { + offerings, apiErr = repo.GetServiceOfferingsForSpace(spaceGuid) + if apiErr != nil { + return + } + + matchingOffering := models.ServiceOfferings{} + + for _, offering := range offerings { + if offering.Label == label { + matchingOffering = append(matchingOffering, offering) + } + } + return matchingOffering, nil +} + +func (repo CloudControllerServiceRepository) GetAllServiceOfferings() (models.ServiceOfferings, error) { + return repo.getServiceOfferings("/v2/services") +} + +func (repo CloudControllerServiceRepository) getServiceOfferings(path string) ([]models.ServiceOffering, error) { + var offerings []models.ServiceOffering + apiErr := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.ServiceOfferingResource{}, + func(resource interface{}) bool { + if so, ok := resource.(resources.ServiceOfferingResource); ok { + offerings = append(offerings, so.ToModel()) + } + return true + }) + + return offerings, apiErr +} + +func (repo CloudControllerServiceRepository) FindInstanceByName(name string) (instance models.ServiceInstance, apiErr error) { + path := fmt.Sprintf("%s/v2/spaces/%s/service_instances?return_user_provided_service_instances=true&q=%s&inline-relations-depth=1", repo.config.ApiEndpoint(), repo.config.SpaceFields().Guid, url.QueryEscape("name:"+name)) + + responseJSON := new(resources.PaginatedServiceInstanceResources) + apiErr = repo.gateway.GetResource(path, responseJSON) + if apiErr != nil { + return + } + + if len(responseJSON.Resources) == 0 { + apiErr = errors.NewModelNotFoundError("Service instance", name) + return + } + + instanceResource := responseJSON.Resources[0] + instance = instanceResource.ToModel() + + if instanceResource.Entity.ServicePlan.Metadata.Guid != "" { + resource := &resources.ServiceOfferingResource{} + path = fmt.Sprintf("%s/v2/services/%s", repo.config.ApiEndpoint(), instanceResource.Entity.ServicePlan.Entity.ServiceOfferingGuid) + apiErr = repo.gateway.GetResource(path, resource) + instance.ServiceOffering = resource.ToFields() + } + + return +} + +func (repo CloudControllerServiceRepository) CreateServiceInstance(name, planGuid string) (err error) { + path := "/v2/service_instances" + data := fmt.Sprintf( + `{"name":"%s","service_plan_guid":"%s","space_guid":"%s", "async": true}`, + name, planGuid, repo.config.SpaceFields().Guid, + ) + + err = repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(data)) + + if httpErr, ok := err.(errors.HttpError); ok && httpErr.ErrorCode() == errors.SERVICE_INSTANCE_NAME_TAKEN { + serviceInstance, findInstanceErr := repo.FindInstanceByName(name) + + if findInstanceErr == nil && serviceInstance.ServicePlan.Guid == planGuid { + return errors.NewModelAlreadyExistsError("Service", name) + } + } + + return +} + +func (repo CloudControllerServiceRepository) UpdateServiceInstance(instanceGuid, planGuid string) (err error) { + path := fmt.Sprintf("/v2/service_instances/%s", instanceGuid) + data := fmt.Sprintf(`{"service_plan_guid":"%s"}`, planGuid) + + err = repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(data)) + + return +} + +func (repo CloudControllerServiceRepository) RenameService(instance models.ServiceInstance, newName string) (apiErr error) { + body := fmt.Sprintf(`{"name":"%s"}`, newName) + path := fmt.Sprintf("/v2/service_instances/%s", instance.Guid) + + if instance.IsUserProvided() { + path = fmt.Sprintf("/v2/user_provided_service_instances/%s", instance.Guid) + } + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerServiceRepository) DeleteService(instance models.ServiceInstance) (apiErr error) { + if len(instance.ServiceBindings) > 0 { + return errors.New("Cannot delete service instance, apps are still bound to it") + } + path := fmt.Sprintf("/v2/service_instances/%s", instance.Guid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerServiceRepository) PurgeServiceOffering(offering models.ServiceOffering) error { + url := fmt.Sprintf("/v2/services/%s?purge=true", offering.Guid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), url) +} + +func (repo CloudControllerServiceRepository) FindServiceOfferingsByLabel(label string) (models.ServiceOfferings, error) { + path := fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:"+label)) + offerings, apiErr := repo.getServiceOfferings(path) + + if apiErr != nil { + return models.ServiceOfferings{}, apiErr + } else if len(offerings) == 0 { + apiErr = errors.NewModelNotFoundError("Service offering", label) + return models.ServiceOfferings{}, apiErr + } + + return offerings, apiErr +} + +func (repo CloudControllerServiceRepository) FindServiceOfferingByLabelAndProvider(label, provider string) (models.ServiceOffering, error) { + path := fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:"+label+";provider:"+provider)) + offerings, apiErr := repo.getServiceOfferings(path) + + if apiErr != nil { + return models.ServiceOffering{}, apiErr + } else if len(offerings) == 0 { + apiErr = errors.NewModelNotFoundError("Service offering", label+" "+provider) + return models.ServiceOffering{}, apiErr + } + + return offerings[0], apiErr +} + +func (repo CloudControllerServiceRepository) FindServicePlanByDescription(planDescription resources.ServicePlanDescription) (string, error) { + path := fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", + url.QueryEscape("label:"+planDescription.ServiceLabel+";provider:"+planDescription.ServiceProvider)) + + var planGuid string + offerings, apiErr := repo.getServiceOfferings(path) + if apiErr != nil { + return planGuid, apiErr + } + + for _, serviceOfferingResource := range offerings { + for _, servicePlanResource := range serviceOfferingResource.Plans { + if servicePlanResource.Name == planDescription.ServicePlanName { + planGuid := servicePlanResource.Guid + return planGuid, apiErr + } + } + } + + apiErr = errors.NewModelNotFoundError("Plan", planDescription.String()) + + return planGuid, apiErr +} + +func (repo CloudControllerServiceRepository) ListServicesFromBroker(brokerGuid string) (offerings []models.ServiceOffering, err error) { + err = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("service_broker_guid:"+brokerGuid)), + resources.ServiceOfferingResource{}, + func(resource interface{}) bool { + if offering, ok := resource.(resources.ServiceOfferingResource); ok { + offerings = append(offerings, offering.ToModel()) + } + return true + }) + return +} + +func (repo CloudControllerServiceRepository) MigrateServicePlanFromV1ToV2(v1PlanGuid, v2PlanGuid string) (changedCount int, apiErr error) { + path := fmt.Sprintf("/v2/service_plans/%s/service_instances", v1PlanGuid) + body := strings.NewReader(fmt.Sprintf(`{"service_plan_guid":"%s"}`, v2PlanGuid)) + response := new(resources.ServiceMigrateV1ToV2Response) + + apiErr = repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, body, response) + if apiErr != nil { + return + } + + changedCount = response.ChangedCount + return +} + +func (repo CloudControllerServiceRepository) GetServiceInstanceCountForServicePlan(v1PlanGuid string) (count int, apiErr error) { + path := fmt.Sprintf("%s/v2/service_plans/%s/service_instances?results-per-page=1", repo.config.ApiEndpoint(), v1PlanGuid) + response := new(resources.PaginatedServiceInstanceResources) + apiErr = repo.gateway.GetResource(path, response) + count = response.TotalResults + return +} diff --git a/cf/api/services_test.go b/cf/api/services_test.go new file mode 100644 index 00000000000..6819fbedd39 --- /dev/null +++ b/cf/api/services_test.go @@ -0,0 +1,1250 @@ +package api_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Services Repo", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo ServiceRepository + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + configRepo.SetAccessToken("BEARER my_access_token") + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerServiceRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("GetAllServiceOfferings", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/services", + Response: firstOfferingsResponse, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/services", + Response: multipleOfferingsResponse, + }), + ) + }) + + It("gets all public service offerings", func() { + offerings, err := repo.GetAllServiceOfferings() + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(len(offerings)).To(Equal(3)) + + firstOffering := offerings[0] + Expect(firstOffering.Label).To(Equal("first-Offering 1")) + Expect(firstOffering.Version).To(Equal("1.0")) + Expect(firstOffering.Description).To(Equal("first Offering 1 description")) + Expect(firstOffering.Provider).To(Equal("Offering 1 provider")) + Expect(firstOffering.Guid).To(Equal("first-offering-1-guid")) + }) + }) + + Describe("GetServiceOfferingsForSpace", func() { + It("gets all service offerings in a given space", func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/services", + Response: firstOfferingsForSpaceResponse, + }), + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/services", + Response: multipleOfferingsResponse, + })) + + offerings, err := repo.GetServiceOfferingsForSpace("my-space-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(offerings)).To(Equal(3)) + + firstOffering := offerings[0] + Expect(firstOffering.Label).To(Equal("first-Offering 1")) + Expect(firstOffering.Version).To(Equal("1.0")) + Expect(firstOffering.Description).To(Equal("first Offering 1 description")) + Expect(firstOffering.Provider).To(Equal("Offering 1 provider")) + Expect(firstOffering.Guid).To(Equal("first-offering-1-guid")) + Expect(len(firstOffering.Plans)).To(Equal(0)) + + secondOffering := offerings[1] + Expect(secondOffering.Label).To(Equal("Offering 1")) + Expect(secondOffering.Version).To(Equal("1.0")) + Expect(secondOffering.Description).To(Equal("Offering 1 description")) + Expect(secondOffering.Provider).To(Equal("Offering 1 provider")) + Expect(secondOffering.Guid).To(Equal("offering-1-guid")) + Expect(len(secondOffering.Plans)).To(Equal(0)) + }) + }) + + Describe("find by service broker", func() { + BeforeEach(func() { + body1 := ` +{ + "total_results": 2, + "total_pages": 2, + "prev_url": null, + "next_url": "/v2/services?q=service_broker_guid%3Amy-service-broker-guid&page=2", + "resources": [ + { + "metadata": { + "guid": "my-service-guid" + }, + "entity": { + "label": "my-service", + "provider": "androsterone-ensphere", + "description": "Dummy addon that is cool", + "version": "damageableness-preheat", + "documentation_url": "YESWECAN.com" + } + } + ] +}` + body2 := ` +{ + "total_results": 1, + "total_pages": 1, + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "my-service-guid2" + }, + "entity": { + "label": "my-service2", + "provider": "androsterone-ensphere", + "description": "Dummy addon that is cooler", + "version": "seraphine-lowdah", + "documentation_url": "YESWECAN.com" + } + } + ] +}` + + setupTestServer( + testapi.NewCloudControllerTestRequest( + testnet.TestRequest{ + Method: "GET", + Path: "/v2/services?q=service_broker_guid%3Amy-service-broker-guid", + Response: testnet.TestResponse{Status: http.StatusOK, Body: body1}, + }), + testapi.NewCloudControllerTestRequest( + testnet.TestRequest{ + Method: "GET", + Path: "/v2/services?q=service_broker_guid%3Amy-service-broker-guid", + Response: testnet.TestResponse{Status: http.StatusOK, Body: body2}, + }), + ) + }) + + It("returns the service brokers services", func() { + services, err := repo.ListServicesFromBroker("my-service-broker-guid") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(services)).To(Equal(2)) + + Expect(services[0].Guid).To(Equal("my-service-guid")) + Expect(services[1].Guid).To(Equal("my-service-guid2")) + }) + }) + + Describe("creating a service instance", func() { + It("makes the right request", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_instances", + Matcher: testnet.RequestBodyMatcher(`{"name":"instance-name","service_plan_guid":"plan-guid","space_guid":"my-space-guid","async":true}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + + err := repo.CreateServiceInstance("instance-name", "plan-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when the name is taken but an identical service exists", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_instances", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"plan-guid","space_guid":"my-space-guid","async":true}`), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{"code":60002,"description":"The service instance name is taken: my-service"}`, + }}), + findServiceInstanceReq, + serviceOfferingReq) + }) + + It("returns a ModelAlreadyExistsError if the plan is the same", func() { + err := repo.CreateServiceInstance("my-service", "plan-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelAlreadyExistsError{})) + }) + }) + + Context("when the name is taken and no identical service instance exists", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/service_instances", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"different-plan-guid","space_guid":"my-space-guid","async":true}`), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{"code":60002,"description":"The service instance name is taken: my-service"}`, + }}), + findServiceInstanceReq, + serviceOfferingReq) + }) + + It("fails if the plan is different", func() { + err := repo.CreateServiceInstance("my-service", "different-plan-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(errors.NewHttpError(400, "", ""))) + }) + }) + }) + + Describe("UpdateServiceInstance", func() { + It("makes the right request", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_instances/instance-guid", + Matcher: testnet.RequestBodyMatcher(`{"service_plan_guid":"plan-guid"}`), + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.UpdateServiceInstance("instance-guid", "plan-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("When the instance or plan is not found", func() { + It("fails", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_instances/instance-guid", + Matcher: testnet.RequestBodyMatcher(`{"service_plan_guid":"plan-guid"}`), + Response: testnet.TestResponse{Status: http.StatusNotFound}, + })) + + err := repo.UpdateServiceInstance("instance-guid", "plan-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("finding service instances by name", func() { + It("returns the service instance", func() { + setupTestServer(findServiceInstanceReq, serviceOfferingReq) + + instance, err := repo.FindInstanceByName("my-service") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(instance.Name).To(Equal("my-service")) + Expect(instance.Guid).To(Equal("my-service-instance-guid")) + Expect(instance.ServiceOffering.Label).To(Equal("mysql")) + Expect(instance.ServiceOffering.DocumentationUrl).To(Equal("http://info.example.com")) + Expect(instance.ServiceOffering.Description).To(Equal("MySQL database")) + Expect(instance.ServicePlan.Name).To(Equal("plan-name")) + Expect(len(instance.ServiceBindings)).To(Equal(2)) + + binding := instance.ServiceBindings[0] + Expect(binding.Url).To(Equal("/v2/service_bindings/service-binding-1-guid")) + Expect(binding.Guid).To(Equal("service-binding-1-guid")) + Expect(binding.AppGuid).To(Equal("app-1-guid")) + }) + + It("returns user provided services", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { + "guid": "my-service-instance-guid" + }, + "entity": { + "name": "my-service", + "service_bindings": [ + { + "metadata": { + "guid": "service-binding-1-guid", + "url": "/v2/service_bindings/service-binding-1-guid" + }, + "entity": { + "app_guid": "app-1-guid" + } + }, + { + "metadata": { + "guid": "service-binding-2-guid", + "url": "/v2/service_bindings/service-binding-2-guid" + }, + "entity": { + "app_guid": "app-2-guid" + } + } + ], + "service_plan_guid": null + } + } + ] + }`}})) + + instance, err := repo.FindInstanceByName("my-service") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(instance.Name).To(Equal("my-service")) + Expect(instance.Guid).To(Equal("my-service-instance-guid")) + Expect(instance.ServiceOffering.Label).To(Equal("")) + Expect(instance.ServicePlan.Name).To(Equal("")) + Expect(len(instance.ServiceBindings)).To(Equal(2)) + + binding := instance.ServiceBindings[0] + Expect(binding.Url).To(Equal("/v2/service_bindings/service-binding-1-guid")) + Expect(binding.Guid).To(Equal("service-binding-1-guid")) + Expect(binding.AppGuid).To(Equal("app-1-guid")) + }) + + It("it returns a failure response when the instance doesn't exist", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`}, + })) + + _, err := repo.FindInstanceByName("my-service") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + }) + }) + + Describe("DeleteService", func() { + It("it deletes the service when no apps are bound", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/service_instances/my-service-instance-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + serviceInstance := models.ServiceInstance{} + serviceInstance.Guid = "my-service-instance-guid" + + err := repo.DeleteService(serviceInstance) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("doesn't delete the service when apps are bound", func() { + setupTestServer() + + serviceInstance := models.ServiceInstance{} + serviceInstance.Guid = "my-service-instance-guid" + serviceInstance.ServiceBindings = []models.ServiceBindingFields{ + { + Url: "/v2/service_bindings/service-binding-1-guid", + AppGuid: "app-1-guid", + }, + { + Url: "/v2/service_bindings/service-binding-2-guid", + AppGuid: "app-2-guid", + }, + } + + err := repo.DeleteService(serviceInstance) + Expect(err.Error()).To(Equal("Cannot delete service instance, apps are still bound to it")) + }) + }) + + Describe("RenameService", func() { + Context("when the service is not user provided", func() { + + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_instances/my-service-instance-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"new-name"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + }) + + It("renames the service", func() { + serviceInstance := models.ServiceInstance{} + serviceInstance.Guid = "my-service-instance-guid" + serviceInstance.ServicePlan = models.ServicePlanFields{ + Guid: "some-plan-guid", + } + + err := repo.RenameService(serviceInstance, "new-name") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the service is user provided", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/user_provided_service_instances/my-service-instance-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"new-name"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + }) + + It("renames the service", func() { + serviceInstance := models.ServiceInstance{} + serviceInstance.Guid = "my-service-instance-guid" + + err := repo.RenameService(serviceInstance, "new-name") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("FindServiceOfferingByLabelAndProvider", func() { + Context("when the service offering can be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "offering-1", + "provider": "provider-1", + "description": "offering 1 description", + "version" : "1.0", + "service_plans": [] + } + } + ] + }`}}) + }) + + It("finds service offerings by label and provider", func() { + offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1") + Expect(offering.Guid).To(Equal("offering-1-guid")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the service offering cannot be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": null, + "resources": [] + }`, + }, + }) + }) + It("returns a ModelNotFoundError", func() { + offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1") + + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + Expect(offering.Guid).To(Equal("")) + }) + }) + + It("handles api errors when finding service offerings", func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")), + Response: testnet.TestResponse{ + Status: 400, + Body: ` + { + "code": 10005, + "description": "The query parameter is invalid" + }`}}) + + _, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1") + Expect(err).To(HaveOccurred()) + Expect(err.(errors.HttpError).ErrorCode()).To(Equal("10005")) + }) + }) + + Describe("FindServiceOfferingsByLabel", func() { + Context("when the service offering can be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "offering-1", + "provider": "provider-1", + "description": "offering 1 description", + "version" : "1.0", + "service_plans": [], + "service_broker_guid": "broker-1-guid" + } + } + ] + }`}}) + }) + + It("finds service offerings by label", func() { + offerings, err := repo.FindServiceOfferingsByLabel("offering-1") + Expect(offerings[0].Guid).To(Equal("offering-1-guid")) + Expect(offerings[0].Label).To(Equal("offering-1")) + Expect(offerings[0].Provider).To(Equal("provider-1")) + Expect(offerings[0].Description).To(Equal("offering 1 description")) + Expect(offerings[0].Version).To(Equal("1.0")) + Expect(offerings[0].BrokerGuid).To(Equal("broker-1-guid")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the service offering cannot be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": null, + "resources": [] + }`, + }, + }) + }) + + It("returns a ModelNotFoundError", func() { + offerings, err := repo.FindServiceOfferingsByLabel("offering-1") + + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + Expect(offerings).To(Equal(models.ServiceOfferings{})) + }) + }) + + It("handles api errors when finding service offerings", func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: 400, + Body: ` + { + "code": 10005, + "description": "The query parameter is invalid" + }`}}) + + _, err := repo.FindServiceOfferingsByLabel("offering-1") + Expect(err).To(HaveOccurred()) + Expect(err.(errors.HttpError).ErrorCode()).To(Equal("10005")) + }) + }) + + Describe("GetServiceOfferingByGuid", func() { + Context("when the service offering can be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services/offering-1-guid"), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "offering-1", + "provider": "provider-1", + "description": "offering 1 description", + "version" : "1.0", + "service_plans": [], + "service_broker_guid": "broker-1-guid" + } + }`}}) + }) + + It("finds service offerings by guid", func() { + offering, err := repo.GetServiceOfferingByGuid("offering-1-guid") + Expect(offering.Guid).To(Equal("offering-1-guid")) + Expect(offering.Label).To(Equal("offering-1")) + Expect(offering.Provider).To(Equal("provider-1")) + Expect(offering.Description).To(Equal("offering 1 description")) + Expect(offering.Version).To(Equal("1.0")) + Expect(offering.BrokerGuid).To(Equal("broker-1-guid")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the service offering cannot be found", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services/offering-1-guid"), + Response: testnet.TestResponse{ + Status: 404, + Body: ` + { + "code": 120003, + "description": "The service could not be found: offering-1-guid", + "error_code": "CF-ServiceNotFound" + }`, + }, + }) + }) + + It("returns a ModelNotFoundError", func() { + offering, err := repo.GetServiceOfferingByGuid("offering-1-guid") + + Expect(err).To(BeAssignableToTypeOf(&errors.HttpNotFoundError{})) + Expect(offering.Guid).To(Equal("")) + }) + }) + }) + + Describe("PurgeServiceOffering", func() { + It("purges service offerings", func() { + setupTestServer(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/services/the-service-guid?purge=true", + Response: testnet.TestResponse{ + Status: 204, + }}) + + offering := maker.NewServiceOffering("the-offering") + offering.Guid = "the-service-guid" + + err := repo.PurgeServiceOffering(offering) + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("getting the count of service instances for a service plan", func() { + var planGuid = "abc123" + + It("returns the number of service instances", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGuid), + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "total_results": 9, + "total_pages": 9, + "prev_url": null, + "next_url": "/v2/service_plans/abc123/service_instances?page=2&results-per-page=1", + "resources": [ + { + "metadata": { + "guid": "def456", + "url": "/v2/service_instances/def456", + "created_at": "2013-06-06T02:42:55+00:00", + "updated_at": null + }, + "entity": { + "name": "pet-db", + "credentials": { "name": "the_name" }, + "service_plan_guid": "abc123", + "space_guid": "ghi789", + "dashboard_url": "https://example.com/dashboard", + "type": "managed_service_instance", + "space_url": "/v2/spaces/ghi789", + "service_plan_url": "/v2/service_plans/abc123", + "service_bindings_url": "/v2/service_instances/def456/service_bindings" + } + } + ] + } + `}, + })) + + count, err := repo.GetServiceInstanceCountForServicePlan(planGuid) + Expect(count).To(Equal(9)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the API error when one occurs", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGuid), + Response: testnet.TestResponse{Status: http.StatusInternalServerError}, + })) + + _, err := repo.GetServiceInstanceCountForServicePlan(planGuid) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("finding a service plan", func() { + var planDescription resources.ServicePlanDescription + + Context("when the service is a v1 service", func() { + BeforeEach(func() { + planDescription = resources.ServicePlanDescription{ + ServiceLabel: "v1-elephantsql", + ServicePlanName: "v1-panda", + ServiceProvider: "v1-elephantsql", + } + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v1-elephantsql;provider:v1-elephantsql")), + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "v1-elephantsql", + "provider": "v1-elephantsql", + "description": "Offering 1 description", + "version" : "1.0", + "service_plans": [ + { + "metadata": {"guid": "offering-1-plan-1-guid"}, + "entity": {"name": "not-the-plan-youre-looking-for"} + }, + { + "metadata": {"guid": "offering-1-plan-2-guid"}, + "entity": {"name": "v1-panda"} + } + ] + } + } + ] + }`}})) + }) + + It("returns the plan guid for a v1 plan", func() { + guid, err := repo.FindServicePlanByDescription(planDescription) + + Expect(guid).To(Equal("offering-1-plan-2-guid")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the service is a v2 service", func() { + BeforeEach(func() { + planDescription = resources.ServicePlanDescription{ + ServiceLabel: "v2-elephantsql", + ServicePlanName: "v2-panda", + } + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-elephantsql;provider:")), + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "v2-elephantsql", + "provider": null, + "description": "Offering 1 description", + "version" : "1.0", + "service_plans": [ + { + "metadata": {"guid": "offering-1-plan-1-guid"}, + "entity": {"name": "not-the-plan-youre-looking-for"} + }, + { + "metadata": {"guid": "offering-1-plan-2-guid"}, + "entity": {"name": "v2-panda"} + } + ] + } + } + ] + }`}})) + }) + + It("returns the plan guid for a v2 plan", func() { + guid, err := repo.FindServicePlanByDescription(planDescription) + Expect(err).NotTo(HaveOccurred()) + Expect(guid).To(Equal("offering-1-plan-2-guid")) + }) + }) + + Context("when no service matches the description", func() { + BeforeEach(func() { + planDescription = resources.ServicePlanDescription{ + ServiceLabel: "v2-service-label", + ServicePlanName: "v2-plan-name", + } + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")), + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`}, + })) + }) + + It("returns an error", func() { + _, err := repo.FindServicePlanByDescription(planDescription) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + Expect(err.Error()).To(ContainSubstring("Plan")) + Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name")) + }) + }) + + Context("when the described service has no matching plan", func() { + BeforeEach(func() { + planDescription = resources.ServicePlanDescription{ + ServiceLabel: "v2-service-label", + ServicePlanName: "v2-plan-name", + } + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")), + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "v2-elephantsql", + "provider": null, + "description": "Offering 1 description", + "version" : "1.0", + "service_plans": [ + { + "metadata": {"guid": "offering-1-plan-1-guid"}, + "entity": {"name": "not-the-plan-youre-looking-for"} + }, + { + "metadata": {"guid": "offering-1-plan-2-guid"}, + "entity": {"name": "also-not-the-plan-youre-looking-for"} + } + ] + } + } + ] + }`}})) + }) + + It("returns a ModelNotFoundError", func() { + _, err := repo.FindServicePlanByDescription(planDescription) + + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + Expect(err.Error()).To(ContainSubstring("Plan")) + Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name")) + }) + }) + + Context("when we get an HTTP error", func() { + BeforeEach(func() { + planDescription = resources.ServicePlanDescription{ + ServiceLabel: "v2-service-label", + ServicePlanName: "v2-plan-name", + } + + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")), + Response: testnet.TestResponse{ + Status: http.StatusInternalServerError, + }})) + }) + + It("returns an error", func() { + _, err := repo.FindServicePlanByDescription(planDescription) + + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(errors.NewHttpError(500, "", ""))) + }) + }) + }) + + Describe("migrating service plans", func() { + It("makes a request to CC to migrate the instances from v1 to v2", func() { + setupTestServer(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_plans/v1-guid/service_instances", + Matcher: testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`), + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"changed_count":3}`}, + }) + + changedCount, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid") + Expect(err).NotTo(HaveOccurred()) + Expect(changedCount).To(Equal(3)) + }) + + It("returns an error when migrating fails", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/service_plans/v1-guid/service_instances", + Matcher: testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`), + Response: testnet.TestResponse{Status: http.StatusInternalServerError}, + })) + + _, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid") + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("FindServiceOfferingsForSpaceByLabel", func() { + It("finds service offerings within a space by label", func() { + setupTestServer( + testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": "/v2/spaces/my-space-guid/services?q=label%3Aoffering-1&page=2", + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "offering-1", + "provider": "provider-1", + "description": "offering 1 description", + "version" : "1.0" + } + } + ] + }`}}, + testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: 200, + Body: ` + { + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "offering-2-guid" + }, + "entity": { + "label": "offering-2", + "provider": "provider-2", + "description": "offering 2 description", + "version" : "1.0" + } + } + ] + }`}}) + + offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1") + Expect(err).ToNot(HaveOccurred()) + Expect(offerings).To(HaveLen(2)) + Expect(offerings[0].Guid).To(Equal("offering-1-guid")) + }) + + It("returns an error if the offering cannot be found", func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": null, + "resources": [] + }`, + }, + }) + + offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1") + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + Expect(offerings).To(HaveLen(0)) + }) + + It("handles api errors when finding service offerings", func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{ + "code": 9001, + "description": "Something Happened" + }`, + }, + }) + + _, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1") + Expect(err).To(BeAssignableToTypeOf(errors.NewHttpError(400, "", ""))) + }) + + Describe("when api returns query by label is invalid", func() { + It("makes a backwards-compatible request", func() { + failedRequestByQueryLabel := testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:my-service-offering")), + Response: testnet.TestResponse{ + Status: http.StatusBadRequest, + Body: `{"code": 10005,"description": "The query parameter is invalid"}`, + }, + } + + firstPaginatedRequest := testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services"), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/spaces/my-space-guid/services?page=2", + "resources": [ + { + "metadata": { + "guid": "my-service-offering-guid" + }, + "entity": { + "label": "my-service-offering", + "provider": "some-other-provider", + "description": "a description that does not match your provider", + "version" : "1.0" + } + } + ] + }`, + }, + } + + secondPaginatedRequest := testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/spaces/my-space-guid/services"), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"next_url": null, + "resources": [ + { + "metadata": { + "guid": "my-service-offering-guid" + }, + "entity": { + "label": "my-service-offering", + "provider": "my-provider", + "description": "offering 1 description", + "version" : "1.0" + } + } + ]}`, + }, + } + + setupTestServer(failedRequestByQueryLabel, firstPaginatedRequest, secondPaginatedRequest) + + serviceOfferings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "my-service-offering") + Expect(err).NotTo(HaveOccurred()) + Expect(len(serviceOfferings)).To(Equal(2)) + }) + }) + }) +}) + +var firstOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/services?page=2", + "resources": [ + { + "metadata": { + "guid": "first-offering-1-guid" + }, + "entity": { + "label": "first-Offering 1", + "provider": "Offering 1 provider", + "description": "first Offering 1 description", + "version" : "1.0" + } + } + ]}`, +} + +var firstOfferingsForSpaceResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "next_url": "/v2/spaces/my-space-guid/services?inline-relations-depth=1&page=2", + "resources": [ + { + "metadata": { + "guid": "first-offering-1-guid" + }, + "entity": { + "label": "first-Offering 1", + "provider": "Offering 1 provider", + "description": "first Offering 1 description", + "version" : "1.0" + } + } + ]}`, +} + +var multipleOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "offering-1-guid" + }, + "entity": { + "label": "Offering 1", + "provider": "Offering 1 provider", + "description": "Offering 1 description", + "version" : "1.0" + } + }, + { + "metadata": { + "guid": "offering-2-guid" + }, + "entity": { + "label": "Offering 2", + "provider": "Offering 2 provider", + "description": "Offering 2 description", + "version" : "1.5" + } + } + ]}`, +} + +var serviceOfferingReq = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/services/the-service-guid", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + { + "metadata": { + "guid": "15790581-a293-489b-9efc-847ecf1b1339" + }, + "entity": { + "label": "mysql", + "provider": "mysql", + "documentation_url": "http://info.example.com", + "description": "MySQL database" + } + }`, + }}) + +var findServiceInstanceReq = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service", + Response: testnet.TestResponse{Status: http.StatusOK, Body: ` + {"resources": [ + { + "metadata": { + "guid": "my-service-instance-guid" + }, + "entity": { + "name": "my-service", + "service_bindings": [ + { + "metadata": { + "guid": "service-binding-1-guid", + "url": "/v2/service_bindings/service-binding-1-guid" + }, + "entity": { + "app_guid": "app-1-guid" + } + }, + { + "metadata": { + "guid": "service-binding-2-guid", + "url": "/v2/service_bindings/service-binding-2-guid" + }, + "entity": { + "app_guid": "app-2-guid" + } + } + ], + "service_plan": { + "metadata": { + "guid": "plan-guid" + }, + "entity": { + "name": "plan-name", + "service_guid": "the-service-guid" + } + } + } + } + ]}`}}) diff --git a/cf/api/space_quotas/fakes/fake_space_quota_repository.go b/cf/api/space_quotas/fakes/fake_space_quota_repository.go new file mode 100644 index 00000000000..97eb0e97b50 --- /dev/null +++ b/cf/api/space_quotas/fakes/fake_space_quota_repository.go @@ -0,0 +1,335 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/models" + "sync" +) + +type FakeSpaceQuotaRepository struct { + FindByNameStub func(name string) (quota models.SpaceQuota, apiErr error) + findByNameMutex sync.RWMutex + findByNameArgsForCall []struct { + arg1 string + } + findByNameReturns struct { + result1 models.SpaceQuota + result2 error + } + FindByOrgStub func(guid string) (quota []models.SpaceQuota, apiErr error) + findByOrgMutex sync.RWMutex + findByOrgArgsForCall []struct { + arg1 string + } + findByOrgReturns struct { + result1 []models.SpaceQuota + result2 error + } + FindByGuidStub func(guid string) (quota models.SpaceQuota, apiErr error) + findByGuidMutex sync.RWMutex + findByGuidArgsForCall []struct { + arg1 string + } + findByGuidReturns struct { + result1 models.SpaceQuota + result2 error + } + AssociateSpaceWithQuotaStub func(spaceGuid string, quotaGuid string) error + associateSpaceWithQuotaMutex sync.RWMutex + associateSpaceWithQuotaArgsForCall []struct { + arg1 string + arg2 string + } + associateSpaceWithQuotaReturns struct { + result1 error + } + UnassignQuotaFromSpaceStub func(spaceGuid string, quotaGuid string) error + unassignQuotaFromSpaceMutex sync.RWMutex + unassignQuotaFromSpaceArgsForCall []struct { + arg1 string + arg2 string + } + unassignQuotaFromSpaceReturns struct { + result1 error + } + CreateStub func(quota models.SpaceQuota) error + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 models.SpaceQuota + } + createReturns struct { + result1 error + } + UpdateStub func(quota models.SpaceQuota) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 models.SpaceQuota + } + updateReturns struct { + result1 error + } + DeleteStub func(quotaGuid string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 string + } + deleteReturns struct { + result1 error + } +} + +func (fake *FakeSpaceQuotaRepository) FindByName(arg1 string) (quota models.SpaceQuota, apiErr error) { + fake.findByNameMutex.Lock() + defer fake.findByNameMutex.Unlock() + fake.findByNameArgsForCall = append(fake.findByNameArgsForCall, struct { + arg1 string + }{arg1}) + if fake.FindByNameStub != nil { + return fake.FindByNameStub(arg1) + } else { + return fake.findByNameReturns.result1, fake.findByNameReturns.result2 + } +} + +func (fake *FakeSpaceQuotaRepository) FindByNameCallCount() int { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return len(fake.findByNameArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) FindByNameArgsForCall(i int) string { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return fake.findByNameArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) FindByNameReturns(result1 models.SpaceQuota, result2 error) { + fake.findByNameReturns = struct { + result1 models.SpaceQuota + result2 error + }{result1, result2} +} + +func (fake *FakeSpaceQuotaRepository) FindByOrg(arg1 string) (quota []models.SpaceQuota, apiErr error) { + fake.findByOrgMutex.Lock() + defer fake.findByOrgMutex.Unlock() + fake.findByOrgArgsForCall = append(fake.findByOrgArgsForCall, struct { + arg1 string + }{arg1}) + if fake.FindByOrgStub != nil { + return fake.FindByOrgStub(arg1) + } else { + return fake.findByOrgReturns.result1, fake.findByOrgReturns.result2 + } +} + +func (fake *FakeSpaceQuotaRepository) FindByOrgCallCount() int { + fake.findByOrgMutex.RLock() + defer fake.findByOrgMutex.RUnlock() + return len(fake.findByOrgArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) FindByOrgArgsForCall(i int) string { + fake.findByOrgMutex.RLock() + defer fake.findByOrgMutex.RUnlock() + return fake.findByOrgArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) FindByOrgReturns(result1 []models.SpaceQuota, result2 error) { + fake.findByOrgReturns = struct { + result1 []models.SpaceQuota + result2 error + }{result1, result2} +} + +func (fake *FakeSpaceQuotaRepository) FindByGuid(arg1 string) (quota models.SpaceQuota, apiErr error) { + fake.findByGuidMutex.Lock() + defer fake.findByGuidMutex.Unlock() + fake.findByGuidArgsForCall = append(fake.findByGuidArgsForCall, struct { + arg1 string + }{arg1}) + if fake.FindByGuidStub != nil { + return fake.FindByGuidStub(arg1) + } else { + return fake.findByGuidReturns.result1, fake.findByGuidReturns.result2 + } +} + +func (fake *FakeSpaceQuotaRepository) FindByGuidCallCount() int { + fake.findByGuidMutex.RLock() + defer fake.findByGuidMutex.RUnlock() + return len(fake.findByGuidArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) FindByGuidArgsForCall(i int) string { + fake.findByGuidMutex.RLock() + defer fake.findByGuidMutex.RUnlock() + return fake.findByGuidArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) FindByGuidReturns(result1 models.SpaceQuota, result2 error) { + fake.findByGuidReturns = struct { + result1 models.SpaceQuota + result2 error + }{result1, result2} +} + +func (fake *FakeSpaceQuotaRepository) AssociateSpaceWithQuota(arg1 string, arg2 string) error { + fake.associateSpaceWithQuotaMutex.Lock() + defer fake.associateSpaceWithQuotaMutex.Unlock() + fake.associateSpaceWithQuotaArgsForCall = append(fake.associateSpaceWithQuotaArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.AssociateSpaceWithQuotaStub != nil { + return fake.AssociateSpaceWithQuotaStub(arg1, arg2) + } else { + return fake.associateSpaceWithQuotaReturns.result1 + } +} + +func (fake *FakeSpaceQuotaRepository) AssociateSpaceWithQuotaCallCount() int { + fake.associateSpaceWithQuotaMutex.RLock() + defer fake.associateSpaceWithQuotaMutex.RUnlock() + return len(fake.associateSpaceWithQuotaArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) AssociateSpaceWithQuotaArgsForCall(i int) (string, string) { + fake.associateSpaceWithQuotaMutex.RLock() + defer fake.associateSpaceWithQuotaMutex.RUnlock() + return fake.associateSpaceWithQuotaArgsForCall[i].arg1, fake.associateSpaceWithQuotaArgsForCall[i].arg2 +} + +func (fake *FakeSpaceQuotaRepository) AssociateSpaceWithQuotaReturns(result1 error) { + fake.associateSpaceWithQuotaReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSpaceQuotaRepository) UnassignQuotaFromSpace(arg1 string, arg2 string) error { + fake.unassignQuotaFromSpaceMutex.Lock() + defer fake.unassignQuotaFromSpaceMutex.Unlock() + fake.unassignQuotaFromSpaceArgsForCall = append(fake.unassignQuotaFromSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.UnassignQuotaFromSpaceStub != nil { + return fake.UnassignQuotaFromSpaceStub(arg1, arg2) + } else { + return fake.unassignQuotaFromSpaceReturns.result1 + } +} + +func (fake *FakeSpaceQuotaRepository) UnassignQuotaFromSpaceCallCount() int { + fake.unassignQuotaFromSpaceMutex.RLock() + defer fake.unassignQuotaFromSpaceMutex.RUnlock() + return len(fake.unassignQuotaFromSpaceArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) UnassignQuotaFromSpaceArgsForCall(i int) (string, string) { + fake.unassignQuotaFromSpaceMutex.RLock() + defer fake.unassignQuotaFromSpaceMutex.RUnlock() + return fake.unassignQuotaFromSpaceArgsForCall[i].arg1, fake.unassignQuotaFromSpaceArgsForCall[i].arg2 +} + +func (fake *FakeSpaceQuotaRepository) UnassignQuotaFromSpaceReturns(result1 error) { + fake.unassignQuotaFromSpaceReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSpaceQuotaRepository) Create(arg1 models.SpaceQuota) error { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 models.SpaceQuota + }{arg1}) + if fake.CreateStub != nil { + return fake.CreateStub(arg1) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeSpaceQuotaRepository) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) CreateArgsForCall(i int) models.SpaceQuota { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) CreateReturns(result1 error) { + fake.createReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSpaceQuotaRepository) Update(arg1 models.SpaceQuota) error { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 models.SpaceQuota + }{arg1}) + if fake.UpdateStub != nil { + return fake.UpdateStub(arg1) + } else { + return fake.updateReturns.result1 + } +} + +func (fake *FakeSpaceQuotaRepository) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) UpdateArgsForCall(i int) models.SpaceQuota { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return fake.updateArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) UpdateReturns(result1 error) { + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSpaceQuotaRepository) Delete(arg1 string) error { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 string + }{arg1}) + if fake.DeleteStub != nil { + return fake.DeleteStub(arg1) + } else { + return fake.deleteReturns.result1 + } +} + +func (fake *FakeSpaceQuotaRepository) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeSpaceQuotaRepository) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].arg1 +} + +func (fake *FakeSpaceQuotaRepository) DeleteReturns(result1 error) { + fake.deleteReturns = struct { + result1 error + }{result1} +} + +var _ SpaceQuotaRepository = new(FakeSpaceQuotaRepository) diff --git a/cf/api/space_quotas/space_quotas.go b/cf/api/space_quotas/space_quotas.go new file mode 100644 index 00000000000..1916fdcef4c --- /dev/null +++ b/cf/api/space_quotas/space_quotas.go @@ -0,0 +1,118 @@ +package space_quotas + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type SpaceQuotaRepository interface { + FindByName(name string) (quota models.SpaceQuota, apiErr error) + FindByOrg(guid string) (quota []models.SpaceQuota, apiErr error) + FindByGuid(guid string) (quota models.SpaceQuota, apiErr error) + + AssociateSpaceWithQuota(spaceGuid string, quotaGuid string) error + UnassignQuotaFromSpace(spaceGuid string, quotaGuid string) error + + // CRUD ahoy + Create(quota models.SpaceQuota) error + Update(quota models.SpaceQuota) error + Delete(quotaGuid string) error +} + +type CloudControllerSpaceQuotaRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerSpaceQuotaRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerSpaceQuotaRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerSpaceQuotaRepository) findAllWithPath(path string) ([]models.SpaceQuota, error) { + var quotas []models.SpaceQuota + apiErr := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.SpaceQuotaResource{}, + func(resource interface{}) bool { + if qr, ok := resource.(resources.SpaceQuotaResource); ok { + quotas = append(quotas, qr.ToModel()) + } + return true + }) + return quotas, apiErr +} + +func (repo CloudControllerSpaceQuotaRepository) FindByName(name string) (quota models.SpaceQuota, apiErr error) { + quotas, apiErr := repo.FindByOrg(repo.config.OrganizationFields().Guid) + if apiErr != nil { + return + } + + for _, quota := range quotas { + if quota.Name == name { + return quota, nil + } + } + + apiErr = errors.NewModelNotFoundError("Space Quota", name) + return models.SpaceQuota{}, apiErr +} + +func (repo CloudControllerSpaceQuotaRepository) FindByOrg(guid string) ([]models.SpaceQuota, error) { + path := fmt.Sprintf("/v2/organizations/%s/space_quota_definitions", guid) + quotas, apiErr := repo.findAllWithPath(path) + if apiErr != nil { + return nil, apiErr + } + return quotas, nil +} + +func (repo CloudControllerSpaceQuotaRepository) FindByGuid(guid string) (quota models.SpaceQuota, apiErr error) { + quotas, apiErr := repo.FindByOrg(repo.config.OrganizationFields().Guid) + if apiErr != nil { + return + } + + for _, quota := range quotas { + if quota.Guid == guid { + return quota, nil + } + } + + apiErr = errors.NewModelNotFoundError("Space Quota", guid) + return models.SpaceQuota{}, apiErr +} + +func (repo CloudControllerSpaceQuotaRepository) Create(quota models.SpaceQuota) error { + path := "/v2/space_quota_definitions" + return repo.gateway.CreateResourceFromStruct(repo.config.ApiEndpoint(), path, quota) +} + +func (repo CloudControllerSpaceQuotaRepository) Update(quota models.SpaceQuota) error { + path := fmt.Sprintf("/v2/space_quota_definitions/%s", quota.Guid) + return repo.gateway.UpdateResourceFromStruct(repo.config.ApiEndpoint(), path, quota) +} + +func (repo CloudControllerSpaceQuotaRepository) AssociateSpaceWithQuota(spaceGuid string, quotaGuid string) error { + path := fmt.Sprintf("/v2/space_quota_definitions/%s/spaces/%s", quotaGuid, spaceGuid) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader("")) +} + +func (repo CloudControllerSpaceQuotaRepository) UnassignQuotaFromSpace(spaceGuid string, quotaGuid string) error { + path := fmt.Sprintf("/v2/space_quota_definitions/%s/spaces/%s", quotaGuid, spaceGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} + +func (repo CloudControllerSpaceQuotaRepository) Delete(quotaGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/space_quota_definitions/%s", quotaGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/space_quotas/space_quotas_suite_test.go b/cf/api/space_quotas/space_quotas_suite_test.go new file mode 100644 index 00000000000..d85a8be0827 --- /dev/null +++ b/cf/api/space_quotas/space_quotas_suite_test.go @@ -0,0 +1,19 @@ +package space_quotas_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpaceQuotas(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "SpaceQuotas Suite") +} diff --git a/cf/api/space_quotas/space_quotas_test.go b/cf/api/space_quotas/space_quotas_test.go new file mode 100644 index 00000000000..833a0245aa5 --- /dev/null +++ b/cf/api/space_quotas/space_quotas_test.go @@ -0,0 +1,321 @@ +package space_quotas_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/space_quotas" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CloudControllerQuotaRepository", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo CloudControllerSpaceQuotaRepository + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerSpaceQuotaRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("FindByName", func() { + BeforeEach(func() { + setupTestServer(firstSpaceQuotaRequest, secondSpaceQuotaRequest) + }) + + It("Finds Quota definitions by name", func() { + quota, err := repo.FindByName("my-remote-quota") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(quota).To(Equal(models.SpaceQuota{ + Guid: "my-quota-guid", + Name: "my-remote-quota", + MemoryLimit: 1024, + RoutesLimit: 123, + ServicesLimit: 321, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + })) + }) + + It("Returns an error if the quota cannot be found", func() { + _, err := repo.FindByName("totally-not-a-quota") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe("FindByOrg", func() { + BeforeEach(func() { + setupTestServer(firstSpaceQuotaRequest, secondSpaceQuotaRequest) + }) + + It("finds all quota definitions by org guid", func() { + quotas, err := repo.FindByOrg("my-org-guid") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(len(quotas)).To(Equal(3)) + Expect(quotas[0].Guid).To(Equal("my-quota-guid")) + Expect(quotas[0].Name).To(Equal("my-remote-quota")) + Expect(quotas[0].MemoryLimit).To(Equal(int64(1024))) + Expect(quotas[0].RoutesLimit).To(Equal(123)) + Expect(quotas[0].ServicesLimit).To(Equal(321)) + Expect(quotas[0].OrgGuid).To(Equal("my-org-guid")) + + Expect(quotas[1].Guid).To(Equal("my-quota-guid2")) + Expect(quotas[1].OrgGuid).To(Equal("my-org-guid")) + Expect(quotas[2].Guid).To(Equal("my-quota-guid3")) + Expect(quotas[2].OrgGuid).To(Equal("my-org-guid")) + }) + }) + + Describe("FindByGuid", func() { + BeforeEach(func() { + setupTestServer(firstSpaceQuotaRequest, secondSpaceQuotaRequest) + }) + + It("Finds Quota definitions by Guid", func() { + quota, err := repo.FindByGuid("my-quota-guid") + + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(quota).To(Equal(models.SpaceQuota{ + Guid: "my-quota-guid", + Name: "my-remote-quota", + MemoryLimit: 1024, + RoutesLimit: 123, + ServicesLimit: 321, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + })) + }) + It("Returns an error if the quota cannot be found", func() { + _, err := repo.FindByGuid("totally-not-a-quota-guid") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err.(*errors.ModelNotFoundError)).NotTo(BeNil()) + }) + }) + + Describe("AssociateSpaceWithQuota", func() { + It("sets the quota for a space", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/space_quota_definitions/my-quota-guid/spaces/my-space-guid", + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + setupTestServer(req) + + err := repo.AssociateSpaceWithQuota("my-space-guid", "my-quota-guid") + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("UnassignQuotaFromSpace", func() { + It("deletes the association between the quota and the space", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/space_quota_definitions/my-quota-guid/spaces/my-space-guid", + Response: testnet.TestResponse{Status: http.StatusNoContent}, + }) + setupTestServer(req) + + err := repo.UnassignQuotaFromSpace("my-space-guid", "my-quota-guid") + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("Create", func() { + It("creates a new quota with the given name", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/space_quota_definitions", + Matcher: testnet.RequestBodyMatcher(`{ + "name": "not-so-strict", + "non_basic_services_allowed": false, + "total_services": 1, + "total_routes": 12, + "memory_limit": 123, + "instance_memory_limit": 0, + "organization_guid": "my-org-guid" + }`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + setupTestServer(req) + + quota := models.SpaceQuota{ + Name: "not-so-strict", + ServicesLimit: 1, + RoutesLimit: 12, + MemoryLimit: 123, + OrgGuid: "my-org-guid", + } + err := repo.Create(quota) + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("Update", func() { + It("updates an existing quota", func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/space_quota_definitions/my-quota-guid", + Matcher: testnet.RequestBodyMatcher(`{ + "guid": "my-quota-guid", + "non_basic_services_allowed": false, + "name": "amazing-quota", + "total_services": 1, + "total_routes": 12, + "memory_limit": 123, + "instance_memory_limit": 1234, + "organization_guid": "myorgguid" + }`), + })) + + quota := models.SpaceQuota{ + Guid: "my-quota-guid", + Name: "amazing-quota", + NonBasicServicesAllowed: false, + ServicesLimit: 1, + RoutesLimit: 12, + MemoryLimit: 123, + InstanceMemoryLimit: 1234, + OrgGuid: "myorgguid", + } + + err := repo.Update(quota) + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) + + Describe("Delete", func() { + It("deletes the quota with the given name", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/space_quota_definitions/my-quota-guid", + Response: testnet.TestResponse{Status: http.StatusNoContent}, + }) + setupTestServer(req) + + err := repo.Delete("my-quota-guid") + Expect(err).NotTo(HaveOccurred()) + Expect(testHandler).To(HaveAllRequestsCalled()) + }) + }) +}) + +var firstQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/space_quota_definitions", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/quota_definitions?page=2", + "resources": [ + { + "metadata": { "guid": "my-quota-guid" }, + "entity": { + "name": "my-remote-quota", + "memory_limit": 1024, + "total_routes": 123, + "total_services": 321, + "non_basic_services_allowed": true, + } + } + ]}`, + }, +}) + +var secondQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/space_quota_definitions?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "resources": [ + { + "metadata": { "guid": "my-quota-guid2" }, + "entity": { "name": "my-remote-quota2", "memory_limit": 1024 } + }, + { + "metadata": { "guid": "my-quota-guid3" }, + "entity": { "name": "my-remote-quota3", "memory_limit": 1024 } + } + ]}`, + }, +}) + +var firstSpaceQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/space_quota_definitions", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/organizations/my-org-guid/space_quota_definitions?page=2", + "resources": [ + { + "metadata": { "guid": "my-quota-guid" }, + "entity": { + "name": "my-remote-quota", + "memory_limit": 1024, + "total_routes": 123, + "total_services": 321, + "non_basic_services_allowed": true, + "organization_guid": "my-org-guid" + } + } + ]}`, + }, +}) + +var secondSpaceQuotaRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/space_quota_definitions?page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "resources": [ + { + "metadata": { "guid": "my-quota-guid2" }, + "entity": { "name": "my-remote-quota2", "memory_limit": 1024, "organization_guid": "my-org-guid" } + }, + { + "metadata": { "guid": "my-quota-guid3" }, + "entity": { "name": "my-remote-quota3", "memory_limit": 1024, "organization_guid": "my-org-guid" } + } + ]}`, + }, +}) diff --git a/cf/api/spaces/spaces.go b/cf/api/spaces/spaces.go new file mode 100644 index 00000000000..4ce44862c2c --- /dev/null +++ b/cf/api/spaces/spaces.go @@ -0,0 +1,100 @@ +package spaces + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type SpaceRepository interface { + ListSpaces(func(models.Space) bool) error + FindByName(name string) (space models.Space, apiErr error) + FindByNameInOrg(name, orgGuid string) (space models.Space, apiErr error) + Create(name string, orgGuid string, spaceQuotaGuid string) (space models.Space, apiErr error) + Rename(spaceGuid, newName string) (apiErr error) + Delete(spaceGuid string) (apiErr error) +} + +type CloudControllerSpaceRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerSpaceRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerSpaceRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerSpaceRepository) ListSpaces(callback func(models.Space) bool) error { + return repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/organizations/%s/spaces?inline-relations-depth=1", repo.config.OrganizationFields().Guid), + resources.SpaceResource{}, + func(resource interface{}) bool { + return callback(resource.(resources.SpaceResource).ToModel()) + }) +} + +func (repo CloudControllerSpaceRepository) FindByName(name string) (space models.Space, apiErr error) { + return repo.FindByNameInOrg(name, repo.config.OrganizationFields().Guid) +} + +func (repo CloudControllerSpaceRepository) FindByNameInOrg(name, orgGuid string) (space models.Space, apiErr error) { + foundSpace := false + apiErr = repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + fmt.Sprintf("/v2/organizations/%s/spaces?q=%s&inline-relations-depth=1", orgGuid, url.QueryEscape("name:"+strings.ToLower(name))), + resources.SpaceResource{}, + func(resource interface{}) bool { + space = resource.(resources.SpaceResource).ToModel() + foundSpace = true + return false + }) + + if !foundSpace { + apiErr = errors.NewModelNotFoundError("Space", name) + } + + return +} + +func (repo CloudControllerSpaceRepository) Create(name, orgGuid, spaceQuotaGuid string) (space models.Space, apiErr error) { + path := "/v2/spaces?inline-relations-depth=1" + + bodyMap := map[string]string{"name": name, "organization_guid": orgGuid} + if spaceQuotaGuid != "" { + bodyMap["space_quota_definition_guid"] = spaceQuotaGuid + } + + body, apiErr := json.Marshal(bodyMap) + if apiErr != nil { + return + } + + resource := new(resources.SpaceResource) + apiErr = repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, strings.NewReader(string(body)), resource) + if apiErr != nil { + return + } + space = resource.ToModel() + return +} + +func (repo CloudControllerSpaceRepository) Rename(spaceGuid, newName string) (apiErr error) { + path := fmt.Sprintf("/v2/spaces/%s", spaceGuid) + body := fmt.Sprintf(`{"name":"%s"}`, newName) + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, strings.NewReader(body)) +} + +func (repo CloudControllerSpaceRepository) Delete(spaceGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/spaces/%s?recursive=true", spaceGuid) + return repo.gateway.DeleteResource(repo.config.ApiEndpoint(), path) +} diff --git a/cf/api/spaces/spaces_suite_test.go b/cf/api/spaces/spaces_suite_test.go new file mode 100644 index 00000000000..68f9e9f6bbc --- /dev/null +++ b/cf/api/spaces/spaces_suite_test.go @@ -0,0 +1,19 @@ +package spaces_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpaces(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Spaces Suite") +} diff --git a/cf/api/spaces/spaces_test.go b/cf/api/spaces/spaces_test.go new file mode 100644 index 00000000000..62ac498b05a --- /dev/null +++ b/cf/api/spaces/spaces_test.go @@ -0,0 +1,331 @@ +package spaces_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/spaces" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Space Repository", func() { + It("lists all the spaces", func() { + firstPageSpacesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/spaces?inline-relations-depth=1", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "next_url": "/v2/organizations/my-org-guid/spaces?inline-relations-depth=1&page=2", + "resources": [ + { + "metadata": { + "guid": "acceptance-space-guid" + }, + "entity": { + "name": "acceptance", + "security_groups": [ + { + "metadata": { + "guid": "4302b3b4-4afc-4f12-ae6d-ed1bb815551f" + }, + "entity": { + "name": "imma-security-group" + } + } + ] + } + } + ] + }`}}) + + secondPageSpacesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/spaces?inline-relations-depth=1&page=2", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { + "metadata": { + "guid": "staging-space-guid" + }, + "entity": { + "name": "staging", + "security_groups": [] + } + } + ] + }`}}) + + ts, handler, repo := createSpacesRepo(firstPageSpacesRequest, secondPageSpacesRequest) + defer ts.Close() + + spaces := []models.Space{} + apiErr := repo.ListSpaces(func(space models.Space) bool { + spaces = append(spaces, space) + return true + }) + + Expect(len(spaces)).To(Equal(2)) + Expect(spaces[0].Guid).To(Equal("acceptance-space-guid")) + Expect(spaces[0].SecurityGroups[0].Name).To(Equal("imma-security-group")) + Expect(spaces[1].Guid).To(Equal("staging-space-guid")) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(handler).To(HaveAllRequestsCalled()) + }) + + Describe("finding spaces by name", func() { + It("returns the space", func() { + testSpacesFindByNameWithOrg("my-org-guid", + func(repo SpaceRepository, spaceName string) (models.Space, error) { + return repo.FindByName(spaceName) + }, + ) + }) + + It("can find spaces in a particular org", func() { + testSpacesFindByNameWithOrg("another-org-guid", + func(repo SpaceRepository, spaceName string) (models.Space, error) { + return repo.FindByNameInOrg(spaceName, "another-org-guid") + }, + ) + }) + + It("returns a 'not found' response when the space doesn't exist", func() { + testSpacesDidNotFindByNameWithOrg("my-org-guid", + func(repo SpaceRepository, spaceName string) (models.Space, error) { + return repo.FindByName(spaceName) + }, + ) + }) + + It("returns a 'not found' response when the space doesn't exist in the given org", func() { + testSpacesDidNotFindByNameWithOrg("another-org-guid", + func(repo SpaceRepository, spaceName string) (models.Space, error) { + return repo.FindByNameInOrg(spaceName, "another-org-guid") + }, + ) + }) + }) + + It("creates spaces without a space-quota", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/spaces", + Matcher: testnet.RequestBodyMatcher(`{"name":"space-name","organization_guid":"my-org-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { + "guid": "space-guid" + }, + "entity": { + "name": "space-name" + } + }`}, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + space, apiErr := repo.Create("space-name", "my-org-guid", "") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(space.Guid).To(Equal("space-guid")) + Expect(space.SpaceQuotaGuid).To(Equal("")) + }) + + It("creates spaces with a space-quota", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/spaces", + Matcher: testnet.RequestBodyMatcher(`{"name":"space-name","organization_guid":"my-org-guid","space_quota_definition_guid":"space-quota-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` + { + "metadata": { + "guid": "space-guid" + }, + "entity": { + "name": "space-name", + "space_quota_definition_guid":"space-quota-guid" + } + }`}, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + space, apiErr := repo.Create("space-name", "my-org-guid", "space-quota-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(space.Guid).To(Equal("space-guid")) + Expect(space.SpaceQuotaGuid).To(Equal("space-quota-guid")) + }) + + It("renames spaces", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/spaces/my-space-guid", + Matcher: testnet.RequestBodyMatcher(`{"name":"new-space-name"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + apiErr := repo.Rename("my-space-guid", "new-space-name") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("deletes spaces", func() { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/spaces/my-space-guid?recursive=true", + Response: testnet.TestResponse{Status: http.StatusOK}, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + apiErr := repo.Delete("my-space-guid") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) +}) + +func testSpacesFindByNameWithOrg(orgGuid string, findByName func(SpaceRepository, string) (models.Space, error)) { + findSpaceByNameResponse := testnet.TestResponse{ + Status: http.StatusOK, + Body: ` +{ + "resources": [ + { + "metadata": { + "guid": "space1-guid" + }, + "entity": { + "name": "Space1", + "organization_guid": "org1-guid", + "organization": { + "metadata": { + "guid": "org1-guid" + }, + "entity": { + "name": "Org1" + } + }, + "apps": [ + { + "metadata": { + "guid": "app1-guid" + }, + "entity": { + "name": "app1" + } + }, + { + "metadata": { + "guid": "app2-guid" + }, + "entity": { + "name": "app2" + } + } + ], + "domains": [ + { + "metadata": { + "guid": "domain1-guid" + }, + "entity": { + "name": "domain1" + } + } + ], + "service_instances": [ + { + "metadata": { + "guid": "service1-guid" + }, + "entity": { + "name": "service1" + } + } + ] + } + } + ] +}`} + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/organizations/%s/spaces?q=name%%3Aspace1&inline-relations-depth=1", orgGuid), + Response: findSpaceByNameResponse, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + space, apiErr := findByName(repo, "Space1") + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(space.Name).To(Equal("Space1")) + Expect(space.Guid).To(Equal("space1-guid")) + + Expect(space.Organization.Guid).To(Equal("org1-guid")) + + Expect(len(space.Applications)).To(Equal(2)) + Expect(space.Applications[0].Guid).To(Equal("app1-guid")) + Expect(space.Applications[1].Guid).To(Equal("app2-guid")) + + Expect(len(space.Domains)).To(Equal(1)) + Expect(space.Domains[0].Guid).To(Equal("domain1-guid")) + + Expect(len(space.ServiceInstances)).To(Equal(1)) + Expect(space.ServiceInstances[0].Guid).To(Equal("service1-guid")) + + Expect(apiErr).NotTo(HaveOccurred()) + return +} + +func testSpacesDidNotFindByNameWithOrg(orgGuid string, findByName func(SpaceRepository, string) (models.Space, error)) { + request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/v2/organizations/%s/spaces?q=name%%3Aspace1&inline-relations-depth=1", orgGuid), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` { "resources": [ ] }`, + }, + }) + + ts, handler, repo := createSpacesRepo(request) + defer ts.Close() + + _, apiErr := findByName(repo, "Space1") + Expect(handler).To(HaveAllRequestsCalled()) + + Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) +} + +func createSpacesRepo(reqs ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo SpaceRepository) { + ts, handler = testnet.NewServer(reqs) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerSpaceRepository(configRepo, gateway) + return +} diff --git a/cf/api/stacks/fakes/fake_stack_repository.go b/cf/api/stacks/fakes/fake_stack_repository.go new file mode 100644 index 00000000000..381f8cab572 --- /dev/null +++ b/cf/api/stacks/fakes/fake_stack_repository.go @@ -0,0 +1,88 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/api/stacks" + "github.com/cloudfoundry/cli/cf/models" +) + +type FakeStackRepository struct { + FindByNameStub func(name string) (stack models.Stack, apiErr error) + findByNameMutex sync.RWMutex + findByNameArgsForCall []struct { + name string + } + findByNameReturns struct { + result1 models.Stack + result2 error + } + FindAllStub func() (stacks []models.Stack, apiErr error) + findAllMutex sync.RWMutex + findAllArgsForCall []struct{} + findAllReturns struct { + result1 []models.Stack + result2 error + } +} + +func (fake *FakeStackRepository) FindByName(name string) (stack models.Stack, apiErr error) { + fake.findByNameMutex.Lock() + fake.findByNameArgsForCall = append(fake.findByNameArgsForCall, struct { + name string + }{name}) + fake.findByNameMutex.Unlock() + if fake.FindByNameStub != nil { + return fake.FindByNameStub(name) + } else { + return fake.findByNameReturns.result1, fake.findByNameReturns.result2 + } +} + +func (fake *FakeStackRepository) FindByNameCallCount() int { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return len(fake.findByNameArgsForCall) +} + +func (fake *FakeStackRepository) FindByNameArgsForCall(i int) string { + fake.findByNameMutex.RLock() + defer fake.findByNameMutex.RUnlock() + return fake.findByNameArgsForCall[i].name +} + +func (fake *FakeStackRepository) FindByNameReturns(result1 models.Stack, result2 error) { + fake.FindByNameStub = nil + fake.findByNameReturns = struct { + result1 models.Stack + result2 error + }{result1, result2} +} + +func (fake *FakeStackRepository) FindAll() (stacks []models.Stack, apiErr error) { + fake.findAllMutex.Lock() + fake.findAllArgsForCall = append(fake.findAllArgsForCall, struct{}{}) + fake.findAllMutex.Unlock() + if fake.FindAllStub != nil { + return fake.FindAllStub() + } else { + return fake.findAllReturns.result1, fake.findAllReturns.result2 + } +} + +func (fake *FakeStackRepository) FindAllCallCount() int { + fake.findAllMutex.RLock() + defer fake.findAllMutex.RUnlock() + return len(fake.findAllArgsForCall) +} + +func (fake *FakeStackRepository) FindAllReturns(result1 []models.Stack, result2 error) { + fake.FindAllStub = nil + fake.findAllReturns = struct { + result1 []models.Stack + result2 error + }{result1, result2} +} + +var _ stacks.StackRepository = new(FakeStackRepository) diff --git a/cf/api/stacks/stacks.go b/cf/api/stacks/stacks.go new file mode 100644 index 00000000000..89746ec44eb --- /dev/null +++ b/cf/api/stacks/stacks.go @@ -0,0 +1,63 @@ +package stacks + +import ( + "fmt" + "net/url" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type StackRepository interface { + FindByName(name string) (stack models.Stack, apiErr error) + FindAll() (stacks []models.Stack, apiErr error) +} + +type CloudControllerStackRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCloudControllerStackRepository(config core_config.Reader, gateway net.Gateway) (repo CloudControllerStackRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CloudControllerStackRepository) FindByName(name string) (stack models.Stack, apiErr error) { + path := fmt.Sprintf("/v2/stacks?q=%s", url.QueryEscape("name:"+name)) + stacks, apiErr := repo.findAllWithPath(path) + if apiErr != nil { + return + } + + if len(stacks) == 0 { + apiErr = errors.NewModelNotFoundError("Stack", name) + return + } + + stack = stacks[0] + return +} + +func (repo CloudControllerStackRepository) FindAll() (stacks []models.Stack, apiErr error) { + return repo.findAllWithPath("/v2/stacks") +} + +func (repo CloudControllerStackRepository) findAllWithPath(path string) ([]models.Stack, error) { + var stacks []models.Stack + apiErr := repo.gateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.StackResource{}, + func(resource interface{}) bool { + if sr, ok := resource.(resources.StackResource); ok { + stacks = append(stacks, *sr.ToFields()) + } + return true + }) + return stacks, apiErr +} diff --git a/cf/api/stacks/stacks_suite_test.go b/cf/api/stacks/stacks_suite_test.go new file mode 100644 index 00000000000..a7a0b46a7a6 --- /dev/null +++ b/cf/api/stacks/stacks_suite_test.go @@ -0,0 +1,19 @@ +package stacks_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestStacks(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Stacks Suite") +} diff --git a/cf/api/stacks/stacks_test.go b/cf/api/stacks/stacks_test.go new file mode 100644 index 00000000000..86acdad5556 --- /dev/null +++ b/cf/api/stacks/stacks_test.go @@ -0,0 +1,168 @@ +package stacks_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api/stacks" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("StacksRepo", func() { + var ( + testServer *httptest.Server + testHandler *testnet.TestHandler + configRepo core_config.ReadWriter + repo StackRepository + ) + + setupTestServer := func(reqs ...testnet.TestRequest) { + testServer, testHandler = testnet.NewServer(reqs) + configRepo.SetApiEndpoint(testServer.URL) + } + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + configRepo.SetAccessToken("BEARER my_access_token") + + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCloudControllerStackRepository(configRepo, gateway) + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("FindByName", func() { + Context("when a stack exists", func() { + BeforeEach(func() { + setupTestServer(testnet.TestRequest{ + Method: "GET", + Path: "/v2/stacks?q=name%3Alinux", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { + "metadata": { "guid": "custom-linux-guid" }, + "entity": { "name": "custom-linux" } + } + ] + }`}}) + }) + + It("finds the stack", func() { + stack, err := repo.FindByName("linux") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(stack).To(Equal(models.Stack{ + Name: "custom-linux", + Guid: "custom-linux-guid", + })) + }) + }) + + Context("when a stack does not exist", func() { + BeforeEach(func() { + setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/stacks?q=name%3Alinux", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` { "resources": []}`, + }})) + }) + + It("returns an error", func() { + _, err := repo.FindByName("linux") + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + }) + }) + }) + + Describe("FindAll", func() { + BeforeEach(func() { + setupTestServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/stacks", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{ + "next_url": "/v2/stacks?page=2", + "resources": [ + { + "metadata": { + "guid": "stack-guid-1", + "url": "/v2/stacks/stack-guid-1", + "created_at": "2013-08-31 01:32:40 +0000", + "updated_at": "2013-08-31 01:32:40 +0000" + }, + "entity": { + "name": "lucid64", + "description": "Ubuntu 10.04" + } + } + ] + }`}}), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/stacks", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { + "metadata": { + "guid": "stack-guid-2", + "url": "/v2/stacks/stack-guid-2", + "created_at": "2013-08-31 01:32:40 +0000", + "updated_at": "2013-08-31 01:32:40 +0000" + }, + "entity": { + "name": "lucid64custom", + "description": "Fake Ubuntu 10.04" + } + } + ] + }`}})) + }) + + It("finds all the stacks", func() { + stacks, err := repo.FindAll() + + Expect(testHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + Expect(stacks).To(ConsistOf([]models.Stack{ + { + Guid: "stack-guid-1", + Name: "lucid64", + Description: "Ubuntu 10.04", + }, + { + Guid: "stack-guid-2", + Name: "lucid64custom", + Description: "Fake Ubuntu 10.04", + }, + })) + }) + }) +}) diff --git a/cf/api/strategy/domains.go b/cf/api/strategy/domains.go new file mode 100644 index 00000000000..0a1eddbbdbd --- /dev/null +++ b/cf/api/strategy/domains.go @@ -0,0 +1,89 @@ +package strategy + +type DomainsEndpointStrategy interface { + OrgDomainURL(orgGuid, name string) string + DomainURL(name string) string + OrgDomainsURL(orgGuid string) string + PrivateDomainsURL() string + SharedDomainsURL() string + DeleteDomainURL(guid string) string + DeleteSharedDomainURL(guid string) string + PrivateDomainsByOrgURL(guid string) string +} + +type domainsEndpointStrategy struct{} + +func (s domainsEndpointStrategy) DomainURL(name string) string { + return buildURL(v2("domains"), params{ + inlineRelationsDepth: 1, + q: map[string]string{"name": name}, + }) +} + +func (s domainsEndpointStrategy) OrgDomainsURL(orgGuid string) string { + return v2("organizations", orgGuid, "domains") +} + +func (s domainsEndpointStrategy) OrgDomainURL(orgGuid, name string) string { + return buildURL(s.OrgDomainsURL(orgGuid), params{ + inlineRelationsDepth: 1, + q: map[string]string{"name": name}, + }) +} + +func (s domainsEndpointStrategy) PrivateDomainsURL() string { + return v2("domains") +} + +func (s domainsEndpointStrategy) SharedDomainsURL() string { + return v2("domains") +} + +func (s domainsEndpointStrategy) PrivateDomainsByOrgURL(orgGuid string) string { + return v2("domains") +} + +func (s domainsEndpointStrategy) DeleteDomainURL(guid string) string { + return buildURL(v2("domains", guid), params{recursive: true}) +} + +func (s domainsEndpointStrategy) DeleteSharedDomainURL(guid string) string { + return buildURL(v2("domains", guid), params{recursive: true}) +} + +type separatedDomainsEndpointStrategy struct{} + +func (s separatedDomainsEndpointStrategy) DomainURL(name string) string { + return buildURL(v2("shared_domains"), params{ + q: map[string]string{"name": name}, + }) +} + +func (s separatedDomainsEndpointStrategy) OrgDomainsURL(orgGuid string) string { + return v2("organizations", orgGuid, "private_domains") +} + +func (s separatedDomainsEndpointStrategy) OrgDomainURL(orgGuid, name string) string { + return buildURL(s.OrgDomainsURL(orgGuid), params{ + q: map[string]string{"name": name}, + }) +} +func (s separatedDomainsEndpointStrategy) PrivateDomainsURL() string { + return v2("private_domains") +} + +func (s separatedDomainsEndpointStrategy) SharedDomainsURL() string { + return v2("shared_domains") +} + +func (s separatedDomainsEndpointStrategy) PrivateDomainsByOrgURL(orgGuid string) string { + return v2("organizations", orgGuid, "private_domains") +} + +func (s separatedDomainsEndpointStrategy) DeleteDomainURL(guid string) string { + return buildURL(v2("private_domains", guid), params{recursive: true}) +} + +func (s separatedDomainsEndpointStrategy) DeleteSharedDomainURL(guid string) string { + return buildURL(v2("shared_domains", guid), params{recursive: true}) +} diff --git a/cf/api/strategy/endpoint_strategy.go b/cf/api/strategy/endpoint_strategy.go new file mode 100644 index 00000000000..8aa54c24e21 --- /dev/null +++ b/cf/api/strategy/endpoint_strategy.go @@ -0,0 +1,25 @@ +package strategy + +type EndpointStrategy struct { + EventsEndpointStrategy + DomainsEndpointStrategy +} + +func NewEndpointStrategy(versionString string) EndpointStrategy { + version, err := ParseVersion(versionString) + if err != nil { + version = Version{0, 0, 0} + } + + strategy := EndpointStrategy{ + EventsEndpointStrategy: eventsEndpointStrategy{}, + DomainsEndpointStrategy: domainsEndpointStrategy{}, + } + + if version.GreaterThanOrEqualTo(Version{2, 1, 0}) { + strategy.EventsEndpointStrategy = globalEventsEndpointStrategy{} + strategy.DomainsEndpointStrategy = separatedDomainsEndpointStrategy{} + } + + return strategy +} diff --git a/cf/api/strategy/endpoint_strategy_test.go b/cf/api/strategy/endpoint_strategy_test.go new file mode 100644 index 00000000000..1446b777d4a --- /dev/null +++ b/cf/api/strategy/endpoint_strategy_test.go @@ -0,0 +1,75 @@ +package strategy_test + +import ( + "github.com/cloudfoundry/cli/cf/api/resources" + . "github.com/cloudfoundry/cli/cf/api/strategy" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("EndpointStrategy", func() { + var strategy EndpointStrategy + + Describe("events", func() { + Context("when the veresion string can't be parsed", func() { + BeforeEach(func() { + strategy = NewEndpointStrategy("") + }) + + It("uses the oldest possible strategy", func() { + Expect(strategy.EventsURL("the-guid", 20)).To(Equal("/v2/apps/the-guid/events?results-per-page=20")) + Expect(strategy.EventsResource()).To(BeAssignableToTypeOf(resources.EventResourceOldV2{})) + }) + }) + + Context("when targeting a pre-2.1.0 cloud controller", func() { + BeforeEach(func() { + strategy = NewEndpointStrategy("2.0.0") + }) + + It("returns an appropriate endpoint", func() { + Expect(strategy.EventsURL("the-guid", 20)).To(Equal("/v2/apps/the-guid/events?results-per-page=20")) + }) + + It("returns an old EventResource", func() { + Expect(strategy.EventsResource()).To(BeAssignableToTypeOf(resources.EventResourceOldV2{})) + }) + }) + + Context("when targeting a 2.1.0 cloud controller", func() { + BeforeEach(func() { + strategy = NewEndpointStrategy("2.1.0") + }) + + It("returns an appropriate endpoint", func() { + Expect(strategy.EventsURL("guids-r-us", 42)).To(Equal("/v2/events?order-direction=desc&q=actee%3Aguids-r-us&results-per-page=42")) + }) + + It("returns a new EventResource", func() { + Expect(strategy.EventsResource()).To(BeAssignableToTypeOf(resources.EventResourceNewV2{})) + }) + }) + }) + + Describe("domains", func() { + Context("when targeting a pre-2.1.0 cloud controller", func() { + BeforeEach(func() { + strategy = NewEndpointStrategy("2.0.0") + }) + + It("uses the general domains endpoint", func() { + Expect(strategy.PrivateDomainsURL()).To(Equal("/v2/domains")) + }) + }) + + Context("when targeting a v2.1.0 cloud controller", func() { + BeforeEach(func() { + strategy = NewEndpointStrategy("2.1.0") + }) + + It("uses the private domains endpoint", func() { + Expect(strategy.PrivateDomainsURL()).To(Equal("/v2/private_domains")) + }) + }) + }) +}) diff --git a/cf/api/strategy/events.go b/cf/api/strategy/events.go new file mode 100644 index 00000000000..c47937081d3 --- /dev/null +++ b/cf/api/strategy/events.go @@ -0,0 +1,34 @@ +package strategy + +import "github.com/cloudfoundry/cli/cf/api/resources" + +type EventsEndpointStrategy interface { + EventsURL(appGuid string, limit int64) string + EventsResource() resources.EventResource +} + +type eventsEndpointStrategy struct{} + +func (_ eventsEndpointStrategy) EventsURL(appGuid string, limit int64) string { + return buildURL(v2("apps", appGuid, "events"), params{ + resultsPerPage: limit, + }) +} + +func (_ eventsEndpointStrategy) EventsResource() resources.EventResource { + return resources.EventResourceOldV2{} +} + +type globalEventsEndpointStrategy struct{} + +func (strategy globalEventsEndpointStrategy) EventsURL(appGuid string, limit int64) string { + return buildURL(v2("events"), params{ + resultsPerPage: limit, + orderDirection: "desc", + q: map[string]string{"actee": appGuid}, + }) +} + +func (_ globalEventsEndpointStrategy) EventsResource() resources.EventResource { + return resources.EventResourceNewV2{} +} diff --git a/cf/api/strategy/strategy_suite_test.go b/cf/api/strategy/strategy_suite_test.go new file mode 100644 index 00000000000..938aee24730 --- /dev/null +++ b/cf/api/strategy/strategy_suite_test.go @@ -0,0 +1,19 @@ +package strategy_test + +import ( + "testing" + + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestStrategy(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "API Strategy Suite") +} diff --git a/cf/api/strategy/url_helpers.go b/cf/api/strategy/url_helpers.go new file mode 100644 index 00000000000..c5a4bc07052 --- /dev/null +++ b/cf/api/strategy/url_helpers.go @@ -0,0 +1,50 @@ +package strategy + +import ( + "net/url" + "path" + "strconv" +) + +type params struct { + resultsPerPage int64 + orderDirection string + q map[string]string + recursive bool + inlineRelationsDepth int64 +} + +func v2(segments ...string) string { + segments = append([]string{"/v2"}, segments...) + return path.Join(segments...) +} + +func buildURL(path string, params params) string { + query := url.Values{} + + if params.inlineRelationsDepth != 0 { + query.Set("inline-relations-depth", strconv.FormatInt(params.inlineRelationsDepth, 10)) + } + + if params.resultsPerPage != 0 { + query.Set("results-per-page", strconv.FormatInt(params.resultsPerPage, 10)) + } + + if params.orderDirection != "" { + query.Set("order-direction", params.orderDirection) + } + + if params.q != nil { + q := "" + for key, value := range params.q { + q += key + ":" + value + } + query.Set("q", q) + } + + if params.recursive { + query.Set("recursive", "true") + } + + return path + "?" + query.Encode() +} diff --git a/cf/api/strategy/version.go b/cf/api/strategy/version.go new file mode 100644 index 00000000000..a6b7523f9d8 --- /dev/null +++ b/cf/api/strategy/version.go @@ -0,0 +1,61 @@ +package strategy + +import ( + "strconv" + "strings" + + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type Version struct { + Major int64 + Minor int64 + Patch int64 +} + +func ParseVersion(input string) (Version, error) { + parts := strings.Split(input, ".") + if len(parts) != 3 { + return Version{}, errors.NewWithFmt(T("Could not parse version number: {{.Input}}", + map[string]interface{}{"Input": input})) + } + + major, err1 := strconv.ParseInt(parts[0], 10, 64) + minor, err2 := strconv.ParseInt(parts[1], 10, 64) + patch, err3 := strconv.ParseInt(parts[2], 10, 64) + if err1 != nil || err2 != nil || err3 != nil { + return Version{}, errors.NewWithFmt(T("Could not parse version number: {{.Input}}", + map[string]interface{}{"Input": input})) + } + + return Version{major, minor, patch}, nil +} + +func (version Version) LessThan(other Version) bool { + if version.Major < other.Major { + return true + } + + if version.Major > other.Major { + return false + } + + if version.Minor < other.Minor { + return true + } + + if version.Minor > other.Minor { + return false + } + + if version.Patch < other.Patch { + return true + } + + return false +} + +func (version Version) GreaterThanOrEqualTo(other Version) bool { + return !version.LessThan(other) +} diff --git a/cf/api/strategy/version_test.go b/cf/api/strategy/version_test.go new file mode 100644 index 00000000000..60128dffafb --- /dev/null +++ b/cf/api/strategy/version_test.go @@ -0,0 +1,53 @@ +package strategy_test + +import ( + . "github.com/cloudfoundry/cli/cf/api/strategy" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("api version", func() { + Describe("parsing", func() { + It("parses the major, minor and patch numbers", func() { + version, err := ParseVersion("1.2.3") + Expect(err).NotTo(HaveOccurred()) + Expect(version).To(Equal(Version{Major: 1, Minor: 2, Patch: 3})) + }) + + It("returns an error when there aren't three numbers", func() { + _, err := ParseVersion("1.2") + Expect(err).To(HaveOccurred()) + + _, err = ParseVersion("1.2.3.4") + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when there are non-digits in the version numbers", func() { + _, err := ParseVersion("1.2.x") + Expect(err).To(HaveOccurred()) + + _, err = ParseVersion("1.x.2") + Expect(err).To(HaveOccurred()) + + _, err = ParseVersion("x.2.3") + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("comparisons", func() { + It("compares the major version", func() { + Expect(Version{Major: 1, Minor: 2, Patch: 3}.LessThan(Version{Major: 2, Minor: 1, Patch: 1})).To(BeTrue()) + Expect(Version{Major: 2, Minor: 1, Patch: 1}.LessThan(Version{Major: 1, Minor: 3, Patch: 3})).To(BeFalse()) + }) + + It("compares the minor version", func() { + Expect(Version{Major: 1, Minor: 2, Patch: 3}.LessThan(Version{Major: 1, Minor: 3, Patch: 1})).To(BeTrue()) + Expect(Version{Major: 1, Minor: 3, Patch: 1}.LessThan(Version{Major: 1, Minor: 1, Patch: 100})).To(BeFalse()) + }) + + It("compares the patch version", func() { + Expect(Version{Major: 1, Minor: 2, Patch: 3}.LessThan(Version{Major: 1, Minor: 2, Patch: 42})).To(BeTrue()) + Expect(Version{Major: 1, Minor: 2, Patch: 42}.LessThan(Version{Major: 1, Minor: 2, Patch: 3})).To(BeFalse()) + }) + }) +}) diff --git a/cf/api/user_provided_service_instances.go b/cf/api/user_provided_service_instances.go new file mode 100644 index 00000000000..559362b9cb6 --- /dev/null +++ b/cf/api/user_provided_service_instances.go @@ -0,0 +1,71 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +type UserProvidedServiceInstanceRepository interface { + Create(name, drainUrl string, params map[string]interface{}) (apiErr error) + Update(serviceInstanceFields models.ServiceInstanceFields) (apiErr error) +} + +type CCUserProvidedServiceInstanceRepository struct { + config core_config.Reader + gateway net.Gateway +} + +func NewCCUserProvidedServiceInstanceRepository(config core_config.Reader, gateway net.Gateway) (repo CCUserProvidedServiceInstanceRepository) { + repo.config = config + repo.gateway = gateway + return +} + +func (repo CCUserProvidedServiceInstanceRepository) Create(name, drainUrl string, params map[string]interface{}) (apiErr error) { + path := "/v2/user_provided_service_instances" + + type RequestBody struct { + Name string `json:"name"` + Credentials map[string]interface{} `json:"credentials"` + SpaceGuid string `json:"space_guid"` + SysLogDrainUrl string `json:"syslog_drain_url"` + } + + jsonBytes, err := json.Marshal(RequestBody{ + Name: name, + Credentials: params, + SpaceGuid: repo.config.SpaceFields().Guid, + SysLogDrainUrl: drainUrl, + }) + + if err != nil { + apiErr = errors.NewWithError("Error parsing response", err) + return + } + + return repo.gateway.CreateResource(repo.config.ApiEndpoint(), path, bytes.NewReader(jsonBytes)) +} + +func (repo CCUserProvidedServiceInstanceRepository) Update(serviceInstanceFields models.ServiceInstanceFields) (apiErr error) { + path := fmt.Sprintf("/v2/user_provided_service_instances/%s", serviceInstanceFields.Guid) + + type RequestBody struct { + Credentials map[string]interface{} `json:"credentials,omitempty"` + SysLogDrainUrl string `json:"syslog_drain_url,omitempty"` + } + + reqBody := RequestBody{serviceInstanceFields.Params, serviceInstanceFields.SysLogDrainUrl} + jsonBytes, err := json.Marshal(reqBody) + if err != nil { + apiErr = errors.NewWithError("Error parsing response", err) + return + } + + return repo.gateway.UpdateResource(repo.config.ApiEndpoint(), path, bytes.NewReader(jsonBytes)) +} diff --git a/cf/api/user_provided_service_instances_test.go b/cf/api/user_provided_service_instances_test.go new file mode 100644 index 00000000000..fce99f3dd05 --- /dev/null +++ b/cf/api/user_provided_service_instances_test.go @@ -0,0 +1,96 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("UserProvidedServiceRepository", func() { + It("creates a user provided service with a name and credentials", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/user_provided_service_instances", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-custom-service","credentials":{"host":"example.com","password":"secret","user":"me"},"space_guid":"my-space-guid","syslog_drain_url":""}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createUserProvidedServiceInstanceRepo(req) + defer ts.Close() + + apiErr := repo.Create("my-custom-service", "", map[string]interface{}{ + "host": "example.com", + "user": "me", + "password": "secret", + }) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("creates user provided service instances with syslog drains", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/user_provided_service_instances", + Matcher: testnet.RequestBodyMatcher(`{"name":"my-custom-service","credentials":{"host":"example.com","password":"secret","user":"me"},"space_guid":"my-space-guid","syslog_drain_url":"syslog://example.com"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createUserProvidedServiceInstanceRepo(req) + defer ts.Close() + + apiErr := repo.Create("my-custom-service", "syslog://example.com", map[string]interface{}{ + "host": "example.com", + "user": "me", + "password": "secret", + }) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("can update a user provided service, given a service instance with a guid", func() { + req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/user_provided_service_instances/my-instance-guid", + Matcher: testnet.RequestBodyMatcher(`{"credentials":{"host":"example.com","password":"secret","user":"me"},"syslog_drain_url":"syslog://example.com"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + }) + + ts, handler, repo := createUserProvidedServiceInstanceRepo(req) + defer ts.Close() + + params := map[string]interface{}{ + "host": "example.com", + "user": "me", + "password": "secret", + } + serviceInstance := models.ServiceInstanceFields{} + serviceInstance.Guid = "my-instance-guid" + serviceInstance.Params = params + serviceInstance.SysLogDrainUrl = "syslog://example.com" + + apiErr := repo.Update(serviceInstance) + Expect(handler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) +}) + +func createUserProvidedServiceInstanceRepo(req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo UserProvidedServiceInstanceRepository) { + ts, handler = testnet.NewServer([]testnet.TestRequest{req}) + configRepo := testconfig.NewRepositoryWithDefaults() + configRepo.SetApiEndpoint(ts.URL) + gateway := net.NewCloudControllerGateway(configRepo, time.Now, &testterm.FakeUI{}) + repo = NewCCUserProvidedServiceInstanceRepository(configRepo, gateway) + return +} diff --git a/cf/api/users.go b/cf/api/users.go new file mode 100644 index 00000000000..1c3c25c522a --- /dev/null +++ b/cf/api/users.go @@ -0,0 +1,286 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + neturl "net/url" + "strings" + + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" +) + +var orgRoleToPathMap = map[string]string{ + models.ORG_USER: "users", + models.ORG_MANAGER: "managers", + models.BILLING_MANAGER: "billing_managers", + models.ORG_AUDITOR: "auditors", +} + +var spaceRoleToPathMap = map[string]string{ + models.SPACE_MANAGER: "managers", + models.SPACE_DEVELOPER: "developers", + models.SPACE_AUDITOR: "auditors", +} + +type UserRepository interface { + FindByUsername(username string) (user models.UserFields, apiErr error) + ListUsersInOrgForRole(orgGuid string, role string) ([]models.UserFields, error) + ListUsersInSpaceForRole(spaceGuid string, role string) ([]models.UserFields, error) + Create(username, password string) (apiErr error) + Delete(userGuid string) (apiErr error) + SetOrgRole(userGuid, orgGuid, role string) (apiErr error) + UnsetOrgRole(userGuid, orgGuid, role string) (apiErr error) + SetSpaceRole(userGuid, spaceGuid, orgGuid, role string) (apiErr error) + UnsetSpaceRole(userGuid, spaceGuid, role string) (apiErr error) +} + +type CloudControllerUserRepository struct { + config core_config.Reader + uaaGateway net.Gateway + ccGateway net.Gateway +} + +func NewCloudControllerUserRepository(config core_config.Reader, uaaGateway net.Gateway, ccGateway net.Gateway) (repo CloudControllerUserRepository) { + repo.config = config + repo.uaaGateway = uaaGateway + repo.ccGateway = ccGateway + return +} + +func (repo CloudControllerUserRepository) FindByUsername(username string) (models.UserFields, error) { + uaaEndpoint, apiErr := repo.getAuthEndpoint() + var user models.UserFields + if apiErr != nil { + return user, apiErr + } + + usernameFilter := neturl.QueryEscape(fmt.Sprintf(`userName Eq "%s"`, username)) + path := fmt.Sprintf("%s/Users?attributes=id,userName&filter=%s", uaaEndpoint, usernameFilter) + users, apiErr := repo.updateOrFindUsersWithUAAPath([]models.UserFields{}, path) + + if apiErr != nil { + errType, ok := apiErr.(errors.HttpError) + if ok { + if errType.StatusCode() == 403 { + return user, errors.NewAccessDeniedError() + } + } + return user, apiErr + } else if len(users) == 0 { + return user, errors.NewModelNotFoundError("User", username) + } + + return users[0], apiErr +} + +func (repo CloudControllerUserRepository) ListUsersInOrgForRole(orgGuid string, roleName string) (users []models.UserFields, apiErr error) { + return repo.listUsersWithPath(fmt.Sprintf("/v2/organizations/%s/%s", orgGuid, orgRoleToPathMap[roleName])) +} + +func (repo CloudControllerUserRepository) ListUsersInSpaceForRole(spaceGuid string, roleName string) (users []models.UserFields, apiErr error) { + return repo.listUsersWithPath(fmt.Sprintf("/v2/spaces/%s/%s", spaceGuid, spaceRoleToPathMap[roleName])) +} + +func (repo CloudControllerUserRepository) listUsersWithPath(path string) (users []models.UserFields, apiErr error) { + guidFilters := []string{} + + apiErr = repo.ccGateway.ListPaginatedResources( + repo.config.ApiEndpoint(), + path, + resources.UserResource{}, + func(resource interface{}) bool { + user := resource.(resources.UserResource).ToFields() + users = append(users, user) + guidFilters = append(guidFilters, fmt.Sprintf(`Id eq "%s"`, user.Guid)) + return true + }) + if apiErr != nil { + return + } + + if len(guidFilters) == 0 { + return + } + + uaaEndpoint, apiErr := repo.getAuthEndpoint() + if apiErr != nil { + return + } + + filter := strings.Join(guidFilters, " or ") + usersURL := fmt.Sprintf("%s/Users?attributes=id,userName&filter=%s", uaaEndpoint, neturl.QueryEscape(filter)) + users, apiErr = repo.updateOrFindUsersWithUAAPath(users, usersURL) + return +} + +func (repo CloudControllerUserRepository) updateOrFindUsersWithUAAPath(ccUsers []models.UserFields, path string) (updatedUsers []models.UserFields, apiErr error) { + uaaResponse := new(resources.UAAUserResources) + apiErr = repo.uaaGateway.GetResource(path, uaaResponse) + if apiErr != nil { + return + } + + for _, uaaResource := range uaaResponse.Resources { + var ccUserFields models.UserFields + + for _, u := range ccUsers { + if u.Guid == uaaResource.Id { + ccUserFields = u + break + } + } + + updatedUsers = append(updatedUsers, models.UserFields{ + Guid: uaaResource.Id, + Username: uaaResource.Username, + IsAdmin: ccUserFields.IsAdmin, + }) + } + return +} + +func (repo CloudControllerUserRepository) Create(username, password string) (err error) { + uaaEndpoint, err := repo.getAuthEndpoint() + if err != nil { + return + } + + path := "/Users" + body, err := json.Marshal(resources.NewUAAUserResource(username, password)) + + if err != nil { + return + } + + createUserResponse := &resources.UAAUserFields{} + err = repo.uaaGateway.CreateResource(uaaEndpoint, path, bytes.NewReader(body), createUserResponse) + switch httpErr := err.(type) { + case nil: + case errors.HttpError: + if httpErr.StatusCode() == http.StatusConflict { + err = errors.NewModelAlreadyExistsError("user", username) + return + } + return + default: + return + } + + path = "/v2/users" + body, err = json.Marshal(resources.Metadata{ + Guid: createUserResponse.Id, + }) + + if err != nil { + return + } + + return repo.ccGateway.CreateResource(repo.config.ApiEndpoint(), path, bytes.NewReader(body)) +} + +func (repo CloudControllerUserRepository) Delete(userGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/users/%s", userGuid) + + apiErr = repo.ccGateway.DeleteResource(repo.config.ApiEndpoint(), path) + + if httpErr, ok := apiErr.(errors.HttpError); ok && httpErr.ErrorCode() != errors.USER_NOT_FOUND { + return + } + uaaEndpoint, apiErr := repo.getAuthEndpoint() + if apiErr != nil { + return + } + + path = fmt.Sprintf("/Users/%s", userGuid) + return repo.uaaGateway.DeleteResource(uaaEndpoint, path) +} + +func (repo CloudControllerUserRepository) SetOrgRole(userGuid string, orgGuid string, role string) (apiErr error) { + apiErr = repo.setOrUnsetOrgRole("PUT", userGuid, orgGuid, role) + if apiErr != nil { + return + } + return repo.addOrgUserRole(userGuid, orgGuid) +} + +func (repo CloudControllerUserRepository) UnsetOrgRole(userGuid, orgGuid, role string) (apiErr error) { + return repo.setOrUnsetOrgRole("DELETE", userGuid, orgGuid, role) +} + +func (repo CloudControllerUserRepository) setOrUnsetOrgRole(verb, userGuid, orgGuid, role string) (apiErr error) { + rolePath, found := orgRoleToPathMap[role] + + if !found { + apiErr = errors.NewWithFmt(T("Invalid Role {{.Role}}", + map[string]interface{}{"Role": role})) + return + } + + path := fmt.Sprintf("%s/v2/organizations/%s/%s/%s", repo.config.ApiEndpoint(), orgGuid, rolePath, userGuid) + request, apiErr := repo.ccGateway.NewRequest(verb, path, repo.config.AccessToken(), nil) + if apiErr != nil { + return + } + + _, apiErr = repo.ccGateway.PerformRequest(request) + if apiErr != nil { + return + } + return +} + +func (repo CloudControllerUserRepository) SetSpaceRole(userGuid, spaceGuid, orgGuid, role string) (apiErr error) { + rolePath, apiErr := repo.checkSpaceRole(userGuid, spaceGuid, role) + if apiErr != nil { + return + } + + apiErr = repo.addOrgUserRole(userGuid, orgGuid) + if apiErr != nil { + return + } + + return repo.ccGateway.UpdateResource(repo.config.ApiEndpoint(), rolePath, nil) +} + +func (repo CloudControllerUserRepository) UnsetSpaceRole(userGuid, spaceGuid, role string) (apiErr error) { + rolePath, apiErr := repo.checkSpaceRole(userGuid, spaceGuid, role) + if apiErr != nil { + return + } + return repo.ccGateway.DeleteResource(repo.config.ApiEndpoint(), rolePath) +} + +func (repo CloudControllerUserRepository) checkSpaceRole(userGuid, spaceGuid, role string) (string, error) { + var apiErr error + + rolePath, found := spaceRoleToPathMap[role] + + if !found { + apiErr = errors.NewWithFmt(T("Invalid Role {{.Role}}", + map[string]interface{}{"Role": role})) + } + + apiPath := fmt.Sprintf("/v2/spaces/%s/%s/%s", spaceGuid, rolePath, userGuid) + return apiPath, apiErr +} + +func (repo CloudControllerUserRepository) addOrgUserRole(userGuid, orgGuid string) (apiErr error) { + path := fmt.Sprintf("/v2/organizations/%s/users/%s", orgGuid, userGuid) + return repo.ccGateway.UpdateResource(repo.config.ApiEndpoint(), path, nil) +} + +func (repo CloudControllerUserRepository) getAuthEndpoint() (string, error) { + uaaEndpoint := repo.config.UaaEndpoint() + if uaaEndpoint == "" { + return "", errors.New(T("UAA endpoint missing from config file")) + } + return uaaEndpoint, nil +} diff --git a/cf/api/users_test.go b/cf/api/users_test.go new file mode 100644 index 00000000000..b954081b27a --- /dev/null +++ b/cf/api/users_test.go @@ -0,0 +1,541 @@ +package api_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/api" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("User Repository", func() { + var ( + ccServer *httptest.Server + ccHandler *testnet.TestHandler + uaaServer *httptest.Server + uaaHandler *testnet.TestHandler + repo UserRepository + config core_config.ReadWriter + ) + + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + ccGateway := net.NewCloudControllerGateway((config), time.Now, &testterm.FakeUI{}) + uaaGateway := net.NewUAAGateway(config, &testterm.FakeUI{}) + repo = NewCloudControllerUserRepository(config, uaaGateway, ccGateway) + }) + + AfterEach(func() { + if uaaServer != nil { + uaaServer.Close() + } + if ccServer != nil { + ccServer.Close() + } + }) + + setupCCServer := func(requests ...testnet.TestRequest) { + ccServer, ccHandler = testnet.NewServer(requests) + config.SetApiEndpoint(ccServer.URL) + } + + setupUAAServer := func(requests ...testnet.TestRequest) { + uaaServer, uaaHandler = testnet.NewServer(requests) + config.SetUaaEndpoint(uaaServer.URL) + } + + Describe("listing the users with a given role", func() { + Context("when there are no users in the given org", func() { + It("lists the users in a org with a given role", func() { + ccReqs := []testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/managers", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": []}`, + }}), + } + + setupCCServer(ccReqs...) + + users, apiErr := repo.ListUsersInOrgForRole("my-org-guid", models.ORG_MANAGER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(len(users)).To(Equal(0)) + }) + }) + + Context("when there are users in the given org", func() { + It("lists the users in an organization with a given role", func() { + ccReqs, uaaReqs := createUsersByRoleEndpoints("/v2/organizations/my-org-guid/managers") + + setupCCServer(ccReqs...) + setupUAAServer(uaaReqs...) + + users, apiErr := repo.ListUsersInOrgForRole("my-org-guid", models.ORG_MANAGER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(len(users)).To(Equal(3)) + Expect(users[0].Guid).To(Equal("user-1-guid")) + Expect(users[0].Username).To(Equal("Super user 1")) + Expect(users[1].Guid).To(Equal("user-2-guid")) + Expect(users[1].Username).To(Equal("Super user 2")) + }) + }) + + Context("when there are no users in the space", func() { + It("lists the users in a space with a given role", func() { + ccReqs := []testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/spaces/my-space-guid/managers", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: `{"resources": []}`, + }}), + } + + setupCCServer(ccReqs...) + + users, apiErr := repo.ListUsersInSpaceForRole("my-space-guid", models.SPACE_MANAGER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(len(users)).To(Equal(0)) + }) + }) + + Context("when there are users in the space", func() { + It("lists the users in a space with a given role", func() { + ccReqs, uaaReqs := createUsersByRoleEndpoints("/v2/spaces/my-space-guid/managers") + + setupCCServer(ccReqs...) + setupUAAServer(uaaReqs...) + + users, apiErr := repo.ListUsersInSpaceForRole("my-space-guid", models.SPACE_MANAGER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + + Expect(len(users)).To(Equal(3)) + Expect(users[0].Guid).To(Equal("user-1-guid")) + Expect(users[0].Username).To(Equal("Super user 1")) + Expect(users[1].Guid).To(Equal("user-2-guid")) + Expect(users[1].Username).To(Equal("Super user 2")) + }) + }) + + It("does not make a request to the UAA when the cloud controller returns an error", func() { + ccReqs := []testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/v2/organizations/my-org-guid/managers", + Response: testnet.TestResponse{ + Status: http.StatusGatewayTimeout, + }, + }), + } + + setupCCServer(ccReqs...) + + _, apiErr := repo.ListUsersInOrgForRole("my-org-guid", models.ORG_MANAGER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + httpErr, ok := apiErr.(errors.HttpError) + Expect(ok).To(BeTrue()) + Expect(httpErr.StatusCode()).To(Equal(http.StatusGatewayTimeout)) + }) + + It("returns an error when the UAA endpoint cannot be determined", func() { + ccReqs, _ := createUsersByRoleEndpoints("/v2/organizations/my-org-guid/managers") + + setupCCServer(ccReqs...) + + config.SetAuthenticationEndpoint("") + + _, apiErr := repo.ListUsersInOrgForRole("my-org-guid", models.ORG_MANAGER) + Expect(apiErr).To(HaveOccurred()) + }) + }) + + Describe("FindByUsername", func() { + Context("when the user exists", func() { + It("finds the user", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/Users?attributes=id,userName&filter=userName+Eq+%22damien%2Buser1%40pivotallabs.com%22", + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [{ "id": "my-guid", "userName": "my-full-username" }] + }`, + }})) + + user, err := repo.FindByUsername("damien+user1@pivotallabs.com") + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(user).To(Equal(models.UserFields{ + Username: "my-full-username", + Guid: "my-guid", + })) + }) + }) + + Context("when the user does not exist", func() { + It("returns a ModelNotFoundError", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/Users?attributes=id,userName&filter=userName+Eq+%22my-user%22", + Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, + })) + + _, err := repo.FindByUsername("my-user") + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{})) + }) + }) + + Context("when the user does not have permission", func() { + It("returns a AccessDeniedError", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/Users?attributes=id,userName&filter=userName+Eq+%22my-user%22", + Response: testnet.TestResponse{Status: http.StatusForbidden, Body: `{"error":"access_denied","error_description":"Access is denied"}`}, + })) + + _, err := repo.FindByUsername("my-user") + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).To(BeAssignableToTypeOf(&errors.AccessDeniedError{})) + + }) + }) + + Context("when the uaa endpoint request returns a non-403 error", func() { + It("returns the error", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: "/Users?attributes=id,userName&filter=userName+Eq+%22my-user%22", + Response: testnet.TestResponse{Status: 500, Body: `server down!`}, + })) + + _, err := repo.FindByUsername("my-user") + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("creating users", func() { + It("it creates users using the UAA /Users endpoint", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/v2/users", + Matcher: testnet.RequestBodyMatcher(`{"guid":"my-user-guid"}`), + Response: testnet.TestResponse{Status: http.StatusCreated}, + })) + + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/Users", + Matcher: testnet.RequestBodyMatcher(`{ + "userName":"my-user", + "emails":[{"value":"my-user"}], + "password":"my-password", + "name":{ + "givenName":"my-user", + "familyName":"my-user"} + }`), + Response: testnet.TestResponse{ + Status: http.StatusCreated, + Body: `{"id":"my-user-guid"}`, + }, + })) + + apiErr := repo.Create("my-user", "my-password") + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("warns the user if the requested new user already exists", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/Users", + Response: testnet.TestResponse{ + Status: http.StatusConflict, + Body: ` + { + "message":"Username already in use: my-user", + "error":"scim_resource_already_exists" + }`, + }, + })) + + err := repo.Create("my-user", "my-password") + Expect(err).To(BeAssignableToTypeOf(&errors.ModelAlreadyExistsError{})) + }) + It("Returns any http error", func() { + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "POST", + Path: "/Users", + Response: testnet.TestResponse{ + Status: http.StatusForbidden, + Body: ` + { + "message":"Access Denied", + "error":"Forbidden" + }`, + }, + })) + + err := repo.Create("my-user", "my-password") + Expect(err.Error()).To(ContainSubstring("Forbidden")) + }) + }) + + Describe("deleting users", func() { + It("deletes the user", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/Users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + apiErr := repo.Delete("my-user-guid") + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + Context("when the user is not found on the cloud controller", func() { + It("when the user is not found on the cloud controller", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/v2/users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusNotFound, Body: ` + { + "code": 20003, + "description": "The user could not be found" + }`}, + })) + + setupUAAServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: "/Users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.Delete("my-user-guid") + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("assigning users organization roles", func() { + orgRoleURLS := map[string]string{ + "OrgManager": "/v2/organizations/my-org-guid/managers/my-user-guid", + "BillingManager": "/v2/organizations/my-org-guid/billing_managers/my-user-guid", + "OrgAuditor": "/v2/organizations/my-org-guid/auditors/my-user-guid", + } + + for role, roleURL := range orgRoleURLS { + It("gives users the "+role+" role", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: roleURL, + Response: testnet.TestResponse{Status: http.StatusOK}, + }), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/organizations/my-org-guid/users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.SetOrgRole("my-user-guid", "my-org-guid", role) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("unsets the org role from user", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "DELETE", + Path: roleURL, + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + apiErr := repo.UnsetOrgRole("my-user-guid", "my-org-guid", role) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(apiErr).NotTo(HaveOccurred()) + }) + } + + It("returns an error when given an invalid role to set", func() { + err := repo.SetOrgRole("user-guid", "org-guid", "foo") + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Invalid Role")) + }) + + It("returns an error when given an invalid role to unset", func() { + err := repo.UnsetOrgRole("user-guid", "org-guid", "foo") + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Invalid Role")) + }) + }) + + Describe("assigning space roles", func() { + spaceRoleURLS := map[string]string{ + "SpaceManager": "/v2/spaces/my-space-guid/managers/my-user-guid", + "SpaceDeveloper": "/v2/spaces/my-space-guid/developers/my-user-guid", + "SpaceAuditor": "/v2/spaces/my-space-guid/auditors/my-user-guid", + } + + for role, roleURL := range spaceRoleURLS { + It("gives the user the "+role+" role", func() { + setupCCServer( + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: "/v2/organizations/my-org-guid/users/my-user-guid", + Response: testnet.TestResponse{Status: http.StatusOK}, + }), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "PUT", + Path: roleURL, + Response: testnet.TestResponse{Status: http.StatusOK}, + })) + + err := repo.SetSpaceRole("my-user-guid", "my-space-guid", "my-org-guid", role) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + }) + } + + It("returns an error when given an invalid role to set", func() { + err := repo.SetSpaceRole("user-guid", "space-guid", "org-guid", "foo") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Invalid Role")) + }) + }) + + It("lists all users in the org", func() { + ccReqs, uaaReqs := createUsersByRoleEndpoints("/v2/organizations/my-org-guid/users") + + setupCCServer(ccReqs...) + setupUAAServer(uaaReqs...) + + users, err := repo.ListUsersInOrgForRole("my-org-guid", models.ORG_USER) + + Expect(ccHandler).To(HaveAllRequestsCalled()) + Expect(uaaHandler).To(HaveAllRequestsCalled()) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(users)).To(Equal(3)) + Expect(users[0].Guid).To(Equal("user-1-guid")) + Expect(users[0].Username).To(Equal("Super user 1")) + Expect(users[1].Guid).To(Equal("user-2-guid")) + Expect(users[1].Username).To(Equal("Super user 2")) + Expect(users[2].Guid).To(Equal("user-3-guid")) + Expect(users[2].Username).To(Equal("Super user 3")) + }) +}) + +func createUsersByRoleEndpoints(rolePath string) (ccReqs []testnet.TestRequest, uaaReqs []testnet.TestRequest) { + nextUrl := rolePath + "?page=2" + + ccReqs = []testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: rolePath, + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: fmt.Sprintf(` + { + "next_url": "%s", + "resources": [ + {"metadata": {"guid": "user-1-guid"}, "entity": {}} + ] + }`, nextUrl)}}), + + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: nextUrl, + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + {"metadata": {"guid": "user-2-guid"}, "entity": {}}, + {"metadata": {"guid": "user-3-guid"}, "entity": {}} + ] + }`}}), + } + + uaaReqs = []testnet.TestRequest{ + testapi.NewCloudControllerTestRequest(testnet.TestRequest{ + Method: "GET", + Path: fmt.Sprintf( + "/Users?attributes=id,userName&filter=%s", + url.QueryEscape(`Id eq "user-1-guid" or Id eq "user-2-guid" or Id eq "user-3-guid"`)), + Response: testnet.TestResponse{ + Status: http.StatusOK, + Body: ` + { + "resources": [ + { "id": "user-1-guid", "userName": "Super user 1" }, + { "id": "user-2-guid", "userName": "Super user 2" }, + { "id": "user-3-guid", "userName": "Super user 3" } + ] + }`}})} + + return +} diff --git a/cf/app/app.go b/cf/app/app.go new file mode 100644 index 00000000000..1f8a54aca60 --- /dev/null +++ b/cf/app/app.go @@ -0,0 +1,120 @@ +package app + +import ( + "fmt" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/command_runner" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/trace" + "github.com/codegangsta/cli" +) + +const UnknownCommand = "cf: '%s' is not a registered command. See 'cf help'" + +func NewApp(cmdRunner command_runner.Runner, metadatas ...command_metadata.CommandMetadata) (app *cli.App) { + helpCommand := cli.Command{ + Name: "help", + ShortName: "h", + Description: T("Show help"), + Usage: fmt.Sprintf(T("{{.Command}} help [COMMAND]", map[string]interface{}{"Command": cf.Name()})), + Action: func(c *cli.Context) { + args := c.Args() + if len(args) > 0 { + cli.ShowCommandHelp(c, args[0]) + } else { + showAppHelp(appHelpTemplate(), c.App) + } + }, + } + + cli.AppHelpTemplate = appHelpTemplate() + cli.HelpPrinter = ShowHelp + + trace.Logger.Printf("\n%s\n%s\n\n", terminal.HeaderColor(T("VERSION:")), cf.Version) + + app = cli.NewApp() + app.Usage = Usage() + app.Version = cf.Version + "-" + cf.BuiltOnDate + app.Action = helpCommand.Action + app.CommandNotFound = func(c *cli.Context, command string) { + panic(errors.Exception{ + Message: fmt.Sprintf(UnknownCommand, command), + DisplayCrashDialog: false, + }) + } + + compiledAtTime, err := time.Parse("2006-01-02T03:04:05+00:00", cf.BuiltOnDate) + + if err == nil { + app.Compiled = compiledAtTime + } else { + err = nil + app.Compiled = time.Now() + } + + app.Commands = []cli.Command{helpCommand} + + for _, metadata := range metadatas { + app.Commands = append(app.Commands, getCommand(metadata, cmdRunner)) + } + return +} + +func getCommand(metadata command_metadata.CommandMetadata, runner command_runner.Runner) cli.Command { + return cli.Command{ + Name: metadata.Name, + ShortName: metadata.ShortName, + Description: metadata.Description, + Usage: strings.Replace(metadata.Usage, "CF_NAME", cf.Name(), -1), + Action: func(context *cli.Context) { + err := runner.RunCmdByName(metadata.Name, context) + if err != nil { + panic(terminal.QuietPanic) + } + }, + Flags: metadata.Flags, + SkipFlagParsing: metadata.SkipFlagParsing, + } +} + +func Usage() string { + return T("A command line tool to interact with Cloud Foundry") +} + +func appHelpTemplate() string { + return `{{.Title "` + T("NAME:") + `"}} + {{.Name}} - {{.Usage}} + +{{.Title "` + T("USAGE:") + `"}} + ` + T("[environment variables]") + ` {{.Name}} ` + T("[global options] command [arguments...] [command options]") + ` + +{{.Title "` + T("VERSION:") + `"}} + {{.Version}} + +{{.Title "` + T("BUILD TIME:") + `"}} + {{.Compiled}} + {{range .Commands}} +{{.SubTitle .Name}}{{range .CommandSubGroups}} +{{range .}} {{.Name}} {{.Description}} +{{end}}{{end}}{{end}} +{{.Title "` + T("ENVIRONMENT VARIABLES") + `"}} + CF_COLOR=false ` + T("Do not colorize output") + ` + CF_HOME=path/to/dir/ ` + T("Override path to default config directory") + ` + CF_PLUGIN_HOME=path/to/dir/ ` + T("Override path to default plugin config directory") + ` + CF_STAGING_TIMEOUT=15 ` + T("Max wait time for buildpack staging, in minutes") + ` + CF_STARTUP_TIMEOUT=5 ` + T("Max wait time for app instance startup, in minutes") + ` + CF_TRACE=true ` + T("Print API request diagnostics to stdout") + ` + CF_TRACE=path/to/trace.log ` + T("Append API request diagnostics to a log file") + ` + HTTP_PROXY=proxy.example.com:8080 ` + T("Enable HTTP proxying for API requests") + ` + +{{.Title "` + T("GLOBAL OPTIONS") + `"}} + --version, -v ` + T("Print the version") + ` + --help, -h ` + T("Show help") + ` +` +} diff --git a/cf/app/app_suite_test.go b/cf/app/app_suite_test.go new file mode 100644 index 00000000000..9c7e528a531 --- /dev/null +++ b/cf/app/app_suite_test.go @@ -0,0 +1,24 @@ +package app_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "path/filepath" + + "testing" +) + +func TestApp(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "fixtures", "plugins"), "test_1") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "fixtures", "plugins"), "test_2") + RunSpecs(t, "App Suite") +} diff --git a/cf/app/app_test.go b/cf/app/app_test.go new file mode 100644 index 00000000000..228e86274d9 --- /dev/null +++ b/cf/app/app_test.go @@ -0,0 +1,138 @@ +package app_test + +import ( + "bytes" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_factory" + testPluginConfig "github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes" + "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/cli/cf/trace" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + testmanifest "github.com/cloudfoundry/cli/testhelpers/manifest" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + "github.com/codegangsta/cli" + + . "github.com/cloudfoundry/cli/cf/app" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var expectedCommandNames = []string{ + "api", "app", "apps", "auth", "bind-service", "buildpacks", "create-buildpack", + "create-domain", "create-org", "create-route", "create-service", "create-service-auth-token", + "create-service-broker", "create-space", "create-user", "create-user-provided-service", "curl", + "delete", "delete-buildpack", "delete-domain", "delete-shared-domain", "delete-org", "delete-route", + "delete-service", "delete-service-auth-token", "delete-service-broker", "delete-space", "delete-user", + "domains", "env", "events", "files", "login", "logout", "logs", "marketplace", "map-route", "org", + "org-users", "orgs", "passwd", "purge-service-offering", "push", "quotas", "rename", "rename-org", + "rename-service", "rename-service-broker", "rename-space", "restage", "restart", "routes", "scale", + "service", "service-auth-tokens", "service-brokers", "services", "set-env", "set-org-role", + "set-space-role", "create-shared-domain", "space", "space-users", "spaces", "stacks", "start", "stop", + "target", "unbind-service", "unmap-route", "unset-env", "unset-org-role", "unset-space-role", + "update-buildpack", "update-service-broker", "update-service-auth-token", "update-user-provided-service", + "quotas", "create-quota", "delete-quota", "quota", "set-quota", "install-plugin", "plugins", "uninstall-plugin", +} + +var _ = Describe("App", func() { + var ( + app *cli.App + cmdRunner *FakeRunner + ) + + JustBeforeEach(func() { + ui := &testterm.FakeUI{} + config := testconfig.NewRepository() + pluginConfig := &testPluginConfig.FakePluginConfiguration{} + manifestRepo := &testmanifest.FakeManifestRepository{} + + repoLocator := api.NewRepositoryLocator(config, map[string]net.Gateway{ + "auth": net.NewUAAGateway(config, ui), + "cloud-controller": net.NewCloudControllerGateway(config, time.Now, &testterm.FakeUI{}), + "uaa": net.NewUAAGateway(config, ui), + }) + + cmdFactory := command_factory.NewFactory(ui, config, manifestRepo, repoLocator, pluginConfig) + cmdRunner = &FakeRunner{cmdFactory: cmdFactory} + app = NewApp(cmdRunner, cmdFactory.CommandMetadatas()...) + }) + + Describe("trace file integration", func() { + var ( + output *bytes.Buffer + ) + + BeforeEach(func() { + output = bytes.NewBuffer(make([]byte, 1024)) + trace.SetStdout(output) + trace.EnableTrace() + }) + + It("prints its version number to the trace output when constructed", func() { + Expect(strings.Split(output.String(), "\n")).To(ContainSubstrings( + []string{"VERSION:"}, + []string{cf.Version}, + )) + }) + }) + + Context("when given a command name to run", func() { + It("runs the command with that name", func() { + for _, cmdName := range expectedCommandNames { + app.Run([]string{"", cmdName}) + Expect(cmdRunner.cmdName).To(Equal(cmdName)) + } + }) + }) + + Context("when running 'cf --help'", func() { + It("should output the help in our custom format", func() { + + output := io_helpers.CaptureOutput(func() { + app.Run([]string{"", "--help"}) + }) + + mergedOutput := strings.Join(output, "\n") + Expect(mergedOutput).To(ContainSubstring("CF_TRACE=true"), "CF_TRACE=true not in help") + Expect(mergedOutput).To(ContainSubstring("CF_PLUGIN_HOME=path/to/dir/")) + + for _, name := range expectedCommandNames { + Expect(mergedOutput).To(ContainSubstring(name), name+" not in help") + } + }) + }) + + Context("when the user provides an unknown command name", func() { + It("should complain loudly and then panic", func() { + Expect(func() { + io_helpers.CaptureOutput(func() { + app.Run([]string{"cf", "zoidberg"}) + }) + }).To(Panic()) + }) + }) + + It("includes the built on date in its version string", func() { + Expect(app.Version).To(Equal(cf.Version + "-" + cf.BuiltOnDate)) + }) +}) + +type FakeRunner struct { + cmdFactory command_factory.Factory + cmdName string +} + +func (runner *FakeRunner) RunCmdByName(cmdName string, c *cli.Context) (err error) { + _, err = runner.cmdFactory.GetByCmdName(cmdName) + if err != nil { + GinkgoT().Fatal("Error instantiating command with name", cmdName) + return + } + runner.cmdName = cmdName + return +} diff --git a/cf/app/flag_helper.go b/cf/app/flag_helper.go new file mode 100644 index 00000000000..6cffa6ec050 --- /dev/null +++ b/cf/app/flag_helper.go @@ -0,0 +1,61 @@ +package app + +import ( + "fmt" + "strings" + "unicode/utf8" + + "github.com/codegangsta/cli" +) + +func NewIntFlag(name, usage string) IntFlagWithNoDefault { + return IntFlagWithNoDefault{cli.IntFlag{Name: name, Usage: usage}} +} + +func NewIntFlagWithValue(name, usage string, value int) IntFlagWithNoDefault { + return IntFlagWithNoDefault{cli.IntFlag{Name: name, Value: value, Usage: usage}} +} + +func NewStringFlag(name, usage string) StringFlagWithNoDefault { + return StringFlagWithNoDefault{cli.StringFlag{Name: name, Usage: usage}} +} + +func NewStringSliceFlag(name, usage string) StringSliceFlagWithNoDefault { + return StringSliceFlagWithNoDefault{cli.StringSliceFlag{Name: name, Usage: usage, Value: &cli.StringSlice{}}} +} + +type IntFlagWithNoDefault struct { + cli.IntFlag +} + +type StringFlagWithNoDefault struct { + cli.StringFlag +} + +type StringSliceFlagWithNoDefault struct { + cli.StringSliceFlag +} + +func (f IntFlagWithNoDefault) String() string { + defaultVal := fmt.Sprintf("'%v'", f.Value) + return strings.Replace(f.IntFlag.String(), defaultVal, "", 1) +} + +func (f StringFlagWithNoDefault) String() string { + defaultVal := fmt.Sprintf("'%v'", f.Value) + return strings.Replace(f.StringFlag.String(), defaultVal, "", 1) +} + +func (f StringSliceFlagWithNoDefault) String() string { + return fmt.Sprintf("%s%s \t%s", prefixFor(f.Name), f.Name, f.Usage) +} + +func prefixFor(name string) (prefix string) { + if utf8.RuneCountInString(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} diff --git a/cf/app/help.go b/cf/app/help.go new file mode 100644 index 00000000000..34a81fdcaaa --- /dev/null +++ b/cf/app/help.go @@ -0,0 +1,379 @@ +package app + +import ( + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" + "text/template" + "unicode/utf8" + + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type groupedCommands struct { + Name string + CommandSubGroups [][]cmdPresenter +} + +func (c groupedCommands) SubTitle(name string) string { + return terminal.HeaderColor(name + ":") +} + +type cmdPresenter struct { + Name string + Description string +} + +func presentCmdName(cmd cli.Command) (name string) { + name = cmd.Name + if cmd.ShortName != "" { + name = name + ", " + cmd.ShortName + } + return +} + +type appPresenter struct { + cli.App + Commands []groupedCommands +} + +func (p appPresenter) Title(name string) string { + return terminal.HeaderColor(name) +} + +func newAppPresenter(app *cli.App) (presenter appPresenter) { + maxNameLen := 0 + for _, cmd := range app.Commands { + name := presentCmdName(cmd) + if utf8.RuneCountInString(name) > maxNameLen { + maxNameLen = len(name) + } + } + + presentCommand := func(commandName string) (presenter cmdPresenter) { + cmd := app.Command(commandName) + presenter.Name = presentCmdName(*cmd) + padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(presenter.Name)) + presenter.Name = presenter.Name + padding + presenter.Description = cmd.Description + return + } + + presentPluginCommands := func() []cmdPresenter { + pluginConfig := plugin_config.NewPluginConfig(func(err error) { + //fail silently when running help? + }) + + plugins := pluginConfig.Plugins() + var presenters []cmdPresenter + var pluginPresenter cmdPresenter + + for _, pluginMetadata := range plugins { + for _, cmd := range pluginMetadata.Commands { + pluginPresenter.Name = cmd.Name + padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(pluginPresenter.Name)) + pluginPresenter.Name = pluginPresenter.Name + padding + pluginPresenter.Description = cmd.HelpText + presenters = append(presenters, pluginPresenter) + } + } + + return presenters + } + presenter.Name = app.Name + presenter.Flags = app.Flags + presenter.Usage = app.Usage + presenter.Version = app.Version + presenter.Compiled = app.Compiled + presenter.Commands = []groupedCommands{ + { + Name: T("GETTING STARTED"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("login"), + presentCommand("logout"), + presentCommand("passwd"), + presentCommand("target"), + }, { + presentCommand("api"), + presentCommand("auth"), + }, + }, + }, { + Name: T("APPS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("apps"), + presentCommand("app"), + }, { + presentCommand("push"), + presentCommand("scale"), + presentCommand("delete"), + presentCommand("rename"), + }, { + presentCommand("start"), + presentCommand("stop"), + presentCommand("restart"), + presentCommand("restage"), + }, { + presentCommand("events"), + presentCommand("files"), + presentCommand("logs"), + }, { + presentCommand("env"), + presentCommand("set-env"), + presentCommand("unset-env"), + }, { + presentCommand("stacks"), + }, { + presentCommand("copy-source"), + }, + }, + }, { + Name: T("SERVICES"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("marketplace"), + presentCommand("services"), + presentCommand("service"), + }, { + presentCommand("create-service"), + presentCommand("update-service"), + presentCommand("delete-service"), + presentCommand("rename-service"), + }, { + presentCommand("bind-service"), + presentCommand("unbind-service"), + }, { + presentCommand("create-user-provided-service"), + presentCommand("update-user-provided-service"), + }, + }, + }, { + Name: T("ORGS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("orgs"), + presentCommand("org"), + }, { + presentCommand("create-org"), + presentCommand("delete-org"), + presentCommand("rename-org"), + }, + }, + }, { + Name: T("SPACES"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("spaces"), + presentCommand("space"), + }, { + presentCommand("create-space"), + presentCommand("delete-space"), + presentCommand("rename-space"), + }, + }, + }, { + Name: T("DOMAINS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("domains"), + presentCommand("create-domain"), + presentCommand("delete-domain"), + presentCommand("create-shared-domain"), + presentCommand("delete-shared-domain"), + }, + }, + }, { + Name: T("ROUTES"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("routes"), + presentCommand("create-route"), + presentCommand("check-route"), + presentCommand("map-route"), + presentCommand("unmap-route"), + presentCommand("delete-route"), + presentCommand("delete-orphaned-routes"), + }, + }, + }, { + Name: T("BUILDPACKS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("buildpacks"), + presentCommand("create-buildpack"), + presentCommand("update-buildpack"), + presentCommand("rename-buildpack"), + presentCommand("delete-buildpack"), + }, + }, + }, { + Name: T("USER ADMIN"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("create-user"), + presentCommand("delete-user"), + }, { + presentCommand("org-users"), + presentCommand("set-org-role"), + presentCommand("unset-org-role"), + }, { + presentCommand("space-users"), + presentCommand("set-space-role"), + presentCommand("unset-space-role"), + }, + }, + }, { + Name: T("ORG ADMIN"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("quotas"), + presentCommand("quota"), + presentCommand("set-quota"), + }, { + presentCommand("create-quota"), + presentCommand("delete-quota"), + presentCommand("update-quota"), + }, + }, + }, { + Name: T("SPACE ADMIN"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("space-quota"), + presentCommand("space-quotas"), + presentCommand("create-space-quota"), + presentCommand("update-space-quota"), + presentCommand("delete-space-quota"), + presentCommand("set-space-quota"), + presentCommand("unset-space-quota"), + }, + }, + }, { + Name: T("SERVICE ADMIN"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("service-auth-tokens"), + presentCommand("create-service-auth-token"), + presentCommand("update-service-auth-token"), + presentCommand("delete-service-auth-token"), + }, { + presentCommand("service-brokers"), + presentCommand("create-service-broker"), + presentCommand("update-service-broker"), + presentCommand("delete-service-broker"), + presentCommand("rename-service-broker"), + }, { + presentCommand("migrate-service-instances"), + presentCommand("purge-service-offering"), + }, { + presentCommand("service-access"), + presentCommand("enable-service-access"), + presentCommand("disable-service-access"), + }, + }, + }, { + Name: T("SECURITY GROUP"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("security-group"), + presentCommand("security-groups"), + presentCommand("create-security-group"), + presentCommand("update-security-group"), + presentCommand("delete-security-group"), + presentCommand("bind-security-group"), + presentCommand("unbind-security-group"), + }, { + presentCommand("bind-staging-security-group"), + presentCommand("staging-security-groups"), + presentCommand("unbind-staging-security-group"), + }, { + presentCommand("bind-running-security-group"), + presentCommand("running-security-groups"), + presentCommand("unbind-running-security-group"), + }, + }, + }, { + Name: T("ENVIRONMENT VARIABLE GROUPS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("running-environment-variable-group"), + presentCommand("staging-environment-variable-group"), + presentCommand("set-staging-environment-variable-group"), + presentCommand("set-running-environment-variable-group"), + }, + }, + }, + { + Name: T("FEATURE FLAGS"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("feature-flags"), + presentCommand("feature-flag"), + presentCommand("enable-feature-flag"), + presentCommand("disable-feature-flag"), + }, + }, + }, { + Name: T("ADVANCED"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("curl"), + presentCommand("config"), + presentCommand("oauth-token"), + }, + }, + }, { + Name: T("PLUGIN"), + CommandSubGroups: [][]cmdPresenter{ + { + presentCommand("plugins"), + presentCommand("install-plugin"), + presentCommand("uninstall-plugin"), + }, + }, + }, { + Name: T("PLUGIN COMMANDS"), + CommandSubGroups: [][]cmdPresenter{ + presentPluginCommands(), + }, + }, + } + + return +} + +func ShowHelp(helpTemplate string, thingToPrint interface{}) { + translatedTemplatedHelp := T(strings.Replace(helpTemplate, "{{", "[[", -1)) + translatedTemplatedHelp = strings.Replace(translatedTemplatedHelp, "[[", "{{", -1) + + switch thing := thingToPrint.(type) { + case *cli.App: + showAppHelp(translatedTemplatedHelp, thing) + case cli.Command: + showCommandHelp(translatedTemplatedHelp, thing) + default: + panic(fmt.Sprintf("Help printer has received something that is neither app nor command! The beast (%s) looks like this: %s", reflect.TypeOf(thing), thing)) + } +} + +var CodeGangstaHelpPrinter = cli.HelpPrinter + +func showCommandHelp(helpTemplate string, commandToPrint cli.Command) { + CodeGangstaHelpPrinter(helpTemplate, commandToPrint) +} + +func showAppHelp(helpTemplate string, appToPrint *cli.App) { + presenter := newAppPresenter(appToPrint) + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + t := template.Must(template.New("help").Parse(helpTemplate)) + t.Execute(w, presenter) + w.Flush() +} diff --git a/cf/app/help_test.go b/cf/app/help_test.go new file mode 100644 index 00000000000..0fd55f9228d --- /dev/null +++ b/cf/app/help_test.go @@ -0,0 +1,98 @@ +package app_test + +import ( + "path/filepath" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/app" + "github.com/cloudfoundry/cli/cf/command_factory" + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + testPluginConfig "github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + "github.com/codegangsta/cli" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Help", func() { + It("shows help for all commands", func() { + commandFactory := createCommandFactory() + + dummyTemplate := ` +{{range .Commands}}{{range .CommandSubGroups}}{{range .}} +{{.Name}} +{{end}}{{end}}{{end}} +` + output := io_helpers.CaptureOutput(func() { + app.ShowHelp(dummyTemplate, createApp(commandFactory)) + }) + + for _, metadata := range commandFactory.CommandMetadatas() { + Expect(commandInOutput(metadata.Name, output)).To(BeTrue(), metadata.Name+" not in help") + } + }) + + It("shows help for all installed plugin's commands", func() { + config_helpers.PluginRepoDir = func() string { + return filepath.Join("..", "..", "fixtures", "config", "help-plugin-test-config") + } + + commandFactory := createCommandFactory() + + dummyTemplate := ` +{{range .Commands}}{{range .CommandSubGroups}}{{range .}} +{{.Name}} +{{end}}{{end}}{{end}} +` + output := io_helpers.CaptureOutput(func() { + app.ShowHelp(dummyTemplate, createApp(commandFactory)) + }) + + Expect(commandInOutput("test1_cmd1", output)).To(BeTrue(), "plugin command: test1_cmd1 not in help") + Expect(commandInOutput("test1_cmd2", output)).To(BeTrue(), "plugin command: test1_cmd2 not in help") + Expect(commandInOutput("test2_cmd1", output)).To(BeTrue(), "plugin command: test2_cmd1 not in help") + Expect(commandInOutput("test2_cmd2", output)).To(BeTrue(), "plugin command: test2_cmd2 not in help") + + }) + +}) + +func createCommandFactory() command_factory.Factory { + fakeUI := &testterm.FakeUI{} + configRepo := testconfig.NewRepository() + pluginConfig := &testPluginConfig.FakePluginConfiguration{} + + manifestRepo := manifest.NewManifestDiskRepository() + apiRepoLocator := api.NewRepositoryLocator(configRepo, map[string]net.Gateway{ + "auth": net.NewUAAGateway(configRepo, fakeUI), + "cloud-controller": net.NewCloudControllerGateway(configRepo, time.Now, fakeUI), + "uaa": net.NewUAAGateway(configRepo, fakeUI), + }) + + return command_factory.NewFactory(fakeUI, configRepo, manifestRepo, apiRepoLocator, pluginConfig) +} + +func createApp(commandFactory command_factory.Factory) *cli.App { + new_app := cli.NewApp() + new_app.Commands = []cli.Command{} + for _, metadata := range commandFactory.CommandMetadatas() { + new_app.Commands = append(new_app.Commands, cli.Command{Name: metadata.Name}) + } + + return new_app +} + +func commandInOutput(cmdName string, output []string) bool { + for _, line := range output { + if strings.TrimSpace(line) == strings.TrimSpace(cmdName) { + return true + } + } + return false +} diff --git a/cf/app_constants.go b/cf/app_constants.go new file mode 100644 index 00000000000..3bf58175e81 --- /dev/null +++ b/cf/app_constants.go @@ -0,0 +1,15 @@ +package cf + +import ( + "os" + "path/filepath" +) + +const ( + Version = "BUILT_FROM_SOURCE" + BuiltOnDate = "BUILT_AT_UNKNOWN_TIME" +) + +func Name() string { + return filepath.Base(os.Args[0]) +} diff --git a/cf/app_files/app_files.go b/cf/app_files/app_files.go new file mode 100644 index 00000000000..01e29513478 --- /dev/null +++ b/cf/app_files/app_files.go @@ -0,0 +1,147 @@ +package app_files + +import ( + "crypto/sha1" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/models" + cffileutils "github.com/cloudfoundry/cli/fileutils" + "github.com/cloudfoundry/gofileutils/fileutils" +) + +type AppFiles interface { + AppFilesInDir(dir string) (appFiles []models.AppFileFields, err error) + CopyFiles(appFiles []models.AppFileFields, fromDir, toDir string) (err error) + CountFiles(directory string) int64 + WalkAppFiles(dir string, onEachFile func(string, string) error) (err error) +} + +type ApplicationFiles struct{} + +func (appfiles ApplicationFiles) AppFilesInDir(dir string) (appFiles []models.AppFileFields, err error) { + dir, err = filepath.Abs(dir) + if err != nil { + return + } + + err = appfiles.WalkAppFiles(dir, func(fileName string, fullPath string) (err error) { + fileInfo, err := os.Lstat(fullPath) + if err != nil { + return + } + + appFile := models.AppFileFields{ + Path: filepath.ToSlash(fileName), + Size: fileInfo.Size(), + } + + if fileInfo.IsDir() { + appFile.Sha1 = "0" + appFile.Size = 0 + } else { + hash := sha1.New() + err = fileutils.CopyPathToWriter(fullPath, hash) + if err != nil { + return + } + appFile.Sha1 = fmt.Sprintf("%x", hash.Sum(nil)) + } + + appFiles = append(appFiles, appFile) + return + }) + return +} + +func (appfiles ApplicationFiles) CopyFiles(appFiles []models.AppFileFields, fromDir, toDir string) (err error) { + if err != nil { + return + } + + for _, file := range appFiles { + fromPath := filepath.Join(fromDir, file.Path) + toPath := filepath.Join(toDir, file.Path) + err = copyPathToPath(fromPath, toPath) + if err != nil { + return + } + } + return +} + +func (appfiles ApplicationFiles) CountFiles(directory string) int64 { + var count int64 + appfiles.WalkAppFiles(directory, func(_, _ string) error { + count++ + return nil + }) + return count +} + +func (appfiles ApplicationFiles) WalkAppFiles(dir string, onEachFile func(string, string) error) (err error) { + cfIgnore := loadIgnoreFile(dir) + walkFunc := func(fullPath string, f os.FileInfo, inErr error) (err error) { + err = inErr + if err != nil { + return + } + + if fullPath == dir { + return + } + + if !cffileutils.IsRegular(f) && !f.IsDir() { + return + } + + fileRelativePath, _ := filepath.Rel(dir, fullPath) + fileRelativeUnixPath := filepath.ToSlash(fileRelativePath) + + if !cfIgnore.FileShouldBeIgnored(fileRelativeUnixPath) { + err = onEachFile(fileRelativePath, fullPath) + } + + return + } + + err = filepath.Walk(dir, walkFunc) + return +} + +func copyPathToPath(fromPath, toPath string) (err error) { + srcFileInfo, err := os.Stat(fromPath) + if err != nil { + return + } + + if srcFileInfo.IsDir() { + err = os.MkdirAll(toPath, srcFileInfo.Mode()) + if err != nil { + return + } + } else { + var dst *os.File + dst, err = fileutils.Create(toPath) + if err != nil { + return + } + defer dst.Close() + + dst.Chmod(srcFileInfo.Mode()) + + err = fileutils.CopyPathToWriter(fromPath, dst) + } + return err +} + +func loadIgnoreFile(dir string) CfIgnore { + fileContents, err := ioutil.ReadFile(filepath.Join(dir, ".cfignore")) + if err == nil { + return NewCfIgnore(string(fileContents)) + } else { + return NewCfIgnore("") + } +} diff --git a/cf/app_files/app_files_suite_test.go b/cf/app_files/app_files_suite_test.go new file mode 100644 index 00000000000..e4b05fda6c0 --- /dev/null +++ b/cf/app_files/app_files_suite_test.go @@ -0,0 +1,19 @@ +package app_files_test + +import ( + "testing" + + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestAppFiles(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "App Files Suite") +} diff --git a/cf/app_files/app_files_test.go b/cf/app_files/app_files_test.go new file mode 100644 index 00000000000..12076c68851 --- /dev/null +++ b/cf/app_files/app_files_test.go @@ -0,0 +1,107 @@ +package app_files_test + +import ( + . "github.com/cloudfoundry/cli/cf/app_files" + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/models" + cffileutils "github.com/cloudfoundry/cli/fileutils" + "github.com/cloudfoundry/gofileutils/fileutils" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppFiles", func() { + var appFiles = ApplicationFiles{} + fixturePath := filepath.Join("..", "..", "fixtures", "applications") + + Describe("AppFilesInDir", func() { + It("all files have '/' path separators", func() { + files, err := appFiles.AppFilesInDir(fixturePath) + Expect(err).ShouldNot(HaveOccurred()) + + for _, afile := range files { + Expect(afile.Path).Should(Equal(filepath.ToSlash(afile.Path))) + } + }) + + It("excludes files based on the .cfignore file", func() { + appPath := filepath.Join(fixturePath, "app-with-cfignore") + files, err := appFiles.AppFilesInDir(appPath) + Expect(err).ShouldNot(HaveOccurred()) + + paths := []string{} + for _, file := range files { + paths = append(paths, file.Path) + } + + Expect(paths).To(Equal([]string{ + "dir1", + "dir1/child-dir", + "dir1/child-dir/file3.txt", + "dir1/file1.txt", + "dir2", + + // TODO: this should be excluded. + // .cfignore doesn't handle ** patterns right now + "dir2/child-dir2", + })) + }) + + // NB: on windows, you can never rely on the size of a directory being zero + // see: http://msdn.microsoft.com/en-us/library/windows/desktop/aa364946(v=vs.85).aspx + // and: https://www.pivotaltracker.com/story/show/70470232 + It("always sets the size of directories to zero bytes", func() { + fileutils.TempDir("something", func(tempdir string, err error) { + Expect(err).ToNot(HaveOccurred()) + + err = os.Mkdir(filepath.Join(tempdir, "nothing"), 0600) + Expect(err).ToNot(HaveOccurred()) + + files, err := appFiles.AppFilesInDir(tempdir) + Expect(err).ToNot(HaveOccurred()) + + sizes := []int64{} + for _, file := range files { + sizes = append(sizes, file.Size) + } + + Expect(sizes).To(Equal([]int64{0})) + }) + }) + }) + + Describe("CopyFiles", func() { + It("copies only the files specified", func() { + copyDir := filepath.Join(fixturePath, "app-copy-test") + + filesToCopy := []models.AppFileFields{ + {Path: filepath.Join("dir1")}, + {Path: filepath.Join("dir1", "child-dir", "file2.txt")}, + } + + files := []string{} + + cffileutils.TempDir("copyToDir", func(tmpDir string, err error) { + copyErr := appFiles.CopyFiles(filesToCopy, copyDir, tmpDir) + Expect(copyErr).ToNot(HaveOccurred()) + + filepath.Walk(tmpDir, func(path string, fileInfo os.FileInfo, err error) error { + Expect(err).ToNot(HaveOccurred()) + + if !fileInfo.IsDir() { + files = append(files, fileInfo.Name()) + } + return nil + }) + }) + + // file2.txt is in lowest subtree, thus is walked first. + Expect(files).To(Equal([]string{ + "file2.txt", + })) + }) + }) +}) diff --git a/cf/app_files/cf_ignore.go b/cf/app_files/cf_ignore.go new file mode 100644 index 00000000000..e328c35e1fe --- /dev/null +++ b/cf/app_files/cf_ignore.go @@ -0,0 +1,85 @@ +package app_files + +import ( + "path" + "strings" + + "github.com/cloudfoundry/cli/glob" +) + +type CfIgnore interface { + FileShouldBeIgnored(path string) bool +} + +func NewCfIgnore(text string) CfIgnore { + patterns := []ignorePattern{} + lines := strings.Split(text, "\n") + lines = append(defaultIgnoreLines, lines...) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + ignore := true + if strings.HasPrefix(line, "!") { + line = line[1:] + ignore = false + } + + for _, p := range globsForPattern(path.Clean(line)) { + patterns = append(patterns, ignorePattern{ignore, p}) + } + } + + return cfIgnore(patterns) +} + +func (ignore cfIgnore) FileShouldBeIgnored(path string) bool { + result := false + + for _, pattern := range ignore { + if strings.HasPrefix(pattern.glob.String(), "/") && !strings.HasPrefix(path, "/") { + path = "/" + path + } + + if pattern.glob.Match(path) { + result = pattern.exclude + } + } + + return result +} + +func globsForPattern(pattern string) (globs []glob.Glob) { + globs = append(globs, glob.MustCompileGlob(pattern)) + globs = append(globs, glob.MustCompileGlob(path.Join(pattern, "*"))) + globs = append(globs, glob.MustCompileGlob(path.Join(pattern, "**", "*"))) + + if !strings.HasPrefix(pattern, "/") { + globs = append(globs, glob.MustCompileGlob(path.Join("**", pattern))) + globs = append(globs, glob.MustCompileGlob(path.Join("**", pattern, "*"))) + globs = append(globs, glob.MustCompileGlob(path.Join("**", pattern, "**", "*"))) + } + + return +} + +type ignorePattern struct { + exclude bool + glob glob.Glob +} + +type cfIgnore []ignorePattern + +var defaultIgnoreLines = []string{ + ".cfignore", + "/manifest.yml", + ".gitignore", + ".git", + ".hg", + ".svn", + "_darcs", + ".DS_Store", +} diff --git a/cf/app_files/cf_ignore_test.go b/cf/app_files/cf_ignore_test.go new file mode 100644 index 00000000000..c3f8c9b6439 --- /dev/null +++ b/cf/app_files/cf_ignore_test.go @@ -0,0 +1,79 @@ +package app_files_test + +import ( + . "github.com/cloudfoundry/cli/cf/app_files" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CF Ignore", func() { + It("excludes files based on exact path matches", func() { + ignore := NewCfIgnore(`the-dir/the-path`) + Expect(ignore.FileShouldBeIgnored("the-dir/the-path")).To(BeTrue()) + }) + + It("excludes the contents of directories based on exact path matches", func() { + ignore := NewCfIgnore(`dir1/dir2`) + Expect(ignore.FileShouldBeIgnored("dir1/dir2/the-file")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("dir1/dir2/dir3/the-file")).To(BeTrue()) + }) + + It("excludes files based on star patterns", func() { + ignore := NewCfIgnore(`dir1/*.so`) + Expect(ignore.FileShouldBeIgnored("dir1/file1.so")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("dir1/file2.cc")).To(BeFalse()) + }) + + It("excludes files based on double-star patterns", func() { + ignore := NewCfIgnore(`dir1/**/*.so`) + Expect(ignore.FileShouldBeIgnored("dir1/dir2/dir3/file1.so")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("different-dir/dir2/file.so")).To(BeFalse()) + }) + + It("allows files to be explicitly included", func() { + ignore := NewCfIgnore(` +node_modules/* +!node_modules/common +`) + + Expect(ignore.FileShouldBeIgnored("node_modules/something-else")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("node_modules/common")).To(BeFalse()) + }) + + It("applies the patterns in order from top to bottom", func() { + ignore := NewCfIgnore(` +stuff/* +!stuff/*.c +stuff/exclude.c`) + + Expect(ignore.FileShouldBeIgnored("stuff/something.txt")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("stuff/exclude.c")).To(BeTrue()) + Expect(ignore.FileShouldBeIgnored("stuff/include.c")).To(BeFalse()) + }) + + It("ignores certain commonly ingored files by default", func() { + ignore := NewCfIgnore(``) + Expect(ignore.FileShouldBeIgnored(".git/objects")).To(BeTrue()) + + ignore = NewCfIgnore(`!.git`) + Expect(ignore.FileShouldBeIgnored(".git/objects")).To(BeFalse()) + }) + + Describe("files named manifest.yml", func() { + var ( + ignore CfIgnore + ) + + BeforeEach(func() { + ignore = NewCfIgnore("") + }) + + It("ignores manifest.yml at the top level", func() { + Expect(ignore.FileShouldBeIgnored("manifest.yml")).To(BeTrue()) + }) + + It("does not ignore nested manifest.yml files", func() { + Expect(ignore.FileShouldBeIgnored("public/assets/manifest.yml")).To(BeFalse()) + }) + }) +}) diff --git a/cf/app_files/fakes/fake_app_files.go b/cf/app_files/fakes/fake_app_files.go new file mode 100644 index 00000000000..ec757915dfb --- /dev/null +++ b/cf/app_files/fakes/fake_app_files.go @@ -0,0 +1,178 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeAppFiles struct { + AppFilesInDirStub func(dir string) (appFiles []models.AppFileFields, err error) + appFilesInDirMutex sync.RWMutex + appFilesInDirArgsForCall []struct { + dir string + } + appFilesInDirReturns struct { + result1 []models.AppFileFields + result2 error + } + CopyFilesStub func(appFiles []models.AppFileFields, fromDir, toDir string) (err error) + copyFilesMutex sync.RWMutex + copyFilesArgsForCall []struct { + appFiles []models.AppFileFields + fromDir string + toDir string + } + copyFilesReturns struct { + result1 error + } + CountFilesStub func(directory string) int64 + countFilesMutex sync.RWMutex + countFilesArgsForCall []struct { + directory string + } + countFilesReturns struct { + result1 int64 + } + WalkAppFilesStub func(dir string, onEachFile func(string, string) error) (err error) + walkAppFilesMutex sync.RWMutex + walkAppFilesArgsForCall []struct { + dir string + onEachFile func(string, string) error + } + walkAppFilesReturns struct { + result1 error + } +} + +func (fake *FakeAppFiles) AppFilesInDir(dir string) (appFiles []models.AppFileFields, err error) { + fake.appFilesInDirMutex.Lock() + defer fake.appFilesInDirMutex.Unlock() + fake.appFilesInDirArgsForCall = append(fake.appFilesInDirArgsForCall, struct { + dir string + }{dir}) + if fake.AppFilesInDirStub != nil { + return fake.AppFilesInDirStub(dir) + } else { + return fake.appFilesInDirReturns.result1, fake.appFilesInDirReturns.result2 + } +} + +func (fake *FakeAppFiles) AppFilesInDirCallCount() int { + fake.appFilesInDirMutex.RLock() + defer fake.appFilesInDirMutex.RUnlock() + return len(fake.appFilesInDirArgsForCall) +} + +func (fake *FakeAppFiles) AppFilesInDirArgsForCall(i int) string { + fake.appFilesInDirMutex.RLock() + defer fake.appFilesInDirMutex.RUnlock() + return fake.appFilesInDirArgsForCall[i].dir +} + +func (fake *FakeAppFiles) AppFilesInDirReturns(result1 []models.AppFileFields, result2 error) { + fake.appFilesInDirReturns = struct { + result1 []models.AppFileFields + result2 error + }{result1, result2} +} + +func (fake *FakeAppFiles) CopyFiles(appFiles []models.AppFileFields, fromDir string, toDir string) (err error) { + fake.copyFilesMutex.Lock() + defer fake.copyFilesMutex.Unlock() + fake.copyFilesArgsForCall = append(fake.copyFilesArgsForCall, struct { + appFiles []models.AppFileFields + fromDir string + toDir string + }{appFiles, fromDir, toDir}) + if fake.CopyFilesStub != nil { + return fake.CopyFilesStub(appFiles, fromDir, toDir) + } else { + return fake.copyFilesReturns.result1 + } +} + +func (fake *FakeAppFiles) CopyFilesCallCount() int { + fake.copyFilesMutex.RLock() + defer fake.copyFilesMutex.RUnlock() + return len(fake.copyFilesArgsForCall) +} + +func (fake *FakeAppFiles) CopyFilesArgsForCall(i int) ([]models.AppFileFields, string, string) { + fake.copyFilesMutex.RLock() + defer fake.copyFilesMutex.RUnlock() + return fake.copyFilesArgsForCall[i].appFiles, fake.copyFilesArgsForCall[i].fromDir, fake.copyFilesArgsForCall[i].toDir +} + +func (fake *FakeAppFiles) CopyFilesReturns(result1 error) { + fake.copyFilesReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeAppFiles) CountFiles(directory string) int64 { + fake.countFilesMutex.Lock() + defer fake.countFilesMutex.Unlock() + fake.countFilesArgsForCall = append(fake.countFilesArgsForCall, struct { + directory string + }{directory}) + if fake.CountFilesStub != nil { + return fake.CountFilesStub(directory) + } else { + return fake.countFilesReturns.result1 + } +} + +func (fake *FakeAppFiles) CountFilesCallCount() int { + fake.countFilesMutex.RLock() + defer fake.countFilesMutex.RUnlock() + return len(fake.countFilesArgsForCall) +} + +func (fake *FakeAppFiles) CountFilesArgsForCall(i int) string { + fake.countFilesMutex.RLock() + defer fake.countFilesMutex.RUnlock() + return fake.countFilesArgsForCall[i].directory +} + +func (fake *FakeAppFiles) CountFilesReturns(result1 int64) { + fake.countFilesReturns = struct { + result1 int64 + }{result1} +} + +func (fake *FakeAppFiles) WalkAppFiles(dir string, onEachFile func(string, string) error) (err error) { + fake.walkAppFilesMutex.Lock() + defer fake.walkAppFilesMutex.Unlock() + fake.walkAppFilesArgsForCall = append(fake.walkAppFilesArgsForCall, struct { + dir string + onEachFile func(string, string) error + }{dir, onEachFile}) + if fake.WalkAppFilesStub != nil { + return fake.WalkAppFilesStub(dir, onEachFile) + } else { + return fake.walkAppFilesReturns.result1 + } +} + +func (fake *FakeAppFiles) WalkAppFilesCallCount() int { + fake.walkAppFilesMutex.RLock() + defer fake.walkAppFilesMutex.RUnlock() + return len(fake.walkAppFilesArgsForCall) +} + +func (fake *FakeAppFiles) WalkAppFilesArgsForCall(i int) (string, func(string, string) error) { + fake.walkAppFilesMutex.RLock() + defer fake.walkAppFilesMutex.RUnlock() + return fake.walkAppFilesArgsForCall[i].dir, fake.walkAppFilesArgsForCall[i].onEachFile +} + +func (fake *FakeAppFiles) WalkAppFilesReturns(result1 error) { + fake.walkAppFilesReturns = struct { + result1 error + }{result1} +} + +var _ AppFiles = new(FakeAppFiles) diff --git a/cf/app_files/fakes/fake_zipper.go b/cf/app_files/fakes/fake_zipper.go new file mode 100644 index 00000000000..4550c528ab9 --- /dev/null +++ b/cf/app_files/fakes/fake_zipper.go @@ -0,0 +1,176 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/app_files" + + "os" + "sync" +) + +type FakeZipper struct { + ZipStub func(dirToZip string, targetFile *os.File) (err error) + zipMutex sync.RWMutex + zipArgsForCall []struct { + dirToZip string + targetFile *os.File + } + zipReturns struct { + result1 error + } + IsZipFileStub func(path string) bool + isZipFileMutex sync.RWMutex + isZipFileArgsForCall []struct { + path string + } + isZipFileReturns struct { + result1 bool + } + UnzipStub func(appDir string, destDir string) (err error) + unzipMutex sync.RWMutex + unzipArgsForCall []struct { + appDir string + destDir string + } + unzipReturns struct { + result1 error + } + GetZipSizeStub func(zipFile *os.File) (int64, error) + getZipSizeMutex sync.RWMutex + getZipSizeArgsForCall []struct { + zipFile *os.File + } + getZipSizeReturns struct { + result1 int64 + result2 error + } +} + +func (fake *FakeZipper) Zip(dirToZip string, targetFile *os.File) (err error) { + fake.zipMutex.Lock() + defer fake.zipMutex.Unlock() + fake.zipArgsForCall = append(fake.zipArgsForCall, struct { + dirToZip string + targetFile *os.File + }{dirToZip, targetFile}) + if fake.ZipStub != nil { + return fake.ZipStub(dirToZip, targetFile) + } else { + return fake.zipReturns.result1 + } +} + +func (fake *FakeZipper) ZipCallCount() int { + fake.zipMutex.RLock() + defer fake.zipMutex.RUnlock() + return len(fake.zipArgsForCall) +} + +func (fake *FakeZipper) ZipArgsForCall(i int) (string, *os.File) { + fake.zipMutex.RLock() + defer fake.zipMutex.RUnlock() + return fake.zipArgsForCall[i].dirToZip, fake.zipArgsForCall[i].targetFile +} + +func (fake *FakeZipper) ZipReturns(result1 error) { + fake.zipReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeZipper) IsZipFile(path string) bool { + fake.isZipFileMutex.Lock() + defer fake.isZipFileMutex.Unlock() + fake.isZipFileArgsForCall = append(fake.isZipFileArgsForCall, struct { + path string + }{path}) + if fake.IsZipFileStub != nil { + return fake.IsZipFileStub(path) + } else { + return fake.isZipFileReturns.result1 + } +} + +func (fake *FakeZipper) IsZipFileCallCount() int { + fake.isZipFileMutex.RLock() + defer fake.isZipFileMutex.RUnlock() + return len(fake.isZipFileArgsForCall) +} + +func (fake *FakeZipper) IsZipFileArgsForCall(i int) string { + fake.isZipFileMutex.RLock() + defer fake.isZipFileMutex.RUnlock() + return fake.isZipFileArgsForCall[i].path +} + +func (fake *FakeZipper) IsZipFileReturns(result1 bool) { + fake.isZipFileReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeZipper) Unzip(appDir string, destDir string) (err error) { + fake.unzipMutex.Lock() + defer fake.unzipMutex.Unlock() + fake.unzipArgsForCall = append(fake.unzipArgsForCall, struct { + appDir string + destDir string + }{appDir, destDir}) + if fake.UnzipStub != nil { + return fake.UnzipStub(appDir, destDir) + } else { + return fake.unzipReturns.result1 + } +} + +func (fake *FakeZipper) UnzipCallCount() int { + fake.unzipMutex.RLock() + defer fake.unzipMutex.RUnlock() + return len(fake.unzipArgsForCall) +} + +func (fake *FakeZipper) UnzipArgsForCall(i int) (string, string) { + fake.unzipMutex.RLock() + defer fake.unzipMutex.RUnlock() + return fake.unzipArgsForCall[i].appDir, fake.unzipArgsForCall[i].destDir +} + +func (fake *FakeZipper) UnzipReturns(result1 error) { + fake.unzipReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeZipper) GetZipSize(zipFile *os.File) (int64, error) { + fake.getZipSizeMutex.Lock() + defer fake.getZipSizeMutex.Unlock() + fake.getZipSizeArgsForCall = append(fake.getZipSizeArgsForCall, struct { + zipFile *os.File + }{zipFile}) + if fake.GetZipSizeStub != nil { + return fake.GetZipSizeStub(zipFile) + } else { + return fake.getZipSizeReturns.result1, fake.getZipSizeReturns.result2 + } +} + +func (fake *FakeZipper) GetZipSizeCallCount() int { + fake.getZipSizeMutex.RLock() + defer fake.getZipSizeMutex.RUnlock() + return len(fake.getZipSizeArgsForCall) +} + +func (fake *FakeZipper) GetZipSizeArgsForCall(i int) *os.File { + fake.getZipSizeMutex.RLock() + defer fake.getZipSizeMutex.RUnlock() + return fake.getZipSizeArgsForCall[i].zipFile +} + +func (fake *FakeZipper) GetZipSizeReturns(result1 int64, result2 error) { + fake.getZipSizeReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +var _ Zipper = new(FakeZipper) diff --git a/cf/app_files/zipper.go b/cf/app_files/zipper.go new file mode 100644 index 00000000000..3e96e73b76c --- /dev/null +++ b/cf/app_files/zipper.go @@ -0,0 +1,129 @@ +package app_files + +import ( + "archive/zip" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/gofileutils/fileutils" + "io" + "os" + "path/filepath" +) + +type Zipper interface { + Zip(dirToZip string, targetFile *os.File) (err error) + IsZipFile(path string) bool + Unzip(appDir string, destDir string) (err error) + GetZipSize(zipFile *os.File) (int64, error) +} + +type ApplicationZipper struct{} + +func (zipper ApplicationZipper) Zip(dirOrZipFile string, targetFile *os.File) (err error) { + if zipper.IsZipFile(dirOrZipFile) { + err = fileutils.CopyPathToWriter(dirOrZipFile, targetFile) + } else { + err = writeZipFile(dirOrZipFile, targetFile) + } + targetFile.Seek(0, os.SEEK_SET) + return +} + +func (zipper ApplicationZipper) IsZipFile(file string) (result bool) { + _, err := zip.OpenReader(file) + return err == nil +} + +func writeZipFile(dir string, targetFile *os.File) error { + isEmpty, err := fileutils.IsDirEmpty(dir) + if err != nil { + return err + } + + if isEmpty { + return errors.NewEmptyDirError(dir) + } + + writer := zip.NewWriter(targetFile) + defer writer.Close() + + appfiles := ApplicationFiles{} + return appfiles.WalkAppFiles(dir, func(fileName string, fullPath string) error { + fileInfo, err := os.Stat(fullPath) + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + + header.Name = filepath.ToSlash(fileName) + + if fileInfo.IsDir() { + header.Name += "/" + } + + zipFilePart, err := writer.CreateHeader(header) + + if fileInfo.IsDir() { + return nil + } else { + return fileutils.CopyPathToWriter(fullPath, zipFilePart) + } + }) +} + +func (zipper ApplicationZipper) Unzip(appDir string, destDir string) (err error) { + r, err := zip.OpenReader(appDir) + if err != nil { + return + } + defer r.Close() + + for _, f := range r.File { + func() { + // Don't try to extract directories + if f.FileInfo().IsDir() { + return + } + + var rc io.ReadCloser + rc, err = f.Open() + if err != nil { + return + } + + // functional scope from above is important + // otherwise this only closes the last file handle + defer rc.Close() + + destFilePath := filepath.Join(destDir, f.Name) + + err = fileutils.CopyReaderToPath(rc, destFilePath) + if err != nil { + return + } + + err = os.Chmod(destFilePath, f.FileInfo().Mode()) + if err != nil { + return + } + }() + } + + return +} + +func (zipper ApplicationZipper) GetZipSize(zipFile *os.File) (int64, error) { + zipFileSize := int64(0) + + stat, err := zipFile.Stat() + if err != nil { + return 0, err + } + + zipFileSize = int64(stat.Size()) + + return zipFileSize, nil +} diff --git a/cf/app_files/zipper_test.go b/cf/app_files/zipper_test.go new file mode 100644 index 00000000000..ec172b0701a --- /dev/null +++ b/cf/app_files/zipper_test.go @@ -0,0 +1,170 @@ +package app_files_test + +import ( + "archive/zip" + "bytes" + . "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/gofileutils/fileutils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +func readFile(file *os.File) []byte { + bytes, err := ioutil.ReadAll(file) + Expect(err).NotTo(HaveOccurred()) + return bytes +} + +var _ = Describe("Zipper", func() { + var filesInZip = []string{ + "foo.txt", + "fooDir/", + "fooDir/bar/", + "lastDir/", + "subDir/", + "subDir/bar.txt", + "subDir/otherDir/", + "subDir/otherDir/file.txt", + } + + It("zips directories", func() { + fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { + workingDir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + dir := filepath.Join(workingDir, "../../fixtures/zip/") + err = os.Chmod(filepath.Join(dir, "subDir/bar.txt"), 0666) + Expect(err).NotTo(HaveOccurred()) + + zipper := ApplicationZipper{} + err = zipper.Zip(dir, zipFile) + Expect(err).NotTo(HaveOccurred()) + + fileStat, err := zipFile.Stat() + Expect(err).NotTo(HaveOccurred()) + + reader, err := zip.NewReader(zipFile, fileStat.Size()) + Expect(err).NotTo(HaveOccurred()) + + filenames := []string{} + for _, file := range reader.File { + filenames = append(filenames, file.Name) + } + + Expect(filenames).To(Equal(filesInZip)) + + readFileInZip := func(index int) (string, string) { + buf := &bytes.Buffer{} + file := reader.File[index] + fReader, err := file.Open() + _, err = io.Copy(buf, fReader) + + Expect(err).NotTo(HaveOccurred()) + + return file.Name, string(buf.Bytes()) + } + + Expect(err).NotTo(HaveOccurred()) + + name, contents := readFileInZip(0) + Expect(name).To(Equal("foo.txt")) + Expect(contents).To(Equal("This is a simple text file.")) + + name, contents = readFileInZip(5) + Expect(name).To(Equal("subDir/bar.txt")) + Expect(contents).To(Equal("I am in a subdirectory.")) + Expect(reader.File[5].FileInfo().Mode()).To(Equal(os.FileMode(0666))) + }) + }) + + It("is a no-op for a zipfile", func() { + fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { + dir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + zipper := ApplicationZipper{} + fixture := filepath.Join(dir, "../../fixtures/applications/example-app.zip") + err = zipper.Zip(fixture, zipFile) + Expect(err).NotTo(HaveOccurred()) + + zippedFile, err := os.Open(fixture) + Expect(err).NotTo(HaveOccurred()) + Expect(readFile(zipFile)).To(Equal(readFile(zippedFile))) + }) + }) + + It("returns an error when zipping fails", func() { + fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { + zipper := ApplicationZipper{} + err = zipper.Zip("/a/bogus/directory", zipFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("open /a/bogus/directory")) + }) + }) + + It("returns an error when the directory is empty", func() { + fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { + fileutils.TempDir("zip_test", func(emptyDir string, err error) { + zipper := ApplicationZipper{} + err = zipper.Zip(emptyDir, zipFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("is empty")) + }) + }) + }) + + Describe(".Unzip", func() { + It("extracts the zip file", func() { + filez := []string{ + "example-app/.cfignore", + "example-app/app.rb", + "example-app/config.ru", + "example-app/Gemfile", + "example-app/Gemfile.lock", + "example-app/ignore-me", + "example-app/manifest.yml", + } + + dir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + fileutils.TempDir("unzipped_app", func(tmpDir string, err error) { + zipper := ApplicationZipper{} + + fixture := filepath.Join(dir, "../../fixtures/applications/example-app.zip") + err = zipper.Unzip(fixture, tmpDir) + Expect(err).NotTo(HaveOccurred()) + for _, file := range filez { + _, err := os.Stat(filepath.Join(tmpDir, file)) + Expect(os.IsNotExist(err)).To(BeFalse()) + } + }) + }) + }) + + Describe(".GetZipSize", func() { + var zipper = ApplicationZipper{} + + It("returns the size of the zip file", func() { + dir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + zipFile := filepath.Join(dir, "../../fixtures/applications/example-app.zip") + + file, err := os.Open(zipFile) + Expect(err).NotTo(HaveOccurred()) + + fileSize, err := zipper.GetZipSize(file) + Expect(err).NotTo(HaveOccurred()) + Expect(fileSize).To(Equal(int64(1803))) + }) + + It("returns an error if the zip file cannot be found", func() { + tmpFile, _ := os.Open("fooBar") + _, sizeErr := zipper.GetZipSize(tmpFile) + Expect(sizeErr).To(HaveOccurred()) + }) + }) +}) diff --git a/cf/cf_suite_test.go b/cf/cf_suite_test.go new file mode 100644 index 00000000000..1a3fb00975f --- /dev/null +++ b/cf/cf_suite_test.go @@ -0,0 +1,13 @@ +package cf_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCf(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cf Suite") +} diff --git a/cf/command/command.go b/cf/command/command.go new file mode 100644 index 00000000000..994ce82dd42 --- /dev/null +++ b/cf/command/command.go @@ -0,0 +1,13 @@ +package command + +import ( + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/codegangsta/cli" +) + +type Command interface { + Metadata() command_metadata.CommandMetadata + GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) (reqs []requirements.Requirement, err error) + Run(context *cli.Context) +} diff --git a/cf/command/fakes/fake_command.go b/cf/command/fakes/fake_command.go new file mode 100644 index 00000000000..9ac7b8451d7 --- /dev/null +++ b/cf/command/fakes/fake_command.go @@ -0,0 +1,118 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/codegangsta/cli" +) + +type FakeCommand struct { + MetadataStub func() command_metadata.CommandMetadata + metadataMutex sync.RWMutex + metadataArgsForCall []struct{} + metadataReturns struct { + result1 command_metadata.CommandMetadata + } + GetRequirementsStub func(requirementsFactory requirements.Factory, context *cli.Context) (reqs []requirements.Requirement, err error) + getRequirementsMutex sync.RWMutex + getRequirementsArgsForCall []struct { + requirementsFactory requirements.Factory + context *cli.Context + } + getRequirementsReturns struct { + result1 []requirements.Requirement + result2 error + } + RunStub func(context *cli.Context) + runMutex sync.RWMutex + runArgsForCall []struct { + context *cli.Context + } +} + +func (fake *FakeCommand) Metadata() command_metadata.CommandMetadata { + fake.metadataMutex.Lock() + defer fake.metadataMutex.Unlock() + fake.metadataArgsForCall = append(fake.metadataArgsForCall, struct{}{}) + if fake.MetadataStub != nil { + return fake.MetadataStub() + } else { + return fake.metadataReturns.result1 + } +} + +func (fake *FakeCommand) MetadataCallCount() int { + fake.metadataMutex.RLock() + defer fake.metadataMutex.RUnlock() + return len(fake.metadataArgsForCall) +} + +func (fake *FakeCommand) MetadataReturns(result1 command_metadata.CommandMetadata) { + fake.MetadataStub = nil + fake.metadataReturns = struct { + result1 command_metadata.CommandMetadata + }{result1} +} + +func (fake *FakeCommand) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) (reqs []requirements.Requirement, err error) { + fake.getRequirementsMutex.Lock() + defer fake.getRequirementsMutex.Unlock() + fake.getRequirementsArgsForCall = append(fake.getRequirementsArgsForCall, struct { + requirementsFactory requirements.Factory + context *cli.Context + }{requirementsFactory, context}) + if fake.GetRequirementsStub != nil { + return fake.GetRequirementsStub(requirementsFactory, context) + } else { + return fake.getRequirementsReturns.result1, fake.getRequirementsReturns.result2 + } +} + +func (fake *FakeCommand) GetRequirementsCallCount() int { + fake.getRequirementsMutex.RLock() + defer fake.getRequirementsMutex.RUnlock() + return len(fake.getRequirementsArgsForCall) +} + +func (fake *FakeCommand) GetRequirementsArgsForCall(i int) (requirements.Factory, *cli.Context) { + fake.getRequirementsMutex.RLock() + defer fake.getRequirementsMutex.RUnlock() + return fake.getRequirementsArgsForCall[i].requirementsFactory, fake.getRequirementsArgsForCall[i].context +} + +func (fake *FakeCommand) GetRequirementsReturns(result1 []requirements.Requirement, result2 error) { + fake.GetRequirementsStub = nil + fake.getRequirementsReturns = struct { + result1 []requirements.Requirement + result2 error + }{result1, result2} +} + +func (fake *FakeCommand) Run(context *cli.Context) { + fake.runMutex.Lock() + defer fake.runMutex.Unlock() + fake.runArgsForCall = append(fake.runArgsForCall, struct { + context *cli.Context + }{context}) + if fake.RunStub != nil { + fake.RunStub(context) + } +} + +func (fake *FakeCommand) RunCallCount() int { + fake.runMutex.RLock() + defer fake.runMutex.RUnlock() + return len(fake.runArgsForCall) +} + +func (fake *FakeCommand) RunArgsForCall(i int) *cli.Context { + fake.runMutex.RLock() + defer fake.runMutex.RUnlock() + return fake.runArgsForCall[i].context +} + +var _ command.Command = new(FakeCommand) diff --git a/cf/command_factory/command_factory_suite_test.go b/cf/command_factory/command_factory_suite_test.go new file mode 100644 index 00000000000..75303566203 --- /dev/null +++ b/cf/command_factory/command_factory_suite_test.go @@ -0,0 +1,19 @@ +package command_factory_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCommandFactory(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Command Factory Suite") +} diff --git a/cf/command_factory/factory.go b/cf/command_factory/factory.go new file mode 100644 index 00000000000..b87c94b8e1f --- /dev/null +++ b/cf/command_factory/factory.go @@ -0,0 +1,329 @@ +package command_factory + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/actors/service_builder" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/actors/broker_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/commands/buildpack" + "github.com/cloudfoundry/cli/cf/commands/domain" + "github.com/cloudfoundry/cli/cf/commands/environmentvariablegroup" + "github.com/cloudfoundry/cli/cf/commands/featureflag" + "github.com/cloudfoundry/cli/cf/commands/organization" + "github.com/cloudfoundry/cli/cf/commands/plugin" + "github.com/cloudfoundry/cli/cf/commands/quota" + "github.com/cloudfoundry/cli/cf/commands/route" + "github.com/cloudfoundry/cli/cf/commands/securitygroup" + "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/commands/serviceaccess" + "github.com/cloudfoundry/cli/cf/commands/serviceauthtoken" + "github.com/cloudfoundry/cli/cf/commands/servicebroker" + "github.com/cloudfoundry/cli/cf/commands/space" + "github.com/cloudfoundry/cli/cf/commands/spacequota" + "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/words/generator" +) + +type Factory interface { + GetByCmdName(cmdName string) (cmd command.Command, err error) + CommandMetadatas() []command_metadata.CommandMetadata + CheckIfCoreCmdExists(cmdName string) bool +} + +type concreteFactory struct { + cmdsByName map[string]command.Command +} + +func NewFactory(ui terminal.UI, config core_config.ReadWriter, manifestRepo manifest.ManifestRepository, repoLocator api.RepositoryLocator, pluginConfig plugin_config.PluginConfiguration) (factory concreteFactory) { + factory.cmdsByName = make(map[string]command.Command) + + planBuilder := plan_builder.NewBuilder( + repoLocator.GetServicePlanRepository(), + repoLocator.GetServicePlanVisibilityRepository(), + repoLocator.GetOrganizationRepository(), + ) + + serviceBuilder := service_builder.NewBuilder( + repoLocator.GetServiceRepository(), + planBuilder, + ) + + brokerBuilder := broker_builder.NewBuilder( + repoLocator.GetServiceBrokerRepository(), + serviceBuilder, + ) + + factory.cmdsByName["api"] = commands.NewApi(ui, config, repoLocator.GetEndpointRepository()) + factory.cmdsByName["apps"] = application.NewListApps(ui, config, repoLocator.GetAppSummaryRepository()) + factory.cmdsByName["auth"] = commands.NewAuthenticate(ui, config, repoLocator.GetAuthenticationRepository()) + factory.cmdsByName["buildpacks"] = buildpack.NewListBuildpacks(ui, repoLocator.GetBuildpackRepository()) + factory.cmdsByName["config"] = commands.NewConfig(ui, config) + factory.cmdsByName["create-buildpack"] = buildpack.NewCreateBuildpack(ui, repoLocator.GetBuildpackRepository(), repoLocator.GetBuildpackBitsRepository()) + factory.cmdsByName["create-domain"] = domain.NewCreateDomain(ui, config, repoLocator.GetDomainRepository()) + factory.cmdsByName["create-org"] = organization.NewCreateOrg(ui, config, repoLocator.GetOrganizationRepository(), repoLocator.GetQuotaRepository()) + factory.cmdsByName["create-service"] = service.NewCreateService(ui, config, repoLocator.GetServiceRepository(), serviceBuilder) + + factory.cmdsByName["update-service"] = service.NewUpdateService( + ui, + config, + repoLocator.GetServiceRepository(), + plan_builder.NewBuilder( + repoLocator.GetServicePlanRepository(), + repoLocator.GetServicePlanVisibilityRepository(), + repoLocator.GetOrganizationRepository(), + ), + ) + + factory.cmdsByName["create-service-auth-token"] = serviceauthtoken.NewCreateServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) + factory.cmdsByName["create-service-broker"] = servicebroker.NewCreateServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) + factory.cmdsByName["create-user"] = user.NewCreateUser(ui, config, repoLocator.GetUserRepository()) + factory.cmdsByName["create-user-provided-service"] = service.NewCreateUserProvidedService(ui, config, repoLocator.GetUserProvidedServiceInstanceRepository()) + factory.cmdsByName["curl"] = commands.NewCurl(ui, config, repoLocator.GetCurlRepository()) + factory.cmdsByName["delete"] = application.NewDeleteApp(ui, config, repoLocator.GetApplicationRepository(), repoLocator.GetRouteRepository()) + factory.cmdsByName["delete-buildpack"] = buildpack.NewDeleteBuildpack(ui, repoLocator.GetBuildpackRepository()) + factory.cmdsByName["delete-domain"] = domain.NewDeleteDomain(ui, config, repoLocator.GetDomainRepository()) + factory.cmdsByName["delete-shared-domain"] = domain.NewDeleteSharedDomain(ui, config, repoLocator.GetDomainRepository()) + factory.cmdsByName["delete-org"] = organization.NewDeleteOrg(ui, config, repoLocator.GetOrganizationRepository()) + factory.cmdsByName["delete-orphaned-routes"] = route.NewDeleteOrphanedRoutes(ui, config, repoLocator.GetRouteRepository()) + factory.cmdsByName["delete-route"] = route.NewDeleteRoute(ui, config, repoLocator.GetRouteRepository()) + factory.cmdsByName["delete-service"] = service.NewDeleteService(ui, config, repoLocator.GetServiceRepository()) + factory.cmdsByName["delete-service-auth-token"] = serviceauthtoken.NewDeleteServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) + factory.cmdsByName["delete-service-broker"] = servicebroker.NewDeleteServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) + factory.cmdsByName["delete-space"] = space.NewDeleteSpace(ui, config, repoLocator.GetSpaceRepository()) + factory.cmdsByName["delete-user"] = user.NewDeleteUser(ui, config, repoLocator.GetUserRepository()) + factory.cmdsByName["domains"] = domain.NewListDomains(ui, config, repoLocator.GetDomainRepository()) + factory.cmdsByName["env"] = application.NewEnv(ui, config, repoLocator.GetApplicationRepository()) + factory.cmdsByName["events"] = application.NewEvents(ui, config, repoLocator.GetAppEventsRepository()) + factory.cmdsByName["files"] = application.NewFiles(ui, config, repoLocator.GetAppFilesRepository()) + factory.cmdsByName["login"] = commands.NewLogin(ui, config, repoLocator.GetAuthenticationRepository(), repoLocator.GetEndpointRepository(), repoLocator.GetOrganizationRepository(), repoLocator.GetSpaceRepository()) + factory.cmdsByName["logout"] = commands.NewLogout(ui, config) + factory.cmdsByName["logs"] = application.NewLogs(ui, config, repoLocator.GetLogsRepository()) + factory.cmdsByName["oauth-token"] = commands.NewOAuthToken(ui, config, repoLocator.GetAuthenticationRepository()) + factory.cmdsByName["org"] = organization.NewShowOrg(ui, config) + factory.cmdsByName["org-users"] = user.NewOrgUsers(ui, config, repoLocator.GetUserRepository()) + factory.cmdsByName["orgs"] = organization.NewListOrgs(ui, config, repoLocator.GetOrganizationRepository()) + factory.cmdsByName["passwd"] = commands.NewPassword(ui, repoLocator.GetPasswordRepository(), config) + factory.cmdsByName["purge-service-offering"] = service.NewPurgeServiceOffering(ui, config, repoLocator.GetServiceRepository()) + factory.cmdsByName["quotas"] = quota.NewListQuotas(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["quota"] = quota.NewShowQuota(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["create-quota"] = quota.NewCreateQuota(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["update-quota"] = quota.NewUpdateQuota(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["delete-quota"] = quota.NewDeleteQuota(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["rename"] = application.NewRenameApp(ui, config, repoLocator.GetApplicationRepository()) + factory.cmdsByName["rename-buildpack"] = buildpack.NewRenameBuildpack(ui, repoLocator.GetBuildpackRepository()) + factory.cmdsByName["rename-org"] = organization.NewRenameOrg(ui, config, repoLocator.GetOrganizationRepository()) + factory.cmdsByName["rename-service"] = service.NewRenameService(ui, config, repoLocator.GetServiceRepository()) + factory.cmdsByName["rename-service-broker"] = servicebroker.NewRenameServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) + factory.cmdsByName["rename-space"] = space.NewRenameSpace(ui, config, repoLocator.GetSpaceRepository()) + factory.cmdsByName["routes"] = route.NewListRoutes(ui, config, repoLocator.GetRouteRepository()) + factory.cmdsByName["check-route"] = route.NewCheckRoute(ui, config, repoLocator.GetRouteRepository(), repoLocator.GetDomainRepository()) + factory.cmdsByName["service"] = service.NewShowService(ui) + factory.cmdsByName["service-auth-tokens"] = serviceauthtoken.NewListServiceAuthTokens(ui, config, repoLocator.GetServiceAuthTokenRepository()) + factory.cmdsByName["service-brokers"] = servicebroker.NewListServiceBrokers(ui, config, repoLocator.GetServiceBrokerRepository()) + factory.cmdsByName["services"] = service.NewListServices(ui, config, repoLocator.GetServiceSummaryRepository()) + factory.cmdsByName["migrate-service-instances"] = service.NewMigrateServiceInstances(ui, config, repoLocator.GetServiceRepository()) + factory.cmdsByName["set-env"] = application.NewSetEnv(ui, config, repoLocator.GetApplicationRepository()) + factory.cmdsByName["set-org-role"] = user.NewSetOrgRole(ui, config, repoLocator.GetUserRepository()) + factory.cmdsByName["set-quota"] = organization.NewSetQuota(ui, config, repoLocator.GetQuotaRepository()) + factory.cmdsByName["create-shared-domain"] = domain.NewCreateSharedDomain(ui, config, repoLocator.GetDomainRepository()) + factory.cmdsByName["space"] = space.NewShowSpace(ui, config, repoLocator.GetSpaceQuotaRepository()) + factory.cmdsByName["space-users"] = user.NewSpaceUsers(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) + factory.cmdsByName["spaces"] = space.NewListSpaces(ui, config, repoLocator.GetSpaceRepository()) + factory.cmdsByName["stacks"] = commands.NewListStacks(ui, config, repoLocator.GetStackRepository()) + factory.cmdsByName["target"] = commands.NewTarget(ui, config, repoLocator.GetOrganizationRepository(), repoLocator.GetSpaceRepository()) + factory.cmdsByName["unbind-service"] = service.NewUnbindService(ui, config, repoLocator.GetServiceBindingRepository()) + factory.cmdsByName["unset-env"] = application.NewUnsetEnv(ui, config, repoLocator.GetApplicationRepository()) + factory.cmdsByName["unset-org-role"] = user.NewUnsetOrgRole(ui, config, repoLocator.GetUserRepository()) + factory.cmdsByName["unset-space-role"] = user.NewUnsetSpaceRole(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) + factory.cmdsByName["update-buildpack"] = buildpack.NewUpdateBuildpack(ui, repoLocator.GetBuildpackRepository(), repoLocator.GetBuildpackBitsRepository()) + factory.cmdsByName["update-service-broker"] = servicebroker.NewUpdateServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) + factory.cmdsByName["update-service-auth-token"] = serviceauthtoken.NewUpdateServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) + factory.cmdsByName["update-user-provided-service"] = service.NewUpdateUserProvidedService(ui, config, repoLocator.GetUserProvidedServiceInstanceRepository()) + factory.cmdsByName["create-security-group"] = securitygroup.NewCreateSecurityGroup(ui, config, repoLocator.GetSecurityGroupRepository()) + factory.cmdsByName["update-security-group"] = securitygroup.NewUpdateSecurityGroup(ui, config, repoLocator.GetSecurityGroupRepository()) + factory.cmdsByName["delete-security-group"] = securitygroup.NewDeleteSecurityGroup(ui, config, repoLocator.GetSecurityGroupRepository()) + factory.cmdsByName["security-group"] = securitygroup.NewShowSecurityGroup(ui, config, repoLocator.GetSecurityGroupRepository()) + factory.cmdsByName["security-groups"] = securitygroup.NewSecurityGroups(ui, config, repoLocator.GetSecurityGroupRepository()) + factory.cmdsByName["bind-staging-security-group"] = securitygroup.NewBindToStagingGroup( + ui, + config, + repoLocator.GetSecurityGroupRepository(), + repoLocator.GetStagingSecurityGroupsRepository(), + ) + factory.cmdsByName["staging-security-groups"] = securitygroup.NewListStagingSecurityGroups(ui, config, repoLocator.GetStagingSecurityGroupsRepository()) + factory.cmdsByName["unbind-staging-security-group"] = securitygroup.NewUnbindFromStagingGroup( + ui, + config, + repoLocator.GetSecurityGroupRepository(), + repoLocator.GetStagingSecurityGroupsRepository(), + ) + factory.cmdsByName["bind-running-security-group"] = securitygroup.NewBindToRunningGroup( + ui, + config, + repoLocator.GetSecurityGroupRepository(), + repoLocator.GetRunningSecurityGroupsRepository(), + ) + factory.cmdsByName["unbind-running-security-group"] = securitygroup.NewUnbindFromRunningGroup( + ui, + config, + repoLocator.GetSecurityGroupRepository(), + repoLocator.GetRunningSecurityGroupsRepository(), + ) + factory.cmdsByName["running-security-groups"] = securitygroup.NewListRunningSecurityGroups(ui, config, repoLocator.GetRunningSecurityGroupsRepository()) + factory.cmdsByName["bind-security-group"] = securitygroup.NewBindSecurityGroup( + ui, + config, + repoLocator.GetSecurityGroupRepository(), + repoLocator.GetSpaceRepository(), + repoLocator.GetOrganizationRepository(), + repoLocator.GetSecurityGroupSpaceBinder(), + ) + factory.cmdsByName["unbind-security-group"] = securitygroup.NewUnbindSecurityGroup(ui, config, repoLocator.GetSecurityGroupRepository(), repoLocator.GetOrganizationRepository(), repoLocator.GetSpaceRepository(), repoLocator.GetSecurityGroupSpaceBinder()) + + createRoute := route.NewCreateRoute(ui, config, repoLocator.GetRouteRepository()) + factory.cmdsByName["create-route"] = createRoute + factory.cmdsByName["map-route"] = route.NewMapRoute(ui, config, repoLocator.GetRouteRepository(), createRoute) + factory.cmdsByName["unmap-route"] = route.NewUnmapRoute(ui, config, repoLocator.GetRouteRepository()) + + displayApp := application.NewShowApp(ui, config, repoLocator.GetAppSummaryRepository(), repoLocator.GetAppInstancesRepository()) + start := application.NewStart(ui, config, displayApp, repoLocator.GetApplicationRepository(), repoLocator.GetAppInstancesRepository(), repoLocator.GetLogsRepository()) + stop := application.NewStop(ui, config, repoLocator.GetApplicationRepository()) + restart := application.NewRestart(ui, config, start, stop) + restage := application.NewRestage(ui, config, repoLocator.GetApplicationRepository(), start) + bind := service.NewBindService(ui, config, repoLocator.GetServiceBindingRepository()) + + factory.cmdsByName["app"] = displayApp + factory.cmdsByName["bind-service"] = bind + factory.cmdsByName["start"] = start + factory.cmdsByName["stop"] = stop + factory.cmdsByName["restart"] = restart + factory.cmdsByName["restage"] = restage + factory.cmdsByName["push"] = application.NewPush( + ui, config, manifestRepo, start, stop, bind, + repoLocator.GetApplicationRepository(), + repoLocator.GetDomainRepository(), + repoLocator.GetRouteRepository(), + repoLocator.GetStackRepository(), + repoLocator.GetServiceRepository(), + repoLocator.GetAuthenticationRepository(), + generator.NewWordGenerator(), + actors.NewPushActor(repoLocator.GetApplicationBitsRepository(), app_files.ApplicationZipper{}, app_files.ApplicationFiles{}), + app_files.ApplicationZipper{}, + app_files.ApplicationFiles{}) + + factory.cmdsByName["scale"] = application.NewScale(ui, config, restart, repoLocator.GetApplicationRepository()) + + spaceRoleSetter := user.NewSetSpaceRole(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) + factory.cmdsByName["set-space-role"] = spaceRoleSetter + factory.cmdsByName["create-space"] = space.NewCreateSpace(ui, config, spaceRoleSetter, repoLocator.GetSpaceRepository(), repoLocator.GetOrganizationRepository(), repoLocator.GetUserRepository(), repoLocator.GetSpaceQuotaRepository()) + + factory.cmdsByName["service-access"] = serviceaccess.NewServiceAccess( + ui, config, + actors.NewServiceHandler( + repoLocator.GetOrganizationRepository(), + brokerBuilder, + serviceBuilder, + ), + repoLocator.GetAuthenticationRepository(), + ) + factory.cmdsByName["enable-service-access"] = serviceaccess.NewEnableServiceAccess( + ui, config, + actors.NewServicePlanHandler( + repoLocator.GetServicePlanRepository(), + repoLocator.GetServicePlanVisibilityRepository(), + repoLocator.GetOrganizationRepository(), + planBuilder, + serviceBuilder, + ), + repoLocator.GetAuthenticationRepository(), + ) + factory.cmdsByName["disable-service-access"] = serviceaccess.NewDisableServiceAccess( + ui, config, + actors.NewServicePlanHandler( + repoLocator.GetServicePlanRepository(), + repoLocator.GetServicePlanVisibilityRepository(), + repoLocator.GetOrganizationRepository(), + planBuilder, + serviceBuilder, + ), + repoLocator.GetAuthenticationRepository(), + ) + + factory.cmdsByName["marketplace"] = service.NewMarketplaceServices(ui, config, serviceBuilder) + + factory.cmdsByName["create-space-quota"] = spacequota.NewCreateSpaceQuota(ui, config, repoLocator.GetSpaceQuotaRepository(), repoLocator.GetOrganizationRepository()) + factory.cmdsByName["delete-space-quota"] = spacequota.NewDeleteSpaceQuota(ui, config, repoLocator.GetSpaceQuotaRepository()) + + factory.cmdsByName["space-quotas"] = spacequota.NewListSpaceQuotas(ui, config, repoLocator.GetSpaceQuotaRepository()) + factory.cmdsByName["space-quota"] = spacequota.NewSpaceQuota(ui, config, repoLocator.GetSpaceQuotaRepository()) + factory.cmdsByName["update-space-quota"] = spacequota.NewUpdateSpaceQuota(ui, config, repoLocator.GetSpaceQuotaRepository()) + factory.cmdsByName["set-space-quota"] = spacequota.NewSetSpaceQuota(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetSpaceQuotaRepository()) + factory.cmdsByName["unset-space-quota"] = spacequota.NewUnsetSpaceQuota(ui, config, repoLocator.GetSpaceQuotaRepository(), repoLocator.GetSpaceRepository()) + factory.cmdsByName["feature-flags"] = featureflag.NewListFeatureFlags(ui, config, repoLocator.GetFeatureFlagRepository()) + factory.cmdsByName["feature-flag"] = featureflag.NewShowFeatureFlag(ui, config, repoLocator.GetFeatureFlagRepository()) + factory.cmdsByName["enable-feature-flag"] = featureflag.NewEnableFeatureFlag(ui, config, repoLocator.GetFeatureFlagRepository()) + factory.cmdsByName["disable-feature-flag"] = featureflag.NewDisableFeatureFlag(ui, config, repoLocator.GetFeatureFlagRepository()) + factory.cmdsByName["running-environment-variable-group"] = environmentvariablegroup.NewRunningEnvironmentVariableGroup(ui, config, repoLocator.GetEnvironmentVariableGroupsRepository()) + factory.cmdsByName["staging-environment-variable-group"] = environmentvariablegroup.NewStagingEnvironmentVariableGroup(ui, config, repoLocator.GetEnvironmentVariableGroupsRepository()) + factory.cmdsByName["set-staging-environment-variable-group"] = environmentvariablegroup.NewSetStagingEnvironmentVariableGroup(ui, config, repoLocator.GetEnvironmentVariableGroupsRepository()) + factory.cmdsByName["set-running-environment-variable-group"] = environmentvariablegroup.NewSetRunningEnvironmentVariableGroup(ui, config, repoLocator.GetEnvironmentVariableGroupsRepository()) + + factory.cmdsByName["uninstall-plugin"] = plugin.NewPluginUninstall(ui, pluginConfig) + factory.cmdsByName["install-plugin"] = plugin.NewPluginInstall(ui, pluginConfig, factory.cmdsByName) + factory.cmdsByName["plugins"] = plugin.NewPlugins(ui, pluginConfig) + factory.cmdsByName["copy-source"] = application.NewCopySource( + ui, + config, + repoLocator.GetAuthenticationRepository(), + repoLocator.GetApplicationRepository(), + repoLocator.GetOrganizationRepository(), + repoLocator.GetSpaceRepository(), + repoLocator.GetCopyApplicationSourceRepository(), + restart, //note this is built up above. + ) + return +} + +func (f concreteFactory) GetByCmdName(cmdName string) (cmd command.Command, err error) { + cmd, found := f.cmdsByName[cmdName] + if !found { + err = errors.New(T("Command not found")) + } + return +} + +func (f concreteFactory) CheckIfCoreCmdExists(cmdName string) bool { + if _, exists := f.cmdsByName[cmdName]; exists { + return true + } + + for _, singleCmd := range f.cmdsByName { + metaData := singleCmd.Metadata() + if metaData.ShortName == cmdName { + return true + } + } + + return false +} + +func (factory concreteFactory) CommandMetadatas() (commands []command_metadata.CommandMetadata) { + for _, command := range factory.cmdsByName { + commands = append(commands, command.Metadata()) + } + return +} diff --git a/cf/command_factory/factory_test.go b/cf/command_factory/factory_test.go new file mode 100644 index 00000000000..56f8bf3b265 --- /dev/null +++ b/cf/command_factory/factory_test.go @@ -0,0 +1,106 @@ +package command_factory_test + +import ( + "os" + "path/filepath" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/api" + testPluginConfig "github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/command_factory" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("factory", func() { + var ( + factory Factory + ) + + BeforeEach(func() { + fakeUI := &testterm.FakeUI{} + config := testconfig.NewRepository() + manifestRepo := manifest.NewManifestDiskRepository() + pluginConfig := &testPluginConfig.FakePluginConfiguration{} + repoLocator := api.NewRepositoryLocator(config, map[string]net.Gateway{ + "auth": net.NewUAAGateway(config, fakeUI), + "cloud-controller": net.NewCloudControllerGateway(config, time.Now, fakeUI), + "uaa": net.NewUAAGateway(config, fakeUI), + }) + + factory = NewFactory(fakeUI, config, manifestRepo, repoLocator, pluginConfig) + }) + + It("provides the metadata for its commands", func() { + commands := factory.CommandMetadatas() + + suffixesToIgnore := []string{ + "i18n_init.go", // ignore all i18n initializers + "_test.go", // ignore test files + ".test", // ignore generated .test (temporary files) + "#", // emacs autosave files + } + + err := filepath.Walk("../commands", func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + for _, suffix := range suffixesToIgnore { + if strings.HasSuffix(path, suffix) { + return nil + } + } + + extension := filepath.Ext(info.Name()) + expectedCommandName := strings.Replace(info.Name()[0:len(info.Name())-len(extension)], "_", "-", -1) + + matchingCount := 0 + for _, command := range commands { + if command.Name == expectedCommandName { + matchingCount++ + } + } + + Expect(matchingCount).To(Equal(1), "this file has no corresponding command: "+info.Name()) + return nil + }) + + Expect(err).NotTo(HaveOccurred()) + }) + Describe("GetByCmdName", func() { + It("returns the cmd if it exists", func() { + cmd, err := factory.GetByCmdName("push") + Expect(cmd).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if it does not exist", func() { + cmd, err := factory.GetByCmdName("FOOBARRRRR") + Expect(cmd).To(BeNil()) + Expect(err).To(HaveOccurred()) + }) + }) + Describe("CheckIfCoreCmdExists", func() { + It("returns true if the cmd exists", func() { + exists := factory.CheckIfCoreCmdExists("push") + Expect(exists).To(BeTrue()) + }) + + It("retruns true if the cmd short name exists", func() { + exists := factory.CheckIfCoreCmdExists("p") + Expect(exists).To(BeTrue()) + }) + + It("returns an error if it does not exist", func() { + exists := factory.CheckIfCoreCmdExists("FOOOOBARRRR") + Expect(exists).To(BeFalse()) + }) + }) +}) diff --git a/cf/command_metadata/command_metadata.go b/cf/command_metadata/command_metadata.go new file mode 100644 index 00000000000..78a0180b61d --- /dev/null +++ b/cf/command_metadata/command_metadata.go @@ -0,0 +1,12 @@ +package command_metadata + +import "github.com/codegangsta/cli" + +type CommandMetadata struct { + Name string + ShortName string + Usage string + Description string + Flags []cli.Flag + SkipFlagParsing bool +} diff --git a/cf/command_runner/command_runner_suite_test.go b/cf/command_runner/command_runner_suite_test.go new file mode 100644 index 00000000000..74c512502a5 --- /dev/null +++ b/cf/command_runner/command_runner_suite_test.go @@ -0,0 +1,19 @@ +package command_runner_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCommandRunner(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Command Runner Suite") +} diff --git a/cf/command_runner/runner.go b/cf/command_runner/runner.go new file mode 100644 index 00000000000..28f9a8587fd --- /dev/null +++ b/cf/command_runner/runner.go @@ -0,0 +1,52 @@ +package command_runner + +import ( + "errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" + + "github.com/cloudfoundry/cli/cf/command_factory" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/codegangsta/cli" +) + +type Runner interface { + RunCmdByName(cmdName string, c *cli.Context) (err error) +} + +type ConcreteRunner struct { + cmdFactory command_factory.Factory + requirementsFactory requirements.Factory + ui terminal.UI +} + +func NewRunner(cmdFactory command_factory.Factory, requirementsFactory requirements.Factory, ui terminal.UI) (runner ConcreteRunner) { + runner.cmdFactory = cmdFactory + runner.requirementsFactory = requirementsFactory + runner.ui = ui + return +} + +func (runner ConcreteRunner) RunCmdByName(cmdName string, c *cli.Context) error { + cmd, err := runner.cmdFactory.GetByCmdName(cmdName) + if err != nil { + runner.ui.Say(T("Error finding command {{.CmdName}}\n", map[string]interface{}{"CmdName": cmdName})) + return err + } + + requirements, err := cmd.GetRequirements(runner.requirementsFactory, c) + if err != nil { + return err + } + + for _, requirement := range requirements { + success := requirement.Execute() + if !success { + err = errors.New(T("Error in requirement")) + return err + } + } + + cmd.Run(c) + return nil +} diff --git a/cf/command_runner/runner_test.go b/cf/command_runner/runner_test.go new file mode 100644 index 00000000000..56afdec9e23 --- /dev/null +++ b/cf/command_runner/runner_test.go @@ -0,0 +1,92 @@ +package command_runner_test + +import ( + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + . "github.com/cloudfoundry/cli/cf/command_runner" + "github.com/cloudfoundry/cli/cf/requirements" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/codegangsta/cli" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type TestCommandFactory struct { + Cmd command.Command + CmdName string +} + +func (f *TestCommandFactory) GetByCmdName(cmdName string) (cmd command.Command, err error) { + f.CmdName = cmdName + cmd = f.Cmd + return +} + +func (f *TestCommandFactory) CheckIfCoreCmdExists(cmdName string) bool { + return true +} + +func (fake *TestCommandFactory) CommandMetadatas() []command_metadata.CommandMetadata { + return []command_metadata.CommandMetadata{} +} + +type TestCommand struct { + Reqs []requirements.Requirement + WasRunWith *cli.Context +} + +func (cmd *TestCommand) GetRequirements(_ requirements.Factory, _ *cli.Context) (reqs []requirements.Requirement, err error) { + reqs = cmd.Reqs + return +} + +func (command *TestCommand) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{} +} + +func (cmd *TestCommand) Run(c *cli.Context) { + cmd.WasRunWith = c +} + +type TestRequirement struct { + Passes bool + WasExecuted bool +} + +func (r *TestRequirement) Execute() (success bool) { + r.WasExecuted = true + + if !r.Passes { + return false + } + + return true +} + +var _ = Describe("Requirements runner", func() { + It("runs", func() { + passingReq := TestRequirement{Passes: true} + failingReq := TestRequirement{Passes: false} + lastReq := TestRequirement{Passes: true} + + cmd := TestCommand{ + Reqs: []requirements.Requirement{&passingReq, &failingReq, &lastReq}, + } + + cmdFactory := &TestCommandFactory{Cmd: &cmd} + runner := NewRunner(cmdFactory, nil, nil) + + ctxt := testcmd.NewContext("login", []string{}) + err := runner.RunCmdByName("some-cmd", ctxt) + + Expect(cmdFactory.CmdName).To(Equal("some-cmd")) + + Expect(passingReq.WasExecuted).To(BeTrue()) + Expect(failingReq.WasExecuted).To(BeTrue()) + + Expect(lastReq.WasExecuted).To(BeFalse()) + Expect(cmd.WasRunWith).To(BeNil()) + + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/cf/commands/api.go b/cf/commands/api.go new file mode 100644 index 00000000000..4a00b8e8cad --- /dev/null +++ b/cf/commands/api.go @@ -0,0 +1,97 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Api struct { + ui terminal.UI + endpointRepo api.EndpointRepository + config core_config.ReadWriter +} + +func NewApi(ui terminal.UI, config core_config.ReadWriter, endpointRepo api.EndpointRepository) (cmd Api) { + cmd.ui = ui + cmd.config = config + cmd.endpointRepo = endpointRepo + return +} + +func (cmd Api) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "api", + Description: T("Set or view target api url"), + Usage: T("CF_NAME api [URL]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "skip-ssl-validation", Usage: T("Please don't")}, + }, + } +} + +func (cmd Api) GetRequirements(_ requirements.Factory, _ *cli.Context) (reqs []requirements.Requirement, err error) { + return +} + +func (cmd Api) Run(c *cli.Context) { + if len(c.Args()) == 0 { + if cmd.config.ApiEndpoint() == "" { + cmd.ui.Say(fmt.Sprintf(T("No api endpoint set. Use '{{.Name}}' to set an endpoint", + map[string]interface{}{"Name": terminal.CommandColor(cf.Name() + " api")}))) + } else { + cmd.ui.Say(T("API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + map[string]interface{}{"ApiEndpoint": terminal.EntityNameColor(cmd.config.ApiEndpoint()), + "ApiVersion": terminal.EntityNameColor(cmd.config.ApiVersion())})) + } + return + } + + endpoint := c.Args()[0] + + cmd.ui.Say(T("Setting api endpoint to {{.Endpoint}}...", + map[string]interface{}{"Endpoint": terminal.EntityNameColor(endpoint)})) + cmd.setApiEndpoint(endpoint, c.Bool("skip-ssl-validation"), cmd.Metadata().Name) + cmd.ui.Ok() + + cmd.ui.Say("") + cmd.ui.ShowConfiguration(cmd.config) +} + +func (cmd Api) setApiEndpoint(endpoint string, skipSSL bool, cmdName string) { + if strings.HasSuffix(endpoint, "/") { + endpoint = strings.TrimSuffix(endpoint, "/") + } + + cmd.config.SetSSLDisabled(skipSSL) + endpoint, err := cmd.endpointRepo.UpdateEndpoint(endpoint) + + if err != nil { + cmd.config.SetApiEndpoint("") + cmd.config.SetSSLDisabled(false) + + switch typedErr := err.(type) { + case *errors.InvalidSSLCert: + cfApiCommand := terminal.CommandColor(fmt.Sprintf("%s %s --skip-ssl-validation", cf.Name(), cmdName)) + tipMessage := fmt.Sprintf(T("TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + map[string]interface{}{"ApiCommand": cfApiCommand})) + cmd.ui.Failed(T("Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + map[string]interface{}{"URL": typedErr.URL, "TipMessage": tipMessage})) + default: + cmd.ui.Failed(typedErr.Error()) + } + } + + if !strings.HasPrefix(endpoint, "https://") { + cmd.ui.Say(terminal.WarningColor(T("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n"))) + } +} diff --git a/cf/commands/api_test.go b/cf/commands/api_test.go new file mode 100644 index 00000000000..996dab99519 --- /dev/null +++ b/cf/commands/api_test.go @@ -0,0 +1,160 @@ +package commands_test + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +func callApi(args []string, config core_config.ReadWriter, endpointRepo *testapi.FakeEndpointRepo) (ui *testterm.FakeUI) { + ui = new(testterm.FakeUI) + + cmd := NewApi(ui, config, endpointRepo) + requirementsFactory := &testreq.FakeReqFactory{} + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} + +var _ = Describe("api command", func() { + var ( + config core_config.ReadWriter + endpointRepo *testapi.FakeEndpointRepo + ) + + BeforeEach(func() { + config = testconfig.NewRepository() + endpointRepo = &testapi.FakeEndpointRepo{} + }) + + Context("when the api endpoint's ssl certificate is invalid", func() { + It("warns the user and prints out a tip", func() { + endpointRepo.UpdateEndpointError = errors.NewInvalidSSLCert("https://buttontomatoes.org", "why? no. go away") + ui := callApi([]string{"https://buttontomatoes.org"}, config, endpointRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"SSL Cert", "https://buttontomatoes.org"}, + []string{"TIP", "--skip-ssl-validation"}, + )) + }) + }) + + Context("when the user does not provide an endpoint", func() { + Context("when the endpoint is set in the config", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + config.SetApiEndpoint("https://api.run.pivotal.io") + config.SetApiVersion("2.0") + config.SetSSLDisabled(true) + + ui = new(testterm.FakeUI) + requirementsFactory = &testreq.FakeReqFactory{} + }) + + JustBeforeEach(func() { + testcmd.RunCommand(NewApi(ui, config, endpointRepo), []string{}, requirementsFactory) + }) + + It("prints out the api endpoint", func() { + Expect(ui.Outputs).To(ContainSubstrings([]string{"https://api.run.pivotal.io", "2.0"})) + }) + + It("should not change the SSL setting in the config", func() { + Expect(config.IsSSLDisabled()).To(BeTrue()) + }) + }) + + Context("when the endpoint is not set in the config", func() { + It("prompts the user to set an endpoint", func() { + ui := callApi([]string{}, config, endpointRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No api endpoint set", fmt.Sprintf("Use '%s api' to set an endpoint", cf.Name())}, + )) + }) + }) + }) + + Context("when the user provides the --skip-ssl-validation flag", func() { + It("updates the SSLDisabled field in config", func() { + config.SetSSLDisabled(false) + callApi([]string{"--skip-ssl-validation", "https://example.com"}, config, endpointRepo) + + Expect(config.IsSSLDisabled()).To(Equal(true)) + }) + }) + + Context("the user provides an endpoint", func() { + var ( + ui *testterm.FakeUI + ) + + Describe("when the user passed in the skip-ssl-validation flag", func() { + It("disables SSL validation in the config", func() { + ui = callApi([]string{"--skip-ssl-validation", "https://example.com"}, config, endpointRepo) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("https://example.com")) + Expect(config.IsSSLDisabled()).To(BeTrue()) + }) + }) + + Context("when the ssl certificate is valid", func() { + It("updates the api endpoint with the given url", func() { + ui = callApi([]string{"https://example.com"}, config, endpointRepo) + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("https://example.com")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting api endpoint to", "example.com"}, + []string{"OK"}, + )) + }) + + It("trims trailing slashes from the api endpoint", func() { + ui = callApi([]string{"https://example.com/"}, config, endpointRepo) + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("https://example.com")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting api endpoint to", "example.com"}, + []string{"OK"}, + )) + }) + }) + + Context("when the ssl certificate is invalid", func() { + BeforeEach(func() { + endpointRepo.UpdateEndpointError = errors.NewInvalidSSLCert("https://example.com", "it don't work") + }) + + It("fails and gives the user a helpful message about skipping", func() { + ui := callApi([]string{"https://example.com"}, config, endpointRepo) + + Expect(config.ApiEndpoint()).To(Equal("")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Invalid SSL Cert", "https://example.com"}, + []string{"TIP", "api"}, + )) + }) + }) + + Describe("unencrypted http endpoints", func() { + It("warns the user", func() { + ui = callApi([]string{"http://example.com"}, config, endpointRepo) + Expect(ui.Outputs).To(ContainSubstrings([]string{"Warning"})) + }) + }) + }) +}) diff --git a/cf/commands/application/app.go b/cf/commands/application/app.go new file mode 100644 index 00000000000..9ff6e436b3f --- /dev/null +++ b/cf/commands/application/app.go @@ -0,0 +1,156 @@ +package application + +import ( + "fmt" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/app_instances" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/ui_helpers" + "github.com/codegangsta/cli" +) + +type ShowApp struct { + ui terminal.UI + config core_config.Reader + appSummaryRepo api.AppSummaryRepository + appInstancesRepo app_instances.AppInstancesRepository + appReq requirements.ApplicationRequirement +} + +type ApplicationDisplayer interface { + ShowApp(app models.Application, orgName string, spaceName string) +} + +func NewShowApp(ui terminal.UI, config core_config.Reader, appSummaryRepo api.AppSummaryRepository, appInstancesRepo app_instances.AppInstancesRepository) (cmd *ShowApp) { + cmd = new(ShowApp) + cmd.ui = ui + cmd.config = config + cmd.appSummaryRepo = appSummaryRepo + cmd.appInstancesRepo = appInstancesRepo + return +} + +func (cmd *ShowApp) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "app", + Description: T("Display health and status for app"), + Usage: T("CF_NAME app APP"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "guid", Usage: T("Retrieve and display the given app's guid. All other health and status output for the app is suppressed.")}, + }, + } +} + +func (cmd *ShowApp) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *ShowApp) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + + if c.Bool("guid") { + cmd.ui.Say(app.Guid) + } else { + cmd.ShowApp(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) + } +} + +func (cmd *ShowApp) ShowApp(app models.Application, orgName, spaceName string) { + cmd.ui.Say(T("Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(orgName), + "SpaceName": terminal.EntityNameColor(spaceName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + application, apiErr := cmd.appSummaryRepo.GetSummary(app.Guid) + + appIsStopped := (application.State == "stopped") + if err, ok := apiErr.(errors.HttpError); ok { + if err.ErrorCode() == errors.APP_STOPPED || err.ErrorCode() == errors.APP_NOT_STAGED { + appIsStopped = true + } + } + + if apiErr != nil && !appIsStopped { + cmd.ui.Failed(apiErr.Error()) + return + } + + var instances []models.AppInstanceFields + instances, apiErr = cmd.appInstancesRepo.GetInstances(app.Guid) + if apiErr != nil && !appIsStopped { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("\n%s %s", terminal.HeaderColor(T("requested state:")), ui_helpers.ColoredAppState(application.ApplicationFields)) + cmd.ui.Say("%s %s", terminal.HeaderColor(T("instances:")), ui_helpers.ColoredAppInstances(application.ApplicationFields)) + cmd.ui.Say(T("{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + map[string]interface{}{ + "Usage": terminal.HeaderColor(T("usage:")), + "FormattedMemory": formatters.ByteSize(application.Memory * formatters.MEGABYTE), + "InstanceCount": application.InstanceCount})) + + var urls []string + for _, route := range application.Routes { + urls = append(urls, route.URL()) + } + + cmd.ui.Say("%s %s", terminal.HeaderColor(T("urls:")), strings.Join(urls, ", ")) + var lastUpdated string + if application.PackageUpdatedAt != nil { + lastUpdated = application.PackageUpdatedAt.Format("Mon Jan 2 15:04:05 MST 2006") + } else { + lastUpdated = "unknown" + } + cmd.ui.Say("%s %s\n", terminal.HeaderColor(T("last uploaded:")), lastUpdated) + + if appIsStopped { + cmd.ui.Say(T("There are no running instances of this app.")) + return + } + + table := terminal.NewTable(cmd.ui, []string{"", T("state"), T("since"), T("cpu"), T("memory"), T("disk")}) + + for index, instance := range instances { + table.Add( + fmt.Sprintf("#%d", index), + ui_helpers.ColoredInstanceState(instance), + instance.Since.Format("2006-01-02 03:04:05 PM"), + fmt.Sprintf("%.1f%%", instance.CpuUsage*100), + fmt.Sprintf(T("{{.MemUsage}} of {{.MemQuota}}", + map[string]interface{}{ + "MemUsage": formatters.ByteSize(instance.MemUsage), + "MemQuota": formatters.ByteSize(instance.MemQuota)})), + fmt.Sprintf(T("{{.DiskUsage}} of {{.DiskQuota}}", + map[string]interface{}{ + "DiskUsage": formatters.ByteSize(instance.DiskUsage), + "DiskQuota": formatters.ByteSize(instance.DiskQuota)})), + ) + } + + table.Print() +} diff --git a/cf/commands/application/app_test.go b/cf/commands/application/app_test.go new file mode 100644 index 00000000000..935699f7d06 --- /dev/null +++ b/cf/commands/application/app_test.go @@ -0,0 +1,258 @@ +package application_test + +import ( + "time" + + testAppInstanaces "github.com/cloudfoundry/cli/cf/api/app_instances/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + testtime "github.com/cloudfoundry/cli/testhelpers/time" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("app Command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + appSummaryRepo *testapi.FakeAppSummaryRepo + appInstancesRepo *testAppInstanaces.FakeAppInstancesRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + appSummaryRepo = &testapi.FakeAppSummaryRepo{} + appInstancesRepo = &testAppInstanaces.FakeAppInstancesRepository{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedSpaceSuccess: true, + } + }) + + runCommand := func(args ...string) bool { + cmd := NewShowApp(ui, configRepo, appSummaryRepo, appInstancesRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails if not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("cf-plays-dwarf-fortress")).To(BeFalse()) + }) + + It("fails if a space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + Expect(runCommand("cf-plays-dwarf-fortress")).To(BeFalse()) + }) + + It("fails with usage when not provided exactly one arg", func() { + passed := runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + + }) + + Describe("displaying a summary of an app", func() { + BeforeEach(func() { + app := makeAppWithRoute("my-app") + appInstance := models.AppInstanceFields{ + State: models.InstanceRunning, + Since: testtime.MustParse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2012"), + CpuUsage: 1.0, + DiskQuota: 1 * formatters.GIGABYTE, + DiskUsage: 32 * formatters.MEGABYTE, + MemQuota: 64 * formatters.MEGABYTE, + MemUsage: 13 * formatters.BYTE, + } + + appInstance2 := models.AppInstanceFields{ + State: models.InstanceDown, + Since: testtime.MustParse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Apr 1 15:04:05 -0700 MST 2012"), + } + + instances := []models.AppInstanceFields{appInstance, appInstance2} + + appSummaryRepo.GetSummarySummary = app + appInstancesRepo.GetInstancesReturns(instances, nil) + requirementsFactory.Application = app + }) + + It("displays a summary of the app", func() { + runCommand("my-app") + + Expect(appSummaryRepo.GetSummaryAppGuid).To(Equal("app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Showing health and status", "my-app"}, + []string{"state", "started"}, + []string{"instances", "2/2"}, + []string{"usage", "256M x 2 instances"}, + []string{"urls", "my-app.example.com", "foo.example.com"}, + []string{"last uploaded", "Wed Oct 24 19:54:00 UTC 2012"}, + []string{"#0", "running", "2012-01-02 03:04:05 PM", "100.0%", "13 of 64M", "32M of 1G"}, + []string{"#1", "down", "2012-04-01 03:04:05 PM", "0%", "0 of 0", "0 of 0"}, + )) + }) + + Describe("when the package updated at is nil", func() { + BeforeEach(func() { + appSummaryRepo.GetSummarySummary.PackageUpdatedAt = nil + }) + + It("should output whatever greg sez", func() { + runCommand("my-app") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"last uploaded", "unknown"}, + )) + }) + }) + }) + + Describe("when the app is not running", func() { + BeforeEach(func() { + application := models.Application{} + application.Name = "my-app" + application.Guid = "my-app-guid" + application.State = "stopped" + application.InstanceCount = 2 + application.RunningInstances = 0 + application.Memory = 256 + now := time.Now() + application.PackageUpdatedAt = &now + + appSummaryRepo.GetSummarySummary = application + requirementsFactory.Application = application + }) + + It("displays nice output when the app is stopped", func() { + appSummaryRepo.GetSummaryErrorCode = errors.APP_STOPPED + runCommand("my-app") + + Expect(appSummaryRepo.GetSummaryAppGuid).To(Equal("my-app-guid")) + Expect(appInstancesRepo.GetInstancesArgsForCall(0)).To(Equal("my-app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Showing health and status", "my-app", "my-org", "my-space", "my-user"}, + []string{"state", "stopped"}, + []string{"instances", "0/2"}, + []string{"usage", "256M x 2 instances"}, + []string{"no running instances"}, + )) + }) + + It("displays nice output when the app has not yet finished staging", func() { + appSummaryRepo.GetSummaryErrorCode = errors.APP_NOT_STAGED + runCommand("my-app") + + Expect(appSummaryRepo.GetSummaryAppGuid).To(Equal("my-app-guid")) + Expect(appInstancesRepo.GetInstancesArgsForCall(0)).To(Equal("my-app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Showing health and status", "my-app", "my-org", "my-space", "my-user"}, + []string{"state", "stopped"}, + []string{"instances", "0/2"}, + []string{"usage", "256M x 2 instances"}, + []string{"no running instances"}, + )) + }) + }) + + Describe("when running instances is unknown", func() { + BeforeEach(func() { + app := makeAppWithRoute("my-app") + app.RunningInstances = -1 + appInstance := models.AppInstanceFields{ + State: models.InstanceRunning, + Since: testtime.MustParse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2012"), + CpuUsage: 5.0, + DiskQuota: 4 * formatters.GIGABYTE, + DiskUsage: 3 * formatters.GIGABYTE, + MemQuota: 2 * formatters.GIGABYTE, + MemUsage: 1 * formatters.GIGABYTE, + } + + appInstance2 := models.AppInstanceFields{ + State: models.InstanceRunning, + Since: testtime.MustParse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Apr 1 15:04:05 -0700 MST 2012"), + } + + instances := []models.AppInstanceFields{appInstance, appInstance2} + + appSummaryRepo.GetSummarySummary = app + appInstancesRepo.GetInstancesReturns(instances, nil) + requirementsFactory.Application = app + }) + + It("displays a '?' for running instances", func() { + runCommand("my-app") + + Expect(appSummaryRepo.GetSummaryAppGuid).To(Equal("app-guid")) + Expect(appSummaryRepo.GetSummaryAppGuid).To(Equal("app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Showing health and status", "my-app"}, + []string{"state", "started"}, + []string{"instances", "?/2"}, + []string{"usage", "256M x 2 instances"}, + []string{"urls", "my-app.example.com", "foo.example.com"}, + []string{"#0", "running", "2012-01-02 03:04:05 PM", "500.0%", "1G of 2G", "3G of 4G"}, + []string{"#1", "running", "2012-04-01 03:04:05 PM", "0%", "0 of 0", "0 of 0"}, + )) + }) + }) + + Describe("when the user passes the --guid flag", func() { + var app models.Application + BeforeEach(func() { + app = makeAppWithRoute("my-app") + + requirementsFactory.Application = app + }) + + It("displays guid for the requested app", func() { + runCommand("--guid", "my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{app.Guid}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"Showing health and status", "my-app"}, + )) + }) + }) +}) + +func makeAppWithRoute(appName string) models.Application { + application := models.Application{} + application.Name = appName + application.Guid = "app-guid" + + domain := models.DomainFields{} + domain.Name = "example.com" + + route := models.RouteSummary{Host: "foo", Domain: domain} + secondRoute := models.RouteSummary{Host: appName, Domain: domain} + packgeUpdatedAt, _ := time.Parse("2006-01-02T15:04:05Z07:00", "2012-10-24T19:54:00Z") + + application.State = "started" + application.InstanceCount = 2 + application.RunningInstances = 2 + application.Memory = 256 + application.Routes = []models.RouteSummary{route, secondRoute} + application.PackageUpdatedAt = &packgeUpdatedAt + + return application +} diff --git a/cf/commands/application/application_suite_test.go b/cf/commands/application/application_suite_test.go new file mode 100644 index 00000000000..399609debe8 --- /dev/null +++ b/cf/commands/application/application_suite_test.go @@ -0,0 +1,20 @@ +package application_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestApplication(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Application Suite") +} diff --git a/cf/commands/application/apps.go b/cf/commands/application/apps.go new file mode 100644 index 00000000000..6b579ffd024 --- /dev/null +++ b/cf/commands/application/apps.go @@ -0,0 +1,91 @@ +package application + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "strings" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/ui_helpers" + "github.com/codegangsta/cli" +) + +type ListApps struct { + ui terminal.UI + config core_config.Reader + appSummaryRepo api.AppSummaryRepository +} + +func NewListApps(ui terminal.UI, config core_config.Reader, appSummaryRepo api.AppSummaryRepository) (cmd ListApps) { + cmd.ui = ui + cmd.config = config + cmd.appSummaryRepo = appSummaryRepo + return +} + +func (cmd ListApps) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "apps", + ShortName: "a", + Description: T("List all apps in the target space"), + Usage: "CF_NAME apps", + } +} + +func (cmd ListApps) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + } + return +} + +func (cmd ListApps) Run(c *cli.Context) { + cmd.ui.Say(T("Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apps, apiErr := cmd.appSummaryRepo.GetSummariesInCurrentSpace() + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(apps) == 0 { + cmd.ui.Say(T("No apps found")) + return + } + + table := terminal.NewTable(cmd.ui, []string{T("name"), T("requested state"), T("instances"), T("memory"), T("disk"), T("urls")}) + + for _, application := range apps { + var urls []string + for _, route := range application.Routes { + urls = append(urls, route.URL()) + } + + table.Add( + application.Name, + ui_helpers.ColoredAppState(application.ApplicationFields), + ui_helpers.ColoredAppInstances(application.ApplicationFields), + formatters.ByteSize(application.Memory*formatters.MEGABYTE), + formatters.ByteSize(application.DiskQuota*formatters.MEGABYTE), + strings.Join(urls, ", "), + ) + } + + table.Print() +} diff --git a/cf/commands/application/apps_test.go b/cf/commands/application/apps_test.go new file mode 100644 index 00000000000..b306571bdbe --- /dev/null +++ b/cf/commands/application/apps_test.go @@ -0,0 +1,152 @@ +package application_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("list-apps command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + appSummaryRepo *testapi.FakeAppSummaryRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + appSummaryRepo = &testapi.FakeAppSummaryRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedSpaceSuccess: true, + } + }) + + runCommand := func(args ...string) bool { + cmd := NewListApps(ui, configRepo, appSummaryRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand()).To(BeFalse()) + }) + + It("requires the user to have a space targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + + Expect(runCommand()).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and a space is targeted", func() { + It("lists apps in a table", func() { + app1Routes := []models.RouteSummary{ + models.RouteSummary{ + Host: "app1", + Domain: models.DomainFields{ + Name: "cfapps.io", + }, + }, + models.RouteSummary{ + Host: "app1", + Domain: models.DomainFields{ + Name: "example.com", + }, + }} + + app2Routes := []models.RouteSummary{ + models.RouteSummary{ + Host: "app2", + Domain: models.DomainFields{Name: "cfapps.io"}, + }} + + app := models.Application{} + app.Name = "Application-1" + app.State = "started" + app.RunningInstances = 1 + app.InstanceCount = 1 + app.Memory = 512 + app.DiskQuota = 1024 + app.Routes = app1Routes + + app2 := models.Application{} + app2.Name = "Application-2" + app2.State = "started" + app2.RunningInstances = 1 + app2.InstanceCount = 2 + app2.Memory = 256 + app2.DiskQuota = 1024 + app2.Routes = app2Routes + + appSummaryRepo.GetSummariesInCurrentSpaceApps = []models.Application{app, app2} + + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting apps in", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Application-1", "started", "1/1", "512M", "1G", "app1.cfapps.io", "app1.example.com"}, + []string{"Application-2", "started", "1/2", "256M", "1G", "app2.cfapps.io"}, + )) + }) + + Context("when an app's running instances is unknown", func() { + It("dipslays a '?' for running instances", func() { + appRoutes := []models.RouteSummary{ + models.RouteSummary{ + Host: "app1", + Domain: models.DomainFields{Name: "cfapps.io"}, + }} + app := models.Application{} + app.Name = "Application-1" + app.State = "started" + app.RunningInstances = -1 + app.InstanceCount = 2 + app.Memory = 512 + app.DiskQuota = 1024 + app.Routes = appRoutes + + appSummaryRepo.GetSummariesInCurrentSpaceApps = []models.Application{app} + + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting apps in", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Application-1", "started", "?/2", "512M", "1G", "app1.cfapps.io"}, + )) + }) + }) + + Context("when there are no apps", func() { + It("tells the user that there are no apps", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting apps in", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"No apps found"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/application/copy_source.go b/cf/commands/application/copy_source.go new file mode 100644 index 00000000000..c5e0704b6bf --- /dev/null +++ b/cf/commands/application/copy_source.go @@ -0,0 +1,181 @@ +package application + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/api/copy_application_source" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CopySource struct { + ui terminal.UI + config core_config.Reader + authRepo authentication.AuthenticationRepository + appRepo applications.ApplicationRepository + orgRepo organizations.OrganizationRepository + spaceRepo spaces.SpaceRepository + copyAppSourceRepo copy_application_source.CopyApplicationSourceRepository + appRestart ApplicationRestarter +} + +func NewCopySource( + ui terminal.UI, + config core_config.Reader, + authRepo authentication.AuthenticationRepository, + appRepo applications.ApplicationRepository, + orgRepo organizations.OrganizationRepository, + spaceRepo spaces.SpaceRepository, + copyAppSourceRepo copy_application_source.CopyApplicationSourceRepository, + appRestart ApplicationRestarter, +) *CopySource { + + return &CopySource{ + ui: ui, + config: config, + authRepo: authRepo, + appRepo: appRepo, + orgRepo: orgRepo, + spaceRepo: spaceRepo, + copyAppSourceRepo: copyAppSourceRepo, + appRestart: appRestart, + } +} + +func (cmd *CopySource) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "copy-source", + Description: T("Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application."), + Usage: T(" CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("o", T("Org that contains the target application")), + flag_helpers.NewStringFlag("s", T("Space that contains the target application")), + cli.BoolFlag{Name: "no-restart", Usage: T("Override restart of the application in target environment after copy-source completes")}, + }, + } +} + +func (cmd *CopySource) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + } + return +} + +func (cmd *CopySource) Run(c *cli.Context) { + sourceAppName := c.Args()[0] + targetAppName := c.Args()[1] + + targetOrg := c.String("o") + targetSpace := c.String("s") + + if targetOrg != "" && targetSpace == "" { + cmd.ui.Failed(T("Please provide the space within the organization containing the target application")) + } + + _, apiErr := cmd.authRepo.RefreshAuthToken() + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + sourceApp, apiErr := cmd.appRepo.Read(sourceAppName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + var targetOrgName, targetSpaceName, spaceGuid, copyStr string + if targetOrg != "" && targetSpace != "" { + spaceGuid = cmd.findSpaceGuid(targetOrg, targetSpace) + targetOrgName = targetOrg + targetSpaceName = targetSpace + } else if targetSpace != "" { + space, err := cmd.spaceRepo.FindByName(targetSpace) + if err != nil { + cmd.ui.Failed(err.Error()) + } + spaceGuid = space.Guid + targetOrgName = cmd.config.OrganizationFields().Name + targetSpaceName = targetSpace + } else { + spaceGuid = cmd.config.SpaceFields().Guid + targetOrgName = cmd.config.OrganizationFields().Name + targetSpaceName = cmd.config.SpaceFields().Name + } + + copyStr = buildCopyString(sourceAppName, targetAppName, targetOrgName, targetSpaceName, cmd.config.Username()) + + targetApp, apiErr := cmd.appRepo.ReadFromSpace(targetAppName, spaceGuid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Say(copyStr) + cmd.ui.Say(T("Note: this may take some time")) + cmd.ui.Say("") + + apiErr = cmd.copyAppSourceRepo.CopyApplication(sourceApp.Guid, targetApp.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + if !c.Bool("no-restart") { + cmd.appRestart.ApplicationRestart(targetApp, targetOrgName, targetSpaceName) + } + + cmd.ui.Ok() +} + +func (cmd *CopySource) findSpaceGuid(targetOrg, targetSpace string) string { + org, err := cmd.orgRepo.FindByName(targetOrg) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + var space models.SpaceFields + var foundSpace bool + for _, s := range org.Spaces { + if s.Name == targetSpace { + space = s + foundSpace = true + } + } + + if !foundSpace { + cmd.ui.Failed(fmt.Sprintf(T("Could not find space {{.Space}} in organization {{.Org}}", + map[string]interface{}{ + "Space": terminal.EntityNameColor(targetSpace), + "Org": terminal.EntityNameColor(targetOrg), + }, + ))) + } + + return space.Guid +} + +func buildCopyString(sourceAppName, targetAppName, targetOrgName, targetSpaceName, username string) string { + return fmt.Sprintf(T("Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "SourceApp": terminal.EntityNameColor(sourceAppName), + "TargetApp": terminal.EntityNameColor(targetAppName), + "OrgName": terminal.EntityNameColor(targetOrgName), + "SpaceName": terminal.EntityNameColor(targetSpaceName), + "Username": terminal.EntityNameColor(username), + }, + )) + +} diff --git a/cf/commands/application/copy_source_test.go b/cf/commands/application/copy_source_test.go new file mode 100644 index 00000000000..b52cb7c4d01 --- /dev/null +++ b/cf/commands/application/copy_source_test.go @@ -0,0 +1,298 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + testCopyApplication "github.com/cloudfoundry/cli/cf/api/copy_application_source/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + testorg "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CopySource", func() { + + var ( + ui *testterm.FakeUI + config core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + authRepo *testapi.FakeAuthenticationRepository + appRepo *testApplication.FakeApplicationRepository + copyAppSourceRepo *testCopyApplication.FakeCopyApplicationSourceRepository + spaceRepo *testapi.FakeSpaceRepository + orgRepo *testorg.FakeOrganizationRepository + appRestarter *testcmd.FakeApplicationRestarter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + authRepo = &testapi.FakeAuthenticationRepository{} + appRepo = &testApplication.FakeApplicationRepository{} + copyAppSourceRepo = &testCopyApplication.FakeCopyApplicationSourceRepository{} + spaceRepo = &testapi.FakeSpaceRepository{} + orgRepo = &testorg.FakeOrganizationRepository{} + appRestarter = &testcmd.FakeApplicationRestarter{} + config = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewCopySource(ui, config, authRepo, appRepo, orgRepo, spaceRepo, copyAppSourceRepo, appRestarter) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirement failures", func() { + It("when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("source-app", "target-app")).ToNot(HavePassedRequirements()) + }) + + It("when a space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + Expect(runCommand("source-app", "target-app")).ToNot(HavePassedRequirements()) + }) + + It("when provided too many args", func() { + Expect(runCommand("source-app", "target-app", "too-much", "app-name")).ToNot(HavePassedRequirements()) + }) + }) + + Describe("Passing requirements", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + }) + + Context("refreshing the auth token", func() { + It("makes a call for the app token", func() { + runCommand("source-app", "target-app") + Expect(authRepo.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when refreshing the auth token fails", func() { + BeforeEach(func() { + authRepo.RefreshTokenError = errors.New("I accidentally the UAA") + }) + + It("it displays an error", func() { + runCommand("source-app", "target-app") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"accidentally the UAA"}, + )) + }) + }) + + Describe("when retrieving the app token succeeds", func() { + var ( + sourceApp, targetApp models.Application + ) + + BeforeEach(func() { + sourceApp = models.Application{ + ApplicationFields: models.ApplicationFields{ + Name: "source-app", + Guid: "source-app-guid", + }, + } + appRepo.ReadReturns.App = sourceApp + + targetApp = models.Application{ + ApplicationFields: models.ApplicationFields{ + Name: "target-app", + Guid: "target-app-guid", + }, + } + appRepo.ReadFromSpaceReturns(targetApp, nil) + }) + + Describe("when no parameters are passed", func() { + It("obtains both the source and target application from the same space", func() { + runCommand("source-app", "target-app") + + targetAppName, spaceGuid := appRepo.ReadFromSpaceArgsForCall(0) + Expect(targetAppName).To(Equal("target-app")) + Expect(spaceGuid).To(Equal("my-space-guid")) + + Expect(appRepo.ReadArgs.Name).To(Equal("source-app")) + + sourceAppGuid, targetAppGuid := copyAppSourceRepo.CopyApplicationArgsForCall(0) + Expect(sourceAppGuid).To(Equal("source-app-guid")) + Expect(targetAppGuid).To(Equal("target-app-guid")) + + appArg, orgName, spaceName := appRestarter.ApplicationRestartArgsForCall(0) + Expect(appArg).To(Equal(targetApp)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Copying source from app", "source-app", "to target app", "target-app", "in org my-org / space my-space as my-user..."}, + []string{"Note: this may take some time"}, + []string{"OK"}, + )) + }) + + Context("Failures", func() { + It("if we cannot obtain the source application", func() { + appRepo.ReadReturns.Error = errors.New("could not find source app") + runCommand("source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"could not find source app"}, + )) + }) + + It("fails if we cannot obtain the target application", func() { + appRepo.ReadFromSpaceReturns(models.Application{}, errors.New("could not find target app")) + runCommand("source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"could not find target app"}, + )) + }) + }) + }) + + Describe("when a space is provided, but not an org", func() { + It("send the correct target appplication for the current org and target space", func() { + spaceRepo.Spaces = []models.Space{ + { + SpaceFields: models.SpaceFields{ + Name: "space-name", + Guid: "model-space-guid", + }, + }, + } + + runCommand("-s", "space-name", "source-app", "target-app") + + targetAppName, spaceGuid := appRepo.ReadFromSpaceArgsForCall(0) + Expect(targetAppName).To(Equal("target-app")) + Expect(spaceGuid).To(Equal("model-space-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Copying source from app", "source-app", "to target app", "target-app", "in org my-org / space space-name as my-user..."}, + []string{"Note: this may take some time"}, + []string{"OK"}, + )) + }) + + Context("Failures", func() { + It("when we cannot find the provided space", func() { + spaceRepo.FindByNameErr = true + + runCommand("-s", "space-name", "source-app", "target-app") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Error finding space by name."}, + )) + }) + }) + }) + + Describe("when an org and space name are passed as parameters", func() { + It("send the correct target application for the space and org", func() { + orgRepo.FindByNameReturns(models.Organization{ + Spaces: []models.SpaceFields{ + { + Name: "space-name", + Guid: "space-guid", + }, + }, + }, nil) + + runCommand("-o", "org-name", "-s", "space-name", "source-app", "target-app") + + targetAppName, spaceGuid := appRepo.ReadFromSpaceArgsForCall(0) + Expect(targetAppName).To(Equal("target-app")) + Expect(spaceGuid).To(Equal("space-guid")) + + sourceAppGuid, targetAppGuid := copyAppSourceRepo.CopyApplicationArgsForCall(0) + Expect(sourceAppGuid).To(Equal("source-app-guid")) + Expect(targetAppGuid).To(Equal("target-app-guid")) + + appArg, orgName, spaceName := appRestarter.ApplicationRestartArgsForCall(0) + Expect(appArg).To(Equal(targetApp)) + Expect(orgName).To(Equal("org-name")) + Expect(spaceName).To(Equal("space-name")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Copying source from app source-app to target app target-app in org org-name / space space-name as my-user..."}, + []string{"Note: this may take some time"}, + []string{"OK"}, + )) + }) + + Context("failures", func() { + It("cannot just accept an organization and no space", func() { + runCommand("-o", "org-name", "source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Please provide the space within the organization containing the target application"}, + )) + }) + + It("when we cannot find the provided org", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.New("Could not find org")) + runCommand("-o", "org-name", "-s", "space-name", "source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Could not find org"}, + )) + }) + + It("when the org does not contain the space name provide", func() { + orgRepo.FindByNameReturns(models.Organization{}, nil) + runCommand("-o", "org-name", "-s", "space-name", "source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Could not find space space-name in organization org-name"}, + )) + }) + + It("when the targeted app does not exist in the targeted org and space", func() { + orgRepo.FindByNameReturns(models.Organization{ + Spaces: []models.SpaceFields{ + { + Name: "space-name", + Guid: "space-guid", + }, + }, + }, nil) + + appRepo.ReadFromSpaceReturns(models.Application{}, errors.New("could not find app")) + runCommand("-o", "org-name", "-s", "space-name", "source-app", "target-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"could not find app"}, + )) + }) + }) + }) + + Describe("when the --no-restart flag is passed", func() { + It("does not restart the target application", func() { + runCommand("--no-restart", "source-app", "target-app") + Expect(appRestarter.ApplicationRestartCallCount()).To(Equal(0)) + }) + }) + }) + }) + }) +}) diff --git a/cf/commands/application/delete.go b/cf/commands/application/delete.go new file mode 100644 index 00000000000..9f2e6d5406b --- /dev/null +++ b/cf/commands/application/delete.go @@ -0,0 +1,98 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteApp struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + routeRepo api.RouteRepository + appReq requirements.ApplicationRequirement +} + +func NewDeleteApp(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository, routeRepo api.RouteRepository) (cmd *DeleteApp) { + cmd = new(DeleteApp) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + cmd.routeRepo = routeRepo + return +} + +func (cmd *DeleteApp) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete", + ShortName: "d", + Description: T("Delete an app"), + Usage: T("CF_NAME delete APP [-f -r]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + cli.BoolFlag{Name: "r", Usage: T("Also delete any mapped routes")}, + }, + } +} + +func (cmd *DeleteApp) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func (cmd *DeleteApp) Run(c *cli.Context) { + appName := c.Args()[0] + + if !c.Bool("f") { + response := cmd.ui.ConfirmDelete(T("app"), appName) + if !response { + return + } + } + + cmd.ui.Say(T("Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(appName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + app, apiErr := cmd.appRepo.Read(appName) + + switch apiErr.(type) { + case nil: // no error + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("App {{.AppName}} does not exist.", map[string]interface{}{"AppName": appName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + } + + if c.Bool("r") { + for _, route := range app.Routes { + apiErr = cmd.routeRepo.Delete(route.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + } + } + + apiErr = cmd.appRepo.Delete(app.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/application/delete_test.go b/cf/commands/application/delete_test.go new file mode 100644 index 00000000000..0f892e8a5c4 --- /dev/null +++ b/cf/commands/application/delete_test.go @@ -0,0 +1,168 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete app command", func() { + var ( + cmd *DeleteApp + ui *testterm.FakeUI + app models.Application + configRepo core_config.ReadWriter + appRepo *testApplication.FakeApplicationRepository + routeRepo *testapi.FakeRouteRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + app = models.Application{} + app.Name = "app-to-delete" + app.Guid = "app-to-delete-guid" + + ui = &testterm.FakeUI{} + appRepo = &testApplication.FakeApplicationRepository{} + routeRepo = &testapi.FakeRouteRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + + configRepo = testconfig.NewRepositoryWithDefaults() + cmd = NewDeleteApp(ui, configRepo, appRepo, routeRepo) + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails requirements when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("-f", "delete-this-app-plz")).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails with usage when not provided exactly one arg", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("When provided an app that exists", func() { + BeforeEach(func() { + appRepo.ReadReturns.App = app + }) + + It("deletes an app when the user confirms", func() { + ui.Inputs = []string{"y"} + + runCommand("app-to-delete") + + Expect(appRepo.ReadArgs.Name).To(Equal("app-to-delete")) + Expect(appRepo.DeletedAppGuid).To(Equal("app-to-delete-guid")) + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the app app-to-delete"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "app-to-delete", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + }) + + It("does not prompt when the -f flag is provided", func() { + runCommand("-f", "app-to-delete") + + Expect(appRepo.ReadArgs.Name).To(Equal("app-to-delete")) + Expect(appRepo.DeletedAppGuid).To(Equal("app-to-delete-guid")) + Expect(ui.Prompts).To(BeEmpty()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "app-to-delete"}, + []string{"OK"}, + )) + }) + + Describe("mapped routes", func() { + BeforeEach(func() { + route1 := models.RouteSummary{} + route1.Guid = "the-first-route-guid" + route1.Host = "my-app-is-good.com" + + route2 := models.RouteSummary{} + route2.Guid = "the-second-route-guid" + route2.Host = "my-app-is-bad.com" + + appRepo.ReadReturns.App = models.Application{ + Routes: []models.RouteSummary{route1, route2}, + } + }) + + Context("when the -r flag is provided", func() { + Context("when deleting routes succeeds", func() { + It("deletes the app's routes", func() { + runCommand("-f", "-r", "app-to-delete") + + Expect(routeRepo.DeletedRouteGuids).To(ContainElement("the-first-route-guid")) + Expect(routeRepo.DeletedRouteGuids).To(ContainElement("the-second-route-guid")) + }) + }) + + Context("when deleting routes fails", func() { + BeforeEach(func() { + routeRepo.DeleteErr = errors.New("badness") + }) + + It("fails with the api error message", func() { + runCommand("-f", "-r", "app-to-delete") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "app-to-delete"}, + []string{"FAILED"}, + )) + }) + }) + }) + + Context("when the -r flag is not provided", func() { + It("does not delete mapped routes", func() { + runCommand("-f", "app-to-delete") + Expect(routeRepo.DeletedRouteGuids).To(BeEmpty()) + }) + }) + }) + }) + + Context("when the app provided is not found", func() { + BeforeEach(func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("App", "the-app") + }) + + It("warns the user when the provided app does not exist", func() { + runCommand("-f", "app-to-delete") + + Expect(appRepo.ReadArgs.Name).To(Equal("app-to-delete")) + Expect(appRepo.DeletedAppGuid).To(Equal("")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "app-to-delete"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"app-to-delete", "does not exist"})) + }) + }) + }) +}) diff --git a/cf/commands/application/env.go b/cf/commands/application/env.go new file mode 100644 index 00000000000..163e43a0e42 --- /dev/null +++ b/cf/commands/application/env.go @@ -0,0 +1,166 @@ +package application + +import ( + "encoding/json" + "sort" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Env struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository +} + +func NewEnv(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository) (cmd *Env) { + cmd = new(Env) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + return +} + +func (cmd *Env) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "env", + ShortName: "e", + Description: T("Show all env variables for an app"), + Usage: T("CF_NAME env APP"), + } +} + +func (cmd *Env) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + return []requirements.Requirement{requirementsFactory.NewLoginRequirement()}, nil +} + +func (cmd *Env) Run(c *cli.Context) { + app, err := cmd.appRepo.Read(c.Args()[0]) + if notFound, ok := err.(*errors.ModelNotFoundError); ok { + cmd.ui.Failed(notFound.Error()) + } + + cmd.ui.Say(T("Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + env, err := cmd.appRepo.ReadEnv(app.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + cmd.displaySystemiAndAppProvidedEnvironment(env.System, env.Application) + cmd.ui.Say("") + cmd.displayUserProvidedEnvironment(env.Environment) + cmd.ui.Say("") + cmd.displayRunningEnvironment(env.Running) + cmd.ui.Say("") + cmd.displayStagingEnvironment(env.Staging) + cmd.ui.Say("") +} + +func (cmd *Env) displaySystemiAndAppProvidedEnvironment(env map[string]interface{}, app map[string]interface{}) { + var vcapServices string + var vcapApplication string + + servicesAsMap, ok := env["VCAP_SERVICES"].(map[string]interface{}) + if ok && len(servicesAsMap) > 0 { + jsonBytes, err := json.MarshalIndent(env, "", " ") + if err != nil { + cmd.ui.Failed(err.Error()) + } + vcapServices = string(jsonBytes) + } + + applicationAsMap, ok := app["VCAP_APPLICATION"].(map[string]interface{}) + if ok && len(applicationAsMap) > 0 { + jsonBytes, err := json.MarshalIndent(app, "", " ") + if err != nil { + cmd.ui.Failed(err.Error()) + } + vcapApplication = string(jsonBytes) + } + + if len(vcapServices) == 0 && len(vcapApplication) == 0 { + cmd.ui.Say(T("No system-provided env variables have been set")) + return + } + + cmd.ui.Say(terminal.EntityNameColor(T("System-Provided:"))) + + cmd.ui.Say(vcapServices) + cmd.ui.Say("") + cmd.ui.Say(vcapApplication) +} + +func (cmd *Env) displayUserProvidedEnvironment(envVars map[string]interface{}) { + if len(envVars) == 0 { + cmd.ui.Say(T("No user-defined env variables have been set")) + return + } + + keys := make([]string, 0, len(envVars)) + for key, _ := range envVars { + keys = append(keys, key) + } + sort.Strings(keys) + + cmd.ui.Say(terminal.EntityNameColor(T("User-Provided:"))) + for _, key := range keys { + cmd.ui.Say("%s: %v", key, envVars[key]) + } +} + +func (cmd *Env) displayRunningEnvironment(envVars map[string]interface{}) { + if len(envVars) == 0 { + cmd.ui.Say(T("No running env variables have been set")) + return + } + + keys := make([]string, 0, len(envVars)) + for key, _ := range envVars { + keys = append(keys, key) + } + sort.Strings(keys) + + cmd.ui.Say(terminal.EntityNameColor(T("Running Environment Variable Groups:"))) + for _, key := range keys { + cmd.ui.Say("%s: %v", key, envVars[key]) + } +} + +func (cmd *Env) displayStagingEnvironment(envVars map[string]interface{}) { + if len(envVars) == 0 { + cmd.ui.Say(T("No staging env variables have been set")) + return + } + + keys := make([]string, 0, len(envVars)) + for key, _ := range envVars { + keys = append(keys, key) + } + sort.Strings(keys) + + cmd.ui.Say(terminal.EntityNameColor(T("Staging Environment Variable Groups:"))) + for _, key := range keys { + cmd.ui.Say("%s: %v", key, envVars[key]) + } +} diff --git a/cf/commands/application/env_test.go b/cf/commands/application/env_test.go new file mode 100644 index 00000000000..5b84c929f4c --- /dev/null +++ b/cf/commands/application/env_test.go @@ -0,0 +1,209 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("env command", func() { + var ( + ui *testterm.FakeUI + app models.Application + appRepo *testApplication.FakeApplicationRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + + app = models.Application{} + app.Name = "my-app" + appRepo = &testApplication.FakeApplicationRepository{} + appRepo.ReadReturns.App = app + + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewEnv(ui, configRepo, appRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("Requirements", func() { + It("fails when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("my-app")).To(BeFalse()) + }) + }) + + It("fails with usage when no app name is given", func() { + passed := runCommand() + + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + + It("fails with usage when the app cannot be found", func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("app", "hocus-pocus") + runCommand("hocus-pocus") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"not found"}, + )) + }) + + Context("when the app has at least one env var", func() { + BeforeEach(func() { + app = models.Application{} + app.Name = "my-app" + app.Guid = "the-app-guid" + + appRepo.ReadReturns.App = app + appRepo.ReadEnvReturns(&models.Environment{ + Environment: map[string]interface{}{ + "my-key": "my-value", + "my-key2": "my-value2", + "first-key": 0, + "first-bool": false, + }, + System: map[string]interface{}{ + "VCAP_SERVICES": map[string]interface{}{ + "pump-yer-brakes": "drive-slow", + }, + }, + Application: map[string]interface{}{ + "VCAP_APPLICATION": map[string]interface{}{ + "dis-be-an-app-field": "wit-an-app-value", + "app-key-1": 0, + "app-key-2": false, + }, + }, + }, nil) + }) + + It("lists those environment variables, in sorted order for provided services", func() { + runCommand("my-app") + Expect(appRepo.ReadEnvArgsForCall(0)).To(Equal("the-app-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting env variables for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"System-Provided:"}, + []string{"VCAP_SERVICES", ":", "{"}, + []string{"pump-yer-brakes", ":", "drive-slow"}, + []string{"}"}, + []string{"User-Provided:"}, + []string{"first-bool", "false"}, + []string{"first-key", "0"}, + []string{"my-key", "my-value"}, + []string{"my-key2", "my-value2"}, + )) + }) + It("displays the application env info under the System env column", func() { + runCommand("my-app") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting env variables for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"System-Provided:"}, + []string{"VCAP_SERVICES", ":", "{"}, + []string{"pump-yer-brakes", ":", "drive-slow"}, + []string{"}"}, + []string{"VCAP_APPLICATION", ":", "{"}, + []string{"dis-be-an-app-field", ":", "wit-an-app-value"}, + []string{"app-key-1", ":", "0"}, + []string{"app-key-2", ":", "false"}, + []string{"}"}, + )) + }) + }) + + Context("when the app has no user-defined environment variables", func() { + It("shows an empty message", func() { + appRepo.ReadEnvReturns(&models.Environment{}, nil) + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting env variables for app", "my-app"}, + []string{"OK"}, + []string{"No", "system-provided", "env variables", "have been set"}, + []string{"No", "env variables", "have been set"}, + )) + }) + }) + + Context("when the app has no environment variables", func() { + It("informs the user that each group is empty", func() { + appRepo.ReadEnvReturns(&models.Environment{}, nil) + + runCommand("my-app") + Expect(ui.Outputs).To(ContainSubstrings([]string{"No system-provided env variables have been set"})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"No user-defined env variables have been set"})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"No running env variables have been set"})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"No staging env variables have been set"})) + }) + }) + + Context("when the app has at least one running and staging environment variable", func() { + BeforeEach(func() { + app = models.Application{} + app.Name = "my-app" + app.Guid = "the-app-guid" + + appRepo.ReadReturns.App = app + appRepo.ReadEnvReturns(&models.Environment{ + Running: map[string]interface{}{ + "running-key-1": "running-value-1", + "running-key-2": "running-value-2", + "running": true, + "number": 37, + }, + Staging: map[string]interface{}{ + "staging-key-1": "staging-value-1", + "staging-key-2": "staging-value-2", + "staging": false, + "number": 42, + }, + }, nil) + }) + + It("lists the environment variables", func() { + runCommand("my-app") + Expect(appRepo.ReadEnvArgsForCall(0)).To(Equal("the-app-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting env variables for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Running Environment Variable Groups:"}, + []string{"running-key-1", ":", "running-value-1"}, + []string{"running-key-2", ":", "running-value-2"}, + []string{"running", ":", "true"}, + []string{"number", ":", "37"}, + []string{"Staging Environment Variable Groups:"}, + []string{"staging-key-1", ":", "staging-value-1"}, + []string{"staging-key-2", ":", "staging-value-2"}, + []string{"staging", ":", "false"}, + []string{"number", ":", "42"}, + )) + }) + }) + + Context("when reading the environment variables returns an error", func() { + It("tells you about that error", func() { + appRepo.ReadEnvReturns(nil, errors.New("BOO YOU CANT DO THAT; GO HOME; you're drunk")) + runCommand("whatever") + Expect(ui.Outputs).To(ContainSubstrings([]string{"you're drunk"})) + }) + }) +}) diff --git a/cf/commands/application/events.go b/cf/commands/application/events.go new file mode 100644 index 00000000000..c5e8b670b1b --- /dev/null +++ b/cf/commands/application/events.go @@ -0,0 +1,86 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api/app_events" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Events struct { + ui terminal.UI + config core_config.Reader + appReq requirements.ApplicationRequirement + eventsRepo app_events.AppEventsRepository +} + +func NewEvents(ui terminal.UI, config core_config.Reader, eventsRepo app_events.AppEventsRepository) (cmd *Events) { + cmd = new(Events) + cmd.ui = ui + cmd.config = config + cmd.eventsRepo = eventsRepo + return +} + +func (cmd *Events) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "events", + Description: T("Show recent app events"), + Usage: T("CF_NAME events APP"), + } +} + +func (cmd *Events) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *Events) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + + cmd.ui.Say(T("Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + table := cmd.ui.Table([]string{T("time"), T("event"), T("actor"), T("description")}) + + events, apiErr := cmd.eventsRepo.RecentEvents(app.Guid, 50) + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching events.\n{{.ApiErr}}", + map[string]interface{}{"ApiErr": apiErr.Error()})) + return + } + + for _, event := range events { + table.Add( + event.Timestamp.Local().Format("2006-01-02T15:04:05.00-0700"), + event.Name, + event.ActorName, + event.Description, + ) + } + + table.Print() + + if len(events) == 0 { + cmd.ui.Say(T("No events for app {{.AppName}}", + map[string]interface{}{"AppName": terminal.EntityNameColor(app.Name)})) + return + } +} diff --git a/cf/commands/application/events_test.go b/cf/commands/application/events_test.go new file mode 100644 index 00000000000..ebf91bac8fa --- /dev/null +++ b/cf/commands/application/events_test.go @@ -0,0 +1,118 @@ +package application_test + +import ( + "time" + + testapi "github.com/cloudfoundry/cli/cf/api/app_events/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("events command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + eventsRepo *testapi.FakeAppEventsRepository + ui *testterm.FakeUI + ) + + const TIMESTAMP_FORMAT = "2006-01-02T15:04:05.00-0700" + + BeforeEach(func() { + eventsRepo = new(testapi.FakeAppEventsRepository) + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + ui = new(testterm.FakeUI) + }) + + runCommand := func(args ...string) bool { + configRepo := testconfig.NewRepositoryWithDefaults() + cmd := NewEvents(ui, configRepo, eventsRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails with usage when called without an app name", func() { + passed := runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + + It("lists events given an app name", func() { + earlierTimestamp, err := time.Parse(TIMESTAMP_FORMAT, "1999-12-31T23:59:11.00-0000") + Expect(err).NotTo(HaveOccurred()) + + timestamp, err := time.Parse(TIMESTAMP_FORMAT, "2000-01-01T00:01:11.00-0000") + Expect(err).NotTo(HaveOccurred()) + + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + requirementsFactory.Application = app + + eventsRepo.RecentEventsReturns([]models.EventFields{ + { + Guid: "event-guid-1", + Name: "app crashed", + Timestamp: earlierTimestamp, + Description: "reason: app instance exited, exit_status: 78", + ActorName: "George Clooney", + }, + { + Guid: "event-guid-2", + Name: "app crashed", + Timestamp: timestamp, + Description: "reason: app instance was stopped, exit_status: 77", + ActorName: "Marcel Marceau", + }, + }, nil) + + runCommand("my-app") + + Expect(eventsRepo.RecentEventsCallCount()).To(Equal(1)) + appGuid, limit := eventsRepo.RecentEventsArgsForCall(0) + Expect(limit).To(Equal(int64(50))) + Expect(appGuid).To(Equal("my-app-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting events for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"time", "event", "actor", "description"}, + []string{earlierTimestamp.Local().Format(TIMESTAMP_FORMAT), "app crashed", "George Clooney", "app instance exited", "78"}, + []string{timestamp.Local().Format(TIMESTAMP_FORMAT), "app crashed", "Marcel Marceau", "app instance was stopped", "77"}, + )) + }) + + It("tells the user when an error occurs", func() { + eventsRepo.RecentEventsReturns(nil, errors.New("welp")) + + app := models.Application{} + app.Name = "my-app" + requirementsFactory.Application = app + + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"events", "my-app"}, + []string{"FAILED"}, + []string{"welp"}, + )) + }) + + It("tells the user when no events exist for that app", func() { + app := models.Application{} + app.Name = "my-app" + requirementsFactory.Application = app + + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"events", "my-app"}, + []string{"No events", "my-app"}, + )) + }) +}) diff --git a/cf/commands/application/files.go b/cf/commands/application/files.go new file mode 100644 index 00000000000..8f1f70d8821 --- /dev/null +++ b/cf/commands/application/files.go @@ -0,0 +1,98 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api/app_files" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Files struct { + ui terminal.UI + config core_config.Reader + appFilesRepo app_files.AppFilesRepository + appReq requirements.ApplicationRequirement +} + +func NewFiles(ui terminal.UI, config core_config.Reader, appFilesRepo app_files.AppFilesRepository) (cmd *Files) { + cmd = new(Files) + cmd.ui = ui + cmd.config = config + cmd.appFilesRepo = appFilesRepo + return +} + +func (cmd *Files) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "files", + ShortName: "f", + Description: T("Print out a list of files in a directory or the contents of a specific file"), + Usage: T("CF_NAME files APP [-i INSTANCE] [PATH]"), + Flags: []cli.Flag{ + flag_helpers.NewIntFlag("i", T("Instance")), + }, + } +} + +func (cmd *Files) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) < 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *Files) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + + var instance int + if c.IsSet("i") { + instance = c.Int("i") + if instance < 0 { + cmd.ui.Failed(T("Invalid instance: {{.Instance}}\nInstance must be a positive integer", + map[string]interface{}{ + "Instance": instance, + })) + } + if instance >= app.InstanceCount { + cmd.ui.Failed(T("Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + map[string]interface{}{ + "Instance": instance, + "InstanceCount": app.InstanceCount, + })) + } + } + + cmd.ui.Say(T("Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + path := "/" + if len(c.Args()) > 1 { + path = c.Args()[1] + } + + list, apiErr := cmd.appFilesRepo.ListFiles(app.Guid, instance, path) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + cmd.ui.Say("%s", list) +} diff --git a/cf/commands/application/files_test.go b/cf/commands/application/files_test.go new file mode 100644 index 00000000000..366e9fbab92 --- /dev/null +++ b/cf/commands/application/files_test.go @@ -0,0 +1,116 @@ +package application_test + +import ( + testappfiles "github.com/cloudfoundry/cli/cf/api/app_files/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("files command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + appFilesRepo *testappfiles.FakeAppFilesRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + appFilesRepo = &testappfiles.FakeAppFilesRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewFiles(ui, configRepo, appFilesRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedSpaceSuccess = true + runCommand("my-app", "/foo") + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("my-app", "/foo")).To(BeFalse()) + }) + + It("fails with usage when not provided an app name", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + + passed := runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + }) + + Context("when logged in, a space is targeted and a valid app name is provided", func() { + BeforeEach(func() { + app := models.Application{} + app.Name = "my-found-app" + app.Guid = "my-app-guid" + + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + appFilesRepo.ListFilesReturns("file 1\nfile 2", nil) + }) + + It("it lists files in a directory", func() { + runCommand("my-app", "/foo") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting files for app", "my-found-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"file 1"}, + []string{"file 2"}, + )) + + guid, _, path := appFilesRepo.ListFilesArgsForCall(0) + Expect(guid).To(Equal("my-app-guid")) + Expect(path).To(Equal("/foo")) + }) + + It("does not interpolate or interpret special format characters as though it should be a format string", func() { + appFilesRepo.ListFilesReturns("%s %d %i", nil) + runCommand("my-app", "/foo") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"%s %d %i"})) + }) + + Context("checking for bad flags", func() { + It("fails when non-positive value is given for instance", func() { + runCommand("-i", "-1", "my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Invalid instance"}, + []string{"Instance must be a positive integer"}, + )) + }) + + It("fails when instance is larger than instance count", func() { + runCommand("-i", "5", "my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Invalid instance"}, + []string{"Instance must be less than"}, + )) + }) + + }) + + }) +}) diff --git a/cf/commands/application/logs.go b/cf/commands/application/logs.go new file mode 100644 index 00000000000..14f3c9937c3 --- /dev/null +++ b/cf/commands/application/logs.go @@ -0,0 +1,122 @@ +package application + +import ( + "fmt" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/ui_helpers" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/codegangsta/cli" + "time" +) + +type Logs struct { + ui terminal.UI + config core_config.Reader + logsRepo api.LogsRepository + appReq requirements.ApplicationRequirement +} + +func NewLogs(ui terminal.UI, config core_config.Reader, logsRepo api.LogsRepository) (cmd *Logs) { + cmd = new(Logs) + cmd.ui = ui + cmd.config = config + cmd.logsRepo = logsRepo + return +} + +func (cmd *Logs) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "logs", + Description: T("Tail or show recent logs for an app"), + Usage: T("CF_NAME logs APP"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "recent", Usage: T("Dump recent logs instead of tailing")}, + }, + } +} + +func (cmd *Logs) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.appReq, + } + + return +} + +func (cmd *Logs) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + + if c.Bool("recent") { + cmd.recentLogsFor(app) + } else { + cmd.tailLogsFor(app) + } +} + +func (cmd *Logs) recentLogsFor(app models.Application) { + cmd.ui.Say(T("Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + messages, err := cmd.logsRepo.RecentLogsFor(app.Guid) + if err != nil { + cmd.handleError(err) + } + + for _, msg := range messages { + cmd.ui.Say("%s", LogMessageOutput(msg, time.Local)) + } +} + +func (cmd *Logs) tailLogsFor(app models.Application) { + onConnect := func() { + cmd.ui.Say(T("Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } + + err := cmd.logsRepo.TailLogsFor(app.Guid, onConnect, func(msg *logmessage.LogMessage) { + cmd.ui.Say("%s", LogMessageOutput(msg, time.Local)) + }) + + if err != nil { + cmd.handleError(err) + } +} + +func (cmd *Logs) handleError(err error) { + switch err.(type) { + case nil: + case *errors.InvalidSSLCert: + cmd.ui.Failed(err.Error() + T("\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error")) + default: + cmd.ui.Failed(err.Error()) + } +} + +func LogMessageOutput(msg *logmessage.LogMessage, loc *time.Location) string { + logHeader, coloredLogHeader := ui_helpers.ExtractLogHeader(msg, loc) + logContent := ui_helpers.ExtractLogContent(msg, logHeader) + + return fmt.Sprintf("%s%s", coloredLogHeader, logContent) +} diff --git a/cf/commands/application/logs_test.go b/cf/commands/application/logs_test.go new file mode 100644 index 00000000000..8f242d3f9dd --- /dev/null +++ b/cf/commands/application/logs_test.go @@ -0,0 +1,203 @@ +package application_test + +import ( + "time" + + "code.google.com/p/gogoprotobuf/proto" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testlogs "github.com/cloudfoundry/cli/testhelpers/logs" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + "github.com/cloudfoundry/loggregatorlib/logmessage" + + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("logs command", func() { + var ( + ui *testterm.FakeUI + logsRepo *testapi.FakeLogsRepository + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + logsRepo = &testapi.FakeLogsRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewLogs(ui, configRepo, logsRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when called without one argument", func() { + requirementsFactory.LoginSuccess = true + + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when not logged in", func() { + Expect(runCommand("my-app")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + var ( + app models.Application + ) + + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + app = models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + + currentTime := time.Now() + recentLogs := []*logmessage.LogMessage{ + testlogs.NewLogMessage("Log Line 1", app.Guid, "DEA", currentTime), + testlogs.NewLogMessage("Log Line 2", app.Guid, "DEA", currentTime), + } + + appLogs := []*logmessage.LogMessage{ + testlogs.NewLogMessage("Log Line 1", app.Guid, "DEA", time.Now()), + } + + requirementsFactory.Application = app + logsRepo.RecentLogsForReturns(recentLogs, nil) + + logsRepo.TailLogsForStub = func(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error { + onConnect() + for _, log := range appLogs { + onMessage(log) + } + return nil + } + }) + + It("shows the recent logs when the --recent flag is provided", func() { + runCommand("--recent", "my-app") + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(app.Guid).To(Equal(logsRepo.RecentLogsForArgsForCall(0))) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Connected, dumping recent logs for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"Log Line 1"}, + []string{"Log Line 2"}, + )) + }) + + Context("when the log messages contain format string identifiers", func() { + BeforeEach(func() { + logsRepo.RecentLogsForReturns([]*logmessage.LogMessage{ + testlogs.NewLogMessage("hello%2Bworld%v", app.Guid, "DEA", time.Now()), + }, nil) + }) + + It("does not treat them as format strings", func() { + runCommand("--recent", "my-app") + Expect(ui.Outputs).To(ContainSubstrings([]string{"hello%2Bworld%v"})) + }) + }) + + It("tails the app's logs when no flags are given", func() { + runCommand("my-app") + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + appGuid, _, _ := logsRepo.TailLogsForArgsForCall(0) + Expect(app.Guid).To(Equal(appGuid)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Connected, tailing logs for app", "my-app", "my-org", "my-space", "my-user"}, + []string{"Log Line 1"}, + )) + }) + + Context("when the loggregator server has an invalid cert", func() { + Context("when the skip-ssl-validation flag is not set", func() { + It("fails and informs the user about the skip-ssl-validation flag", func() { + logsRepo.TailLogsForReturns(errors.NewInvalidSSLCert("https://example.com", "it don't work good")) + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Received invalid SSL certificate", "https://example.com"}, + []string{"TIP"}, + )) + }) + + It("informs the user of the error when they include the --recent flag", func() { + logsRepo.RecentLogsForReturns(nil, errors.NewInvalidSSLCert("https://example.com", "how does SSL work???")) + runCommand("--recent", "my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Received invalid SSL certificate", "https://example.com"}, + []string{"TIP"}, + )) + }) + }) + }) + + Context("when the loggregator server has a valid cert", func() { + It("tails logs", func() { + runCommand("my-app") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Connected, tailing logs for app", "my-org", "my-space", "my-user"}, + )) + }) + }) + + Describe("Helpers", func() { + date := time.Date(2014, 4, 4, 11, 39, 20, 5, time.UTC) + + createMessage := func(sourceId string, sourceName string, msgType logmessage.LogMessage_MessageType, date time.Time) *logmessage.LogMessage { + timestamp := date.UnixNano() + return &logmessage.LogMessage{ + Message: []byte("Hello World!\n\r\n\r"), + AppId: proto.String("my-app-guid"), + MessageType: &msgType, + SourceId: &sourceId, + Timestamp: ×tamp, + SourceName: &sourceName, + } + } + + Context("when the message comes from an app", func() { + It("includes the instance index", func() { + msg := createMessage("4", "App", logmessage.LogMessage_OUT, date) + Expect(terminal.Decolorize(LogMessageOutput(msg, time.UTC))).To(Equal("2014-04-04T11:39:20.00+0000 [App/4] OUT Hello World!")) + }) + }) + + Context("when the message comes from a cloudfoundry component", func() { + It("doesn't include the instance index", func() { + msg := createMessage("4", "DEA", logmessage.LogMessage_OUT, date) + Expect(terminal.Decolorize(LogMessageOutput(msg, time.UTC))).To(Equal("2014-04-04T11:39:20.00+0000 [DEA] OUT Hello World!")) + }) + }) + + Context("when the message was written to stderr", func() { + It("shows the log type as 'ERR'", func() { + msg := createMessage("4", "DEA", logmessage.LogMessage_ERR, date) + Expect(terminal.Decolorize(LogMessageOutput(msg, time.UTC))).To(Equal("2014-04-04T11:39:20.00+0000 [DEA] ERR Hello World!")) + }) + }) + + It("formats the time in the given time zone", func() { + msg := createMessage("4", "DEA", logmessage.LogMessage_ERR, date) + Expect(terminal.Decolorize(LogMessageOutput(msg, time.FixedZone("the-zone", 3*60*60)))).To(Equal("2014-04-04T14:39:20.00+0300 [DEA] ERR Hello World!")) + }) + }) + }) +}) diff --git a/cf/commands/application/push.go b/cf/commands/application/push.go new file mode 100644 index 00000000000..9349e71a9c8 --- /dev/null +++ b/cf/commands/application/push.go @@ -0,0 +1,614 @@ +package application + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/fileutils" + + "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/api/stacks" + "github.com/cloudfoundry/cli/cf/app_files" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/words/generator" + "github.com/codegangsta/cli" +) + +type Push struct { + ui terminal.UI + config core_config.Reader + manifestRepo manifest.ManifestRepository + appStarter ApplicationStarter + appStopper ApplicationStopper + serviceBinder service.ServiceBinder + appRepo applications.ApplicationRepository + domainRepo api.DomainRepository + routeRepo api.RouteRepository + serviceRepo api.ServiceRepository + stackRepo stacks.StackRepository + authRepo authentication.AuthenticationRepository + wordGenerator generator.WordGenerator + actor actors.PushActor + zipper app_files.Zipper + app_files app_files.AppFiles +} + +func NewPush(ui terminal.UI, config core_config.Reader, manifestRepo manifest.ManifestRepository, + starter ApplicationStarter, stopper ApplicationStopper, binder service.ServiceBinder, + appRepo applications.ApplicationRepository, domainRepo api.DomainRepository, routeRepo api.RouteRepository, + stackRepo stacks.StackRepository, serviceRepo api.ServiceRepository, + authRepo authentication.AuthenticationRepository, wordGenerator generator.WordGenerator, + actor actors.PushActor, zipper app_files.Zipper, app_files app_files.AppFiles) *Push { + return &Push{ + ui: ui, + config: config, + manifestRepo: manifestRepo, + appStarter: starter, + appStopper: stopper, + serviceBinder: binder, + appRepo: appRepo, + domainRepo: domainRepo, + routeRepo: routeRepo, + serviceRepo: serviceRepo, + stackRepo: stackRepo, + authRepo: authRepo, + wordGenerator: wordGenerator, + actor: actor, + zipper: zipper, + app_files: app_files, + } +} + +func (cmd *Push) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "push", + ShortName: "p", + Description: T("Push a new app or sync changes to an existing app"), + Usage: T("Push a single app (with or without a manifest):\n") + T(" CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n") + T(" [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n") + + " [--no-hostname] [--no-manifest] [--no-route] [--no-start]\n" + + "\n" + T(" Push multiple apps with a manifest:\n") + T(" CF_NAME push [-f MANIFEST_PATH]\n"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("b", T("Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)")), + flag_helpers.NewStringFlag("c", T("Startup command, set to null to reset to default start command")), + flag_helpers.NewStringFlag("d", T("Domain (e.g. example.com)")), + flag_helpers.NewStringFlag("f", T("Path to manifest")), + flag_helpers.NewIntFlag("i", T("Number of instances")), + flag_helpers.NewStringFlag("k", T("Disk limit (e.g. 256M, 1024M, 1G)")), + flag_helpers.NewStringFlag("m", T("Memory limit (e.g. 256M, 1024M, 1G)")), + flag_helpers.NewStringFlag("n", T("Hostname (e.g. my-subdomain)")), + flag_helpers.NewStringFlag("p", T("Path to app directory or file")), + flag_helpers.NewStringFlag("s", T("Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)")), + flag_helpers.NewStringFlag("t", T("Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply")), + cli.BoolFlag{Name: "no-hostname", Usage: T("Map the root domain to this app")}, + cli.BoolFlag{Name: "no-manifest", Usage: T("Ignore manifest file")}, + cli.BoolFlag{Name: "no-route", Usage: T("Do not map a route to this app and remove routes from previous pushes of this app.")}, + cli.BoolFlag{Name: "no-start", Usage: T("Do not start an app after pushing")}, + cli.BoolFlag{Name: "random-route", Usage: T("Create a random route for this app")}, + }, + } +} + +func (cmd *Push) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) > 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + } + return +} + +func (cmd *Push) Run(c *cli.Context) { + appSet := cmd.findAndValidateAppsToPush(c) + _, apiErr := cmd.authRepo.RefreshAuthToken() + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + routeActor := actors.NewRouteActor(cmd.ui, cmd.routeRepo) + noHostname := c.Bool("no-hostname") + + for _, appParams := range appSet { + cmd.fetchStackGuid(&appParams) + app := cmd.createOrUpdateApp(appParams) + + cmd.updateRoutes(routeActor, app, appParams, noHostname) + + cmd.ui.Say(T("Uploading {{.AppName}}...", + map[string]interface{}{"AppName": terminal.EntityNameColor(app.Name)})) + + apiErr := cmd.uploadApp(app.Guid, *appParams.Path) + if apiErr != nil { + cmd.ui.Failed(fmt.Sprintf(T("Error uploading application.\n{{.ApiErr}}", + map[string]interface{}{"ApiErr": apiErr.Error()}))) + return + } + cmd.ui.Ok() + + if appParams.ServicesToBind != nil { + cmd.bindAppToServices(*appParams.ServicesToBind, app) + } + + cmd.restart(app, appParams, c) + } +} + +func (cmd *Push) updateRoutes(routeActor actors.RouteActor, app models.Application, appParams models.AppParams, noHostName bool) { + defaultRouteAcceptable := len(app.Routes) == 0 + routeDefined := appParams.Domain != nil || appParams.Host != nil || noHostName + + domain := cmd.findDomain(appParams.Domain) + hostname := cmd.hostnameForApp(appParams.Host, appParams.UseRandomHostname, app.Name, noHostName) + + if appParams.NoRoute { + cmd.removeRoutes(app, routeActor) + } else if routeDefined || defaultRouteAcceptable { + route := routeActor.FindOrCreateRoute(hostname, domain) + routeActor.BindRoute(app, route) + } +} + +func (cmd *Push) removeRoutes(app models.Application, routeActor actors.RouteActor) { + if len(app.Routes) == 0 { + cmd.ui.Say(T("App {{.AppName}} is a worker, skipping route creation", + map[string]interface{}{"AppName": terminal.EntityNameColor(app.Name)})) + } else { + routeActor.UnbindAll(app) + } +} + +func (cmd *Push) hostnameForApp(host *string, useRandomHostName bool, name string, noHostName bool) string { + if noHostName { + return "" + } + + if host != nil { + return *host + } else if useRandomHostName { + return hostNameForString(name) + "-" + cmd.wordGenerator.Babble() + } else { + return hostNameForString(name) + } +} + +var forbiddenHostCharRegex = regexp.MustCompile("[^a-z0-9-]") +var whitespaceRegex = regexp.MustCompile(`[\s_]+`) + +func hostNameForString(name string) string { + name = strings.ToLower(name) + name = whitespaceRegex.ReplaceAllString(name, "-") + name = forbiddenHostCharRegex.ReplaceAllString(name, "") + return name +} + +func (cmd *Push) findDomain(domainName *string) (domain models.DomainFields) { + domain, error := cmd.domainRepo.FirstOrDefault(cmd.config.OrganizationFields().Guid, domainName) + if error != nil { + cmd.ui.Failed(error.Error()) + } + + return +} + +func (cmd *Push) bindAppToServices(services []string, app models.Application) { + for _, serviceName := range services { + serviceInstance, err := cmd.serviceRepo.FindInstanceByName(serviceName) + + if err != nil { + cmd.ui.Failed(T("Could not find service {{.ServiceName}} to bind to {{.AppName}}", + map[string]interface{}{"ServiceName": serviceName, "AppName": app.Name})) + return + } + + cmd.ui.Say(T("Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceInstance.Name), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + err = cmd.serviceBinder.BindApplication(app, serviceInstance) + + switch httpErr := err.(type) { + case errors.HttpError: + if httpErr.ErrorCode() == errors.APP_ALREADY_BOUND { + err = nil + } + } + + if err != nil { + cmd.ui.Failed(T("Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + map[string]interface{}{"ServiceName": serviceName, "Err": err.Error()})) + } + + cmd.ui.Ok() + } +} + +func (cmd *Push) fetchStackGuid(appParams *models.AppParams) { + if appParams.StackName == nil { + return + } + + stackName := *appParams.StackName + cmd.ui.Say(T("Using stack {{.StackName}}...", + map[string]interface{}{"StackName": terminal.EntityNameColor(stackName)})) + + stack, apiErr := cmd.stackRepo.FindByName(stackName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + appParams.StackGuid = &stack.Guid +} + +func (cmd *Push) restart(app models.Application, params models.AppParams, c *cli.Context) { + if app.State != T("stopped") { + cmd.ui.Say("") + app, _ = cmd.appStopper.ApplicationStop(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) + } + + cmd.ui.Say("") + + if c.Bool("no-start") { + return + } + + if params.HealthCheckTimeout != nil { + cmd.appStarter.SetStartTimeoutInSeconds(*params.HealthCheckTimeout) + } + + cmd.appStarter.ApplicationStart(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) +} + +func (cmd *Push) createOrUpdateApp(appParams models.AppParams) (app models.Application) { + if appParams.Name == nil { + cmd.ui.Failed(T("Error: No name found for app")) + } + + app, apiErr := cmd.appRepo.Read(*appParams.Name) + + switch apiErr.(type) { + case nil: + app = cmd.updateApp(app, appParams) + case *errors.ModelNotFoundError: + app = cmd.createApp(appParams) + default: + cmd.ui.Failed(apiErr.Error()) + } + + return +} + +func (cmd *Push) createApp(appParams models.AppParams) (app models.Application) { + spaceGuid := cmd.config.SpaceFields().Guid + appParams.SpaceGuid = &spaceGuid + + cmd.ui.Say(T("Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(*appParams.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + app, apiErr := cmd.appRepo.Create(appParams) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + return +} + +func (cmd *Push) updateApp(app models.Application, appParams models.AppParams) (updatedApp models.Application) { + cmd.ui.Say(T("Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + if appParams.EnvironmentVars != nil { + for key, val := range app.EnvironmentVars { + if _, ok := (*appParams.EnvironmentVars)[key]; !ok { + (*appParams.EnvironmentVars)[key] = val + } + } + } + + var apiErr error + updatedApp, apiErr = cmd.appRepo.Update(app.Guid, appParams) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + return +} + +func (cmd *Push) findAndValidateAppsToPush(c *cli.Context) []models.AppParams { + appsFromManifest := cmd.getAppParamsFromManifest(c) + appFromContext := cmd.getAppParamsFromContext(c) + return cmd.createAppSetFromContextAndManifest(appFromContext, appsFromManifest) +} + +func (cmd *Push) getAppParamsFromManifest(c *cli.Context) []models.AppParams { + if c.Bool("no-manifest") { + return []models.AppParams{} + } + + var path string + if c.String("f") != "" { + path = c.String("f") + } else { + var err error + path, err = os.Getwd() + if err != nil { + cmd.ui.Failed(T("Could not determine the current working directory!"), err) + } + } + + m, err := cmd.manifestRepo.ReadManifest(path) + + if err != nil { + if m.Path == "" && c.String("f") == "" { + return []models.AppParams{} + } else { + cmd.ui.Failed(T("Error reading manifest file:\n{{.Err}}", map[string]interface{}{"Err": err.Error()})) + } + } + + apps, err := m.Applications() + if err != nil { + cmd.ui.Failed("Error reading manifest file:\n%s", err) + } + + cmd.ui.Say(T("Using manifest file {{.Path}}\n", + map[string]interface{}{"Path": terminal.EntityNameColor(m.Path)})) + return apps +} + +func (cmd *Push) createAppSetFromContextAndManifest(contextApp models.AppParams, manifestApps []models.AppParams) (apps []models.AppParams) { + var err error + + switch len(manifestApps) { + case 0: + err = addApp(&apps, contextApp) + case 1: + manifestApps[0].Merge(&contextApp) + err = addApp(&apps, manifestApps[0]) + default: + selectedAppName := contextApp.Name + contextApp.Name = nil + + if !contextApp.IsEmpty() { + cmd.ui.Failed("%s", T("Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.")) + } + + if selectedAppName != nil { + var manifestApp models.AppParams + manifestApp, err = findAppWithNameInManifest(*selectedAppName, manifestApps) + if err == nil { + addApp(&apps, manifestApp) + } + } else { + for _, manifestApp := range manifestApps { + addApp(&apps, manifestApp) + } + } + } + + if err != nil { + cmd.ui.Failed(T("Error: {{.Err}}", map[string]interface{}{"Err": err.Error()})) + } + + return +} + +func addApp(apps *[]models.AppParams, app models.AppParams) (err error) { + if app.Name == nil { + err = errors.New(T("App name is a required field")) + } + if app.Path == nil { + cwd, _ := os.Getwd() + app.Path = &cwd + } + *apps = append(*apps, app) + return +} + +func findAppWithNameInManifest(name string, manifestApps []models.AppParams) (app models.AppParams, err error) { + for _, appParams := range manifestApps { + if appParams.Name != nil && *appParams.Name == name { + app = appParams + return + } + } + + err = errors.New(T("Could not find app named '{{.AppName}}' in manifest", + map[string]interface{}{"AppName": name})) + return +} + +func (cmd *Push) getAppParamsFromContext(c *cli.Context) (appParams models.AppParams) { + if len(c.Args()) > 0 { + appParams.Name = &c.Args()[0] + } + + appParams.NoRoute = c.Bool("no-route") + appParams.UseRandomHostname = c.Bool("random-route") + + if c.String("n") != "" { + hostname := c.String("n") + appParams.Host = &hostname + } + + if c.String("b") != "" { + buildpack := c.String("b") + if buildpack == "null" || buildpack == "default" { + buildpack = "" + } + appParams.BuildpackUrl = &buildpack + } + + if c.String("c") != "" { + command := c.String("c") + if command == "null" || command == "default" { + command = "" + } + appParams.Command = &command + } + + if c.String("d") != "" { + domain := c.String("d") + appParams.Domain = &domain + } + + if c.IsSet("i") { + instances := c.Int("i") + if instances < 1 { + cmd.ui.Failed(T("Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + map[string]interface{}{"InstancesCount": instances})) + } + appParams.InstanceCount = &instances + } + + if c.String("k") != "" { + diskQuota, err := formatters.ToMegabytes(c.String("k")) + if err != nil { + cmd.ui.Failed(T("Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + map[string]interface{}{"DiskQuota": c.String("k"), "Err": err.Error()})) + } + appParams.DiskQuota = &diskQuota + } + + if c.String("m") != "" { + memory, err := formatters.ToMegabytes(c.String("m")) + if err != nil { + cmd.ui.Failed(T("Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + map[string]interface{}{"MemLimit": c.String("m"), "Err": err.Error()})) + } + appParams.Memory = &memory + } + + if c.String("p") != "" { + path := c.String("p") + appParams.Path = &path + } + + if c.String("s") != "" { + stackName := c.String("s") + appParams.StackName = &stackName + } + + if c.String("t") != "" { + timeout, err := strconv.Atoi(c.String("t")) + if err != nil { + cmd.ui.Failed("Error: %s", errors.NewWithFmt(T("Invalid timeout param: {{.Timeout}}\n{{.Err}}", + map[string]interface{}{"Timeout": c.String("t"), "Err": err.Error()}))) + } + + appParams.HealthCheckTimeout = &timeout + } + + return +} + +func (cmd *Push) uploadApp(appGuid string, appDir string) (apiErr error) { + fileutils.TempDir("apps", func(uploadDir string, err error) { + if err != nil { + apiErr = err + return + } + + presentFiles, err := cmd.actor.GatherFiles(appDir, uploadDir) + if err != nil { + apiErr = err + return + } + + fileutils.TempFile("uploads", func(zipFile *os.File, err error) { + err = cmd.zipAppFiles(zipFile, appDir, uploadDir) + if err != nil { + apiErr = err + return + } + + err = cmd.actor.UploadApp(appGuid, zipFile, presentFiles) + if err != nil { + apiErr = err + return + } + }) + return + }) + return +} + +func (cmd *Push) zipAppFiles(zipFile *os.File, appDir string, uploadDir string) (zipErr error) { + zipErr = cmd.zipWithBetterErrors(uploadDir, zipFile) + if zipErr != nil { + return + } + + zipFileSize, zipErr := cmd.zipper.GetZipSize(zipFile) + if zipErr != nil { + return + } + + zipFileCount := cmd.app_files.CountFiles(uploadDir) + + cmd.describeUploadOperation(appDir, zipFileSize, zipFileCount) + return +} + +func (cmd *Push) zipWithBetterErrors(uploadDir string, zipFile *os.File) error { + zipError := cmd.zipper.Zip(uploadDir, zipFile) + switch err := zipError.(type) { + case nil: + return nil + case *errors.EmptyDirError: + zipFile = nil + return zipError + default: + return errors.NewWithError(T("Error zipping application"), err) + } +} + +func (cmd *Push) describeUploadOperation(path string, zipFileBytes, fileCount int64) { + if fileCount > 0 { + cmd.ui.Say(T("Uploading app files from: {{.Path}}", map[string]interface{}{"Path": path})) + cmd.ui.Say(T("Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + map[string]interface{}{ + "ZipFileBytes": formatters.ByteSize(zipFileBytes), + "FileCount": fileCount})) + } else { + cmd.ui.Warn(T("None of your application files have changed. Nothing will be uploaded.")) + } +} diff --git a/cf/commands/application/push_test.go b/cf/commands/application/push_test.go new file mode 100644 index 00000000000..7996732e38b --- /dev/null +++ b/cf/commands/application/push_test.go @@ -0,0 +1,1089 @@ +package application_test + +import ( + "os" + "path/filepath" + "syscall" + + fakeactors "github.com/cloudfoundry/cli/cf/actors/fakes" + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/resources" + testStacks "github.com/cloudfoundry/cli/cf/api/stacks/fakes" + fakeappfiles "github.com/cloudfoundry/cli/cf/app_files/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/generic" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testmanifest "github.com/cloudfoundry/cli/testhelpers/manifest" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + testwords "github.com/cloudfoundry/cli/words/generator/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("Push Command", func() { + var ( + cmd *Push + ui *testterm.FakeUI + configRepo core_config.ReadWriter + manifestRepo *testmanifest.FakeManifestRepository + starter *testcmd.FakeApplicationStarter + stopper *testcmd.FakeApplicationStopper + serviceBinder *testcmd.FakeAppBinder + appRepo *testApplication.FakeApplicationRepository + domainRepo *testapi.FakeDomainRepository + routeRepo *testapi.FakeRouteRepository + stackRepo *testStacks.FakeStackRepository + serviceRepo *testapi.FakeServiceRepo + wordGenerator *testwords.FakeWordGenerator + requirementsFactory *testreq.FakeReqFactory + authRepo *testapi.FakeAuthenticationRepository + actor *fakeactors.FakePushActor + app_files *fakeappfiles.FakeAppFiles + zipper *fakeappfiles.FakeZipper + ) + + BeforeEach(func() { + manifestRepo = &testmanifest.FakeManifestRepository{} + starter = &testcmd.FakeApplicationStarter{} + stopper = &testcmd.FakeApplicationStopper{} + serviceBinder = &testcmd.FakeAppBinder{} + appRepo = &testApplication.FakeApplicationRepository{} + + domainRepo = &testapi.FakeDomainRepository{} + sharedDomain := maker.NewSharedDomainFields(maker.Overrides{"name": "foo.cf-app.com", "guid": "foo-domain-guid"}) + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{sharedDomain} + + routeRepo = &testapi.FakeRouteRepository{} + stackRepo = &testStacks.FakeStackRepository{} + serviceRepo = &testapi.FakeServiceRepo{} + authRepo = &testapi.FakeAuthenticationRepository{} + wordGenerator = new(testwords.FakeWordGenerator) + wordGenerator.BabbleReturns("laughing-cow") + + ui = new(testterm.FakeUI) + configRepo = testconfig.NewRepositoryWithDefaults() + + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + + zipper = &fakeappfiles.FakeZipper{} + app_files = &fakeappfiles.FakeAppFiles{} + actor = &fakeactors.FakePushActor{} + + cmd = NewPush(ui, configRepo, manifestRepo, starter, stopper, serviceBinder, + appRepo, + domainRepo, + routeRepo, + stackRepo, + serviceRepo, + authRepo, + wordGenerator, + actor, + zipper, + app_files) + }) + + callPush := func(args ...string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("passes when logged in and a space is targeted", func() { + Expect(callPush()).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(callPush()).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + Expect(callPush()).To(BeFalse()) + }) + + // yes, we're aware that the args here should probably be provided in a different order + // erg: app-name -p some/path some-extra-arg + // but the test infrastructure for parsing args and flags is sorely lacking + It("fails when provided too many args", func() { + Expect(callPush("-p", "path", "too-much", "app-name")).To(BeFalse()) + }) + }) + + Describe("when pushing a new app", func() { + BeforeEach(func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("App", "the-app") + + zipper.ZipReturns(nil) + zipper.GetZipSizeReturns(9001, nil) + actor.GatherFilesReturns(nil, nil) + actor.UploadAppReturns(nil) + }) + + Context("when the default route for the app already exists", func() { + BeforeEach(func() { + route := models.Route{} + route.Guid = "my-route-guid" + route.Host = "my-new-app" + route.Domain = domainRepo.ListDomainsForOrgDomains[0] + + routeRepo.FindByHostAndDomainReturns.Route = route + }) + + It("binds to existing routes", func() { + callPush("my-new-app") + + Expect(routeRepo.CreatedHost).To(BeEmpty()) + Expect(routeRepo.CreatedDomainGuid).To(BeEmpty()) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-new-app-guid")) + Expect(routeRepo.BoundRouteGuid).To(Equal("my-route-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Using", "my-new-app.foo.cf-app.com"}, + []string{"Binding", "my-new-app.foo.cf-app.com"}, + []string{"OK"}, + )) + }) + }) + + Context("when the default route for the app does not exist", func() { + BeforeEach(func() { + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "couldn't find it") + }) + + It("refreshes the auth token (so fresh)", func() { // so clean + callPush("fresh-prince") + + Expect(authRepo.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when refreshing the auth token fails", func() { + BeforeEach(func() { + authRepo.RefreshTokenError = errors.New("I accidentally the UAA") + }) + + It("it displays an error", func() { + callPush("of-bel-air") + + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"Error refreshing auth token"}, + )) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"accidentally the UAA"}, + )) + }) + }) + + It("creates an app", func() { + callPush("-t", "111", "my-new-app") + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + + Expect(*appRepo.CreatedAppParams().Name).To(Equal("my-new-app")) + Expect(*appRepo.CreatedAppParams().SpaceGuid).To(Equal("my-space-guid")) + + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app")) + Expect(routeRepo.CreatedHost).To(Equal("my-new-app")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("foo-domain-guid")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-new-app-guid")) + Expect(routeRepo.BoundRouteGuid).To(Equal("my-new-app-route-guid")) + + appGuid, _, _ := actor.UploadAppArgsForCall(0) + Expect(appGuid).To(Equal("my-new-app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating app", "my-new-app", "my-org", "my-space"}, + []string{"OK"}, + []string{"Creating", "my-new-app.foo.cf-app.com"}, + []string{"OK"}, + []string{"Binding", "my-new-app.foo.cf-app.com"}, + []string{"OK"}, + []string{"Uploading my-new-app"}, + []string{"OK"}, + )) + + Expect(stopper.ApplicationStopCallCount()).To(Equal(0)) + + app, orgName, spaceName := starter.ApplicationStartArgsForCall(0) + Expect(app.Guid).To(Equal(appGuid)) + Expect(app.Name).To(Equal("my-new-app")) + Expect(orgName).To(Equal(configRepo.OrganizationFields().Name)) + Expect(spaceName).To(Equal(configRepo.SpaceFields().Name)) + Expect(starter.SetStartTimeoutInSecondsArgsForCall(0)).To(Equal(111)) + }) + + It("strips special characters when creating a default route", func() { + callPush("-t", "111", "Tim's 1st-Crazy__app!") + Expect(*appRepo.CreatedAppParams().Name).To(Equal("Tim's 1st-Crazy__app!")) + + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("tims-1st-crazy-app")) + Expect(routeRepo.CreatedHost).To(Equal("tims-1st-crazy-app")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating", "tims-1st-crazy-app.foo.cf-app.com"}, + []string{"Binding", "tims-1st-crazy-app.foo.cf-app.com"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("sets the app params from the flags", func() { + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "bar.cf-app.com", + Guid: "bar-domain-guid", + } + stackRepo.FindByNameReturns(models.Stack{ + Name: "customLinux", + Guid: "custom-linux-guid", + }, nil) + + callPush( + "-c", "unicorn -c config/unicorn.rb -D", + "-d", "bar.cf-app.com", + "-n", "my-hostname", + "-k", "4G", + "-i", "3", + "-m", "2G", + "-b", "https://github.com/heroku/heroku-buildpack-play.git", + "-s", "customLinux", + "-t", "1", + "--no-start", + "my-new-app", + ) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Using", "customLinux"}, + []string{"OK"}, + []string{"Creating app", "my-new-app"}, + []string{"OK"}, + []string{"Creating route", "my-hostname.bar.cf-app.com"}, + []string{"OK"}, + []string{"Binding", "my-hostname.bar.cf-app.com", "my-new-app"}, + []string{"Uploading", "my-new-app"}, + []string{"OK"}, + )) + + Expect(stackRepo.FindByNameArgsForCall(0)).To(Equal("customLinux")) + + Expect(*appRepo.CreatedAppParams().Name).To(Equal("my-new-app")) + Expect(*appRepo.CreatedAppParams().Command).To(Equal("unicorn -c config/unicorn.rb -D")) + Expect(*appRepo.CreatedAppParams().InstanceCount).To(Equal(3)) + Expect(*appRepo.CreatedAppParams().DiskQuota).To(Equal(int64(4096))) + Expect(*appRepo.CreatedAppParams().Memory).To(Equal(int64(2048))) + Expect(*appRepo.CreatedAppParams().StackGuid).To(Equal("custom-linux-guid")) + Expect(*appRepo.CreatedAppParams().HealthCheckTimeout).To(Equal(1)) + Expect(*appRepo.CreatedAppParams().BuildpackUrl).To(Equal("https://github.com/heroku/heroku-buildpack-play.git")) + + Expect(domainRepo.FindByNameInOrgName).To(Equal("bar.cf-app.com")) + Expect(domainRepo.FindByNameInOrgGuid).To(Equal("my-org-guid")) + + Expect(routeRepo.CreatedHost).To(Equal("my-hostname")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("bar-domain-guid")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-new-app-guid")) + Expect(routeRepo.BoundRouteGuid).To(Equal("my-hostname-route-guid")) + + appGuid, _, _ := actor.UploadAppArgsForCall(0) + Expect(appGuid).To(Equal("my-new-app-guid")) + + Expect(starter.ApplicationStartCallCount()).To(Equal(0)) + }) + + Context("when there is a shared domain", func() { + It("creates a route with the shared domain and maps it to the app", func() { + privateDomain := models.DomainFields{ + Shared: false, + Name: "private.cf-app.com", + Guid: "private-domain-guid", + } + sharedDomain := models.DomainFields{ + Name: "shared.cf-app.com", + Shared: true, + Guid: "shared-domain-guid", + } + + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{privateDomain, sharedDomain} + + callPush("-t", "111", "my-new-app") + + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app")) + Expect(routeRepo.CreatedHost).To(Equal("my-new-app")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("shared-domain-guid")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-new-app-guid")) + Expect(routeRepo.BoundRouteGuid).To(Equal("my-new-app-route-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating app", "my-new-app", "my-org", "my-space"}, + []string{"OK"}, + []string{"Creating", "my-new-app.shared.cf-app.com"}, + []string{"OK"}, + []string{"Binding", "my-new-app.shared.cf-app.com"}, + []string{"OK"}, + []string{"Uploading my-new-app"}, + []string{"OK"}, + )) + }) + }) + + Context("when there is no shared domain but there is a private domain in the targeted org", func() { + It("creates a route with the private domain and maps it to the app", func() { + privateDomain := models.DomainFields{ + Shared: false, + Name: "private.cf-app.com", + Guid: "private-domain-guid", + } + + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{privateDomain} + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("App", "the-app") + + callPush("-t", "111", "my-new-app") + + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app")) + Expect(routeRepo.CreatedHost).To(Equal("my-new-app")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("private-domain-guid")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-new-app-guid")) + Expect(routeRepo.BoundRouteGuid).To(Equal("my-new-app-route-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating app", "my-new-app", "my-org", "my-space"}, + []string{"OK"}, + []string{"Creating", "my-new-app.private.cf-app.com"}, + []string{"OK"}, + []string{"Binding", "my-new-app.private.cf-app.com"}, + []string{"OK"}, + []string{"Uploading my-new-app"}, + []string{"OK"}, + )) + }) + }) + + Describe("randomized hostnames", func() { + var manifestApp generic.Map + + BeforeEach(func() { + manifest := singleAppManifest() + manifestApp = manifest.Data.Get("applications").([]interface{})[0].(generic.Map) + manifestApp.Delete("host") + manifestRepo.ReadManifestReturns.Manifest = manifest + }) + + It("provides a random hostname when the --random-route flag is passed", func() { + callPush("--random-route", "my-new-app") + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app-laughing-cow")) + }) + + It("provides a random hostname when the random-route option is set in the manifest", func() { + manifestApp.Set("random-route", true) + + callPush("my-new-app") + + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("my-new-app-laughing-cow")) + }) + }) + + It("pushes the contents of the directory specified using the -p flag", func() { + callPush("-p", "../some/path-to/an-app", "app-with-path") + + appDir, _ := actor.GatherFilesArgsForCall(0) + Expect(appDir).To(Equal("../some/path-to/an-app")) + }) + + It("pushes the contents of the current working directory by default", func() { + callPush("app-with-default-path") + dir, _ := os.Getwd() + + appDir, _ := actor.GatherFilesArgsForCall(0) + Expect(appDir).To(Equal(dir)) + }) + + It("fails when given a bad manifest path", func() { + manifestRepo.ReadManifestReturns.Manifest = manifest.NewEmptyManifest() + manifestRepo.ReadManifestReturns.Error = errors.New("read manifest error") + + callPush("-f", "bad/manifest/path") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"read manifest error"}, + )) + }) + + It("does not fail when the current working directory does not contain a manifest", func() { + manifestRepo.ReadManifestReturns.Manifest = singleAppManifest() + manifestRepo.ReadManifestReturns.Error = syscall.ENOENT + manifestRepo.ReadManifestReturns.Manifest.Path = "" + + callPush("--no-route", "app-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating app", "app-name"}, + []string{"OK"}, + []string{"Uploading", "app-name"}, + []string{"OK"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("uses the manifest in the current directory by default", func() { + manifestRepo.ReadManifestReturns.Manifest = singleAppManifest() + manifestRepo.ReadManifestReturns.Manifest.Path = "manifest.yml" + + callPush("-p", "some/relative/path") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"Using manifest file", "manifest.yml"})) + + cwd, _ := os.Getwd() + Expect(manifestRepo.ReadManifestArgs.Path).To(Equal(cwd)) + }) + + It("does not use a manifest if the 'no-manifest' flag is passed", func() { + callPush("--no-route", "--no-manifest", "app-name") + + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"FAILED"}, + []string{"hacker-manifesto"}, + )) + + Expect(manifestRepo.ReadManifestArgs.Path).To(Equal("")) + Expect(*appRepo.CreatedAppParams().Name).To(Equal("app-name")) + }) + + It("pushes an app when provided a manifest with one app defined", func() { + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "manifest-example.com", + Guid: "bar-domain-guid", + } + + manifestRepo.ReadManifestReturns.Manifest = singleAppManifest() + + callPush() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "manifest-host.manifest-example.com"}, + []string{"OK"}, + []string{"Binding", "manifest-host.manifest-example.com"}, + []string{"manifest-app-name"}, + )) + + Expect(*appRepo.CreatedAppParams().Name).To(Equal("manifest-app-name")) + Expect(*appRepo.CreatedAppParams().Memory).To(Equal(int64(128))) + Expect(*appRepo.CreatedAppParams().InstanceCount).To(Equal(1)) + Expect(*appRepo.CreatedAppParams().StackName).To(Equal("custom-stack")) + Expect(*appRepo.CreatedAppParams().BuildpackUrl).To(Equal("some-buildpack")) + Expect(*appRepo.CreatedAppParams().Command).To(Equal("JAVA_HOME=$PWD/.openjdk JAVA_OPTS=\"-Xss995K\" ./bin/start.sh run")) + // Expect(actor.UploadedDir).To(Equal(filepath.Clean("some/path/from/manifest"))) TODO: Re-enable this once we develop a strategy + + Expect(*appRepo.CreatedAppParams().EnvironmentVars).To(Equal(map[string]interface{}{ + "PATH": "/u/apps/my-app/bin", + "FOO": "baz", + })) + }) + + It("fails when parsing the manifest has errors", func() { + manifestRepo.ReadManifestReturns.Manifest = &manifest.Manifest{Path: "/some-path/"} + manifestRepo.ReadManifestReturns.Error = errors.New("buildpack should not be null") + + callPush() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Error", "reading", "manifest"}, + []string{"buildpack should not be null"}, + )) + }) + + It("does not create a route when provided the --no-route flag", func() { + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "bar.cf-app.com", + Guid: "bar-domain-guid", + } + + callPush("--no-route", "my-new-app") + + Expect(*appRepo.CreatedAppParams().Name).To(Equal("my-new-app")) + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("")) + }) + + It("maps the root domain route to the app when given the --no-hostname flag", func() { + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{{ + Name: "bar.cf-app.com", + Guid: "bar-domain-guid", + Shared: true, + }} + + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "uh oh") + + callPush("--no-hostname", "my-new-app") + + Expect(*appRepo.CreatedAppParams().Name).To(Equal("my-new-app")) + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("bar-domain-guid")) + }) + + It("Does not create a route when the no-route property is in the manifest", func() { + workerManifest := singleAppManifest() + workerManifest.Data.Get("applications").([]interface{})[0].(generic.Map).Set("no-route", true) + manifestRepo.ReadManifestReturns.Manifest = workerManifest + + callPush("worker-app") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"worker-app", "is a worker", "skipping route creation"})) + Expect(routeRepo.BoundAppGuid).To(Equal("")) + Expect(routeRepo.BoundRouteGuid).To(Equal("")) + }) + + It("fails when given an invalid memory limit", func() { + callPush("-m", "abcM", "my-new-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Invalid", "memory limit", "abcM"}, + )) + }) + + Context("when a manifest has many apps", func() { + BeforeEach(func() { + manifestRepo.ReadManifestReturns.Manifest = manifestWithServicesAndEnv() + }) + + It("pushes each app", func() { + callPush() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating", "app1"}, + []string{"Creating", "app2"}, + )) + Expect(len(appRepo.CreateAppParams)).To(Equal(2)) + + firstApp := appRepo.CreateAppParams[0] + secondApp := appRepo.CreateAppParams[1] + Expect(*firstApp.Name).To(Equal("app1")) + Expect(*secondApp.Name).To(Equal("app2")) + + envVars := *firstApp.EnvironmentVars + Expect(envVars["SOMETHING"]).To(Equal("definitely-something")) + + envVars = *secondApp.EnvironmentVars + Expect(envVars["SOMETHING"]).To(Equal("nothing")) + }) + + It("pushes a single app when given the name of a single app in the manifest", func() { + callPush("app2") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"Creating", "app2"})) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Creating", "app1"})) + Expect(len(appRepo.CreateAppParams)).To(Equal(1)) + Expect(*appRepo.CreateAppParams[0].Name).To(Equal("app2")) + }) + + It("fails when given the name of an app that is not in the manifest", func() { + callPush("non-existant-app") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + Expect(len(appRepo.CreateAppParams)).To(Equal(0)) + }) + }) + }) + }) + + Describe("re-pushing an existing app", func() { + var existingApp models.Application + + BeforeEach(func() { + existingApp = models.Application{} + existingApp.Name = "existing-app" + existingApp.Guid = "existing-app-guid" + existingApp.Command = "unicorn -c config/unicorn.rb -D" + existingApp.EnvironmentVars = map[string]interface{}{ + "crazy": "pants", + "FOO": "NotYoBaz", + "foo": "manchu", + } + + appRepo.ReadReturns.App = existingApp + appRepo.UpdateAppResult = existingApp + }) + + It("resets the app's buildpack when the -b flag is provided as 'default'", func() { + callPush("-b", "default", "existing-app") + Expect(*appRepo.UpdateParams.BuildpackUrl).To(Equal("")) + }) + + It("resets the app's command when the -c flag is provided as 'default'", func() { + callPush("-c", "default", "existing-app") + Expect(*appRepo.UpdateParams.Command).To(Equal("")) + }) + + It("resets the app's buildpack when the -b flag is provided as 'null'", func() { + callPush("-b", "null", "existing-app") + Expect(*appRepo.UpdateParams.BuildpackUrl).To(Equal("")) + }) + + It("resets the app's command when the -c flag is provided as 'null'", func() { + callPush("-c", "null", "existing-app") + Expect(*appRepo.UpdateParams.Command).To(Equal("")) + }) + + It("merges env vars from the manifest with those from the server", func() { + manifestRepo.ReadManifestReturns.Manifest = singleAppManifest() + + callPush("existing-app") + + updatedAppEnvVars := *appRepo.UpdateParams.EnvironmentVars + Expect(updatedAppEnvVars["crazy"]).To(Equal("pants")) + Expect(updatedAppEnvVars["FOO"]).To(Equal("baz")) + Expect(updatedAppEnvVars["foo"]).To(Equal("manchu")) + Expect(updatedAppEnvVars["PATH"]).To(Equal("/u/apps/my-app/bin")) + }) + + It("stops the app, achieving a full-downtime deploy!", func() { + appRepo.UpdateAppResult = existingApp + + callPush("existing-app") + + app, orgName, spaceName := stopper.ApplicationStopArgsForCall(0) + Expect(app.Guid).To(Equal(existingApp.Guid)) + Expect(app.Name).To(Equal("existing-app")) + Expect(orgName).To(Equal(configRepo.OrganizationFields().Name)) + Expect(spaceName).To(Equal(configRepo.SpaceFields().Name)) + + appGuid, _, _ := actor.UploadAppArgsForCall(0) + Expect(appGuid).To(Equal(existingApp.Guid)) + }) + + It("does not stop the app when it is already stopped", func() { + existingApp.State = "stopped" + appRepo.ReadReturns.App = existingApp + appRepo.UpdateAppResult = existingApp + + callPush("existing-app") + + Expect(stopper.ApplicationStopCallCount()).To(Equal(0)) + }) + + It("updates the app", func() { + existingRoute := models.RouteSummary{} + existingRoute.Host = "existing-app" + + existingApp.Routes = []models.RouteSummary{existingRoute} + appRepo.ReadReturns.App = existingApp + appRepo.UpdateAppResult = existingApp + + stackRepo.FindByNameReturns(models.Stack{ + Name: "differentStack", + Guid: "differentStack-guid", + }, nil) + + callPush( + "-c", "different start command", + "-i", "10", + "-m", "1G", + "-b", "https://github.com/heroku/heroku-buildpack-different.git", + "-s", "differentStack", + "existing-app", + ) + + Expect(appRepo.UpdateAppGuid).To(Equal(existingApp.Guid)) + Expect(*appRepo.UpdateParams.Command).To(Equal("different start command")) + Expect(*appRepo.UpdateParams.InstanceCount).To(Equal(10)) + Expect(*appRepo.UpdateParams.Memory).To(Equal(int64(1024))) + Expect(*appRepo.UpdateParams.BuildpackUrl).To(Equal("https://github.com/heroku/heroku-buildpack-different.git")) + Expect(*appRepo.UpdateParams.StackGuid).To(Equal("differentStack-guid")) + }) + + It("re-uploads the app", func() { + callPush("existing-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Uploading", "existing-app"}, + []string{"OK"}, + )) + }) + + Describe("when the app has a route bound", func() { + BeforeEach(func() { + domain := models.DomainFields{ + Name: "example.com", + Guid: "domain-guid", + Shared: true, + } + + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{domain} + routeRepo.FindByHostAndDomainReturns.Route = models.Route{ + Host: "existing-app", + Domain: domain, + } + + existingApp.Routes = []models.RouteSummary{models.RouteSummary{ + Guid: "existing-route-guid", + Host: "existing-app", + Domain: domain, + }} + + appRepo.ReadReturns.App = existingApp + appRepo.UpdateAppResult = existingApp + }) + + It("uses the existing route when an app already has it bound", func() { + callPush("-d", "example.com", "existing-app") + + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Creating route"})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"Using route", "existing-app", "example.com"})) + }) + + Context("and no route-related flags are given", func() { + Context("and there is no route in the manifest", func() { + It("does not add a route to the app", func() { + callPush("existing-app") + + appGuid, _, _ := actor.UploadAppArgsForCall(0) + Expect(appGuid).To(Equal("existing-app-guid")) + Expect(domainRepo.FindByNameInOrgName).To(Equal("")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Domain.Name).To(Equal("")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("")) + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("")) + }) + }) + + Context("and there is a route in the manifest", func() { + BeforeEach(func() { + manifestRepo.ReadManifestReturns.Manifest = existingAppManifest() + + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "uh oh") + + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "example.com", + Guid: "example-domain-guid", + } + }) + + It("adds the route", func() { + callPush("existing-app") + Expect(routeRepo.CreatedHost).To(Equal("new-manifest-host")) + }) + }) + }) + + It("creates and binds a route when a different domain is specified", func() { + newDomain := models.DomainFields{Guid: "domain-guid", Name: "newdomain.com"} + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "existing-app.newdomain.com") + domainRepo.FindByNameInOrgDomain = newDomain + + callPush("-d", "newdomain.com", "existing-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "existing-app.newdomain.com"}, + []string{"OK"}, + []string{"Binding", "existing-app.newdomain.com"}, + )) + + Expect(domainRepo.FindByNameInOrgName).To(Equal("newdomain.com")) + Expect(domainRepo.FindByNameInOrgGuid).To(Equal("my-org-guid")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Domain.Name).To(Equal("newdomain.com")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("existing-app")) + Expect(routeRepo.CreatedHost).To(Equal("existing-app")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("domain-guid")) + }) + + It("creates and binds a route when a different hostname is specified", func() { + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "new-host.newdomain.com") + + callPush("-n", "new-host", "existing-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "new-host.example.com"}, + []string{"OK"}, + []string{"Binding", "new-host.example.com"}, + )) + + Expect(routeRepo.FindByHostAndDomainCalledWith.Domain.Name).To(Equal("example.com")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("new-host")) + Expect(routeRepo.CreatedHost).To(Equal("new-host")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("domain-guid")) + }) + + It("removes the route when the --no-route flag is given", func() { + callPush("--no-route", "existing-app") + + appGuid, _, _ := actor.UploadAppArgsForCall(0) + Expect(appGuid).To(Equal("existing-app-guid")) + Expect(domainRepo.FindByNameInOrgName).To(Equal("")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Domain.Name).To(Equal("")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("")) + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("")) + Expect(routeRepo.UnboundRouteGuid).To(Equal("existing-route-guid")) + Expect(routeRepo.UnboundAppGuid).To(Equal("existing-app-guid")) + }) + + It("binds the root domain route to an app with a pre-existing route when the --no-hostname flag is given", func() { + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "existing-app.example.com") + + callPush("--no-hostname", "existing-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "example.com"}, + []string{"OK"}, + []string{"Binding", "example.com"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"existing-app.example.com"})) + + Expect(routeRepo.FindByHostAndDomainCalledWith.Domain.Name).To(Equal("example.com")) + Expect(routeRepo.FindByHostAndDomainCalledWith.Host).To(Equal("")) + Expect(routeRepo.CreatedHost).To(Equal("")) + Expect(routeRepo.CreatedDomainGuid).To(Equal("domain-guid")) + }) + }) + }) + + Describe("service instances", func() { + BeforeEach(func() { + serviceRepo.FindInstanceByNameMap = generic.NewMap(map[interface{}]interface{}{ + "global-service": maker.NewServiceInstance("global-service"), + "app1-service": maker.NewServiceInstance("app1-service"), + "app2-service": maker.NewServiceInstance("app2-service"), + }) + + manifestRepo.ReadManifestReturns.Manifest = manifestWithServicesAndEnv() + }) + + Context("when the service is not bound", func() { + BeforeEach(func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("App", "the-app") + }) + + It("binds service instances to the app", func() { + callPush() + Expect(len(serviceBinder.AppsToBind)).To(Equal(4)) + Expect(serviceBinder.AppsToBind[0].Name).To(Equal("app1")) + Expect(serviceBinder.AppsToBind[1].Name).To(Equal("app1")) + Expect(serviceBinder.InstancesToBindTo[0].Name).To(Equal("app1-service")) + Expect(serviceBinder.InstancesToBindTo[1].Name).To(Equal("global-service")) + + Expect(serviceBinder.AppsToBind[2].Name).To(Equal("app2")) + Expect(serviceBinder.AppsToBind[3].Name).To(Equal("app2")) + Expect(serviceBinder.InstancesToBindTo[2].Name).To(Equal("app2-service")) + Expect(serviceBinder.InstancesToBindTo[3].Name).To(Equal("global-service")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating", "app1"}, + []string{"OK"}, + []string{"Binding service", "app1-service", "app1", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Binding service", "global-service", "app1", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Creating", "app2"}, + []string{"OK"}, + []string{"Binding service", "app2-service", "app2", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Binding service", "global-service", "app2", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + }) + }) + + Context("when the app is already bound to the service", func() { + BeforeEach(func() { + appRepo.ReadReturns.App = maker.NewApp(maker.Overrides{}) + serviceBinder.BindApplicationReturns.Error = errors.NewHttpError(500, "90003", "it don't work") + }) + + It("gracefully continues", func() { + callPush() + Expect(len(serviceBinder.AppsToBind)).To(Equal(4)) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when the service instance can't be found", func() { + BeforeEach(func() { + // routeRepo.FindByHostAndDomainReturns.Error = errors.new("can't find service instance") + serviceRepo.FindInstanceByNameErr = true + manifestRepo.ReadManifestReturns.Manifest = manifestWithServicesAndEnv() + }) + + It("fails with an error", func() { + callPush() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Could not find service", "app1-service", "app1"}, + )) + }) + }) + + }) + + Describe("checking for bad flags", func() { + It("fails when a non-numeric start timeout is given", func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("App", "the-app") + + callPush("-t", "FooeyTimeout", "my-new-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Invalid", "timeout", "FooeyTimeout"}, + )) + }) + }) + + Describe("displaying information about files being uploaded", func() { + It("displays information about the files being uploaded", func() { + app_files.CountFilesReturns(11) + zipper.ZipReturns(nil) + zipper.GetZipSizeReturns(6100000, nil) + actor.GatherFilesReturns([]resources.AppFileResource{resources.AppFileResource{Path: "path/to/app"}, resources.AppFileResource{Path: "bar"}}, nil) + + curDir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + callPush("appName") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Uploading", curDir}, + []string{"5.8M", "11 files"}, + )) + }) + + It("omits the size when there are no files being uploaded", func() { + app_files.CountFilesReturns(0) + + callPush("appName") + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"None of your application files have changed", "Nothing will be uploaded"}, + )) + }) + }) + + It("fails when the app can't be uploaded", func() { + actor.UploadAppReturns(errors.New("Boom!")) + + callPush("app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Uploading"}, + []string{"FAILED"}, + )) + }) + + Describe("when binding the route fails", func() { + BeforeEach(func() { + routeRepo.FindByHostAndDomainReturns.Route.Host = "existing-app" + routeRepo.FindByHostAndDomainReturns.Route.Domain = models.DomainFields{Name: "foo.cf-app.com"} + }) + + It("suggests using 'random-route' if the default route is taken", func() { + routeRepo.BindErr = errors.NewHttpError(400, errors.INVALID_RELATION, "The URL not available") + + callPush("existing-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"existing-app.foo.cf-app.com", "already in use"}, + []string{"TIP", "random-route"}, + )) + }) + + It("does not suggest using 'random-route' for other failures", func() { + routeRepo.BindErr = errors.NewHttpError(500, "some-code", "exception happened") + + callPush("existing-app") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"TIP", "random-route"})) + }) + }) + + It("fails when neither a manifest nor a name is given", func() { + manifestRepo.ReadManifestReturns.Error = errors.New("No such manifest") + callPush() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"App name"}, + )) + }) +}) + +func existingAppManifest() *manifest.Manifest { + return &manifest.Manifest{ + Path: "manifest.yml", + Data: generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "name": "manifest-app-name", + "memory": "128MB", + "instances": 1, + "host": "new-manifest-host", + "domain": "example.com", + "stack": "custom-stack", + "timeout": 360, + "buildpack": "some-buildpack", + "command": `JAVA_HOME=$PWD/.openjdk JAVA_OPTS="-Xss995K" ./bin/start.sh run`, + "path": filepath.Clean("some/path/from/manifest"), + "env": generic.NewMap(map[interface{}]interface{}{ + "FOO": "baz", + "PATH": "/u/apps/my-app/bin", + }), + }), + }, + }), + } +} + +func singleAppManifest() *manifest.Manifest { + return &manifest.Manifest{ + Path: "manifest.yml", + Data: generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "name": "manifest-app-name", + "memory": "128MB", + "instances": 1, + "host": "manifest-host", + "domain": "manifest-example.com", + "stack": "custom-stack", + "timeout": 360, + "buildpack": "some-buildpack", + "command": `JAVA_HOME=$PWD/.openjdk JAVA_OPTS="-Xss995K" ./bin/start.sh run`, + "path": filepath.Clean("some/path/from/manifest"), + "env": generic.NewMap(map[interface{}]interface{}{ + "FOO": "baz", + "PATH": "/u/apps/my-app/bin", + }), + }), + }, + }), + } +} + +func manifestWithServicesAndEnv() *manifest.Manifest { + return &manifest.Manifest{ + Data: generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "name": "app1", + "services": []interface{}{"app1-service", "global-service"}, + "env": generic.NewMap(map[interface{}]interface{}{ + "SOMETHING": "definitely-something", + }), + }), + generic.NewMap(map[interface{}]interface{}{ + "name": "app2", + "services": []interface{}{"app2-service", "global-service"}, + "env": generic.NewMap(map[interface{}]interface{}{ + "SOMETHING": "nothing", + }), + }), + }, + }), + } +} diff --git a/cf/commands/application/rename.go b/cf/commands/application/rename.go new file mode 100644 index 00000000000..2564d08bfa2 --- /dev/null +++ b/cf/commands/application/rename.go @@ -0,0 +1,70 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameApp struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + appReq requirements.ApplicationRequirement +} + +func NewRenameApp(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository) (cmd *RenameApp) { + cmd = new(RenameApp) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + return +} + +func (cmd *RenameApp) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename", + Description: T("Rename an app"), + Usage: T("CF_NAME rename APP_NAME NEW_APP_NAME"), + } +} + +func (cmd *RenameApp) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.appReq, + } + return +} + +func (cmd *RenameApp) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + newName := c.Args()[1] + + cmd.ui.Say(T("Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "NewName": terminal.EntityNameColor(newName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + params := models.AppParams{Name: &newName} + + _, apiErr := cmd.appRepo.Update(app.Guid, params) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + cmd.ui.Ok() +} diff --git a/cf/commands/application/rename_test.go b/cf/commands/application/rename_test.go new file mode 100644 index 00000000000..b5ce8ca7d86 --- /dev/null +++ b/cf/commands/application/rename_test.go @@ -0,0 +1,64 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Rename command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + appRepo *testApplication.FakeApplicationRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + appRepo = &testApplication.FakeApplicationRepository{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewRenameApp(ui, configRepo, appRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not invoked with an old name and a new name", func() { + requirementsFactory.LoginSuccess = true + runCommand("foo") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("my-app", "my-new-app")).To(BeFalse()) + }) + }) + + It("renames an application", func() { + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + requirementsFactory.LoginSuccess = true + requirementsFactory.Application = app + runCommand("my-app", "my-new-app") + + Expect(appRepo.UpdateAppGuid).To(Equal(app.Guid)) + Expect(*appRepo.UpdateParams.Name).To(Equal("my-new-app")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming app", "my-app", "my-new-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + }) +}) diff --git a/cf/commands/application/restage.go b/cf/commands/application/restage.go new file mode 100644 index 00000000000..834b3f29bf4 --- /dev/null +++ b/cf/commands/application/restage.go @@ -0,0 +1,65 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Restage struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + appStagingWatcher ApplicationStagingWatcher +} + +func NewRestage(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository, stagingWatcher ApplicationStagingWatcher) *Restage { + cmd := new(Restage) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + cmd.appStagingWatcher = stagingWatcher + return cmd +} + +func (cmd *Restage) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "restage", + ShortName: "rg", + Description: T("Restage an app"), + Usage: T("CF_NAME restage APP"), + } +} + +func (cmd *Restage) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + return []requirements.Requirement{requirementsFactory.NewLoginRequirement()}, nil +} + +func (cmd *Restage) Run(c *cli.Context) { + app, err := cmd.appRepo.Read(c.Args()[0]) + if notFound, ok := err.(*errors.ModelNotFoundError); ok { + cmd.ui.Failed(notFound.Error()) + } + + cmd.ui.Say(T("Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + cmd.appStagingWatcher.ApplicationWatchStaging(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name, func(app models.Application) (models.Application, error) { + return app, cmd.appRepo.CreateRestageRequest(app.Guid) + }) +} diff --git a/cf/commands/application/restage_test.go b/cf/commands/application/restage_test.go new file mode 100644 index 00000000000..05e46edcbd8 --- /dev/null +++ b/cf/commands/application/restage_test.go @@ -0,0 +1,107 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("restage command", func() { + var ( + ui *testterm.FakeUI + app models.Application + appRepo *testApplication.FakeApplicationRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + stagingWatcher *fakeStagingWatcher + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + + app = models.Application{} + app.Name = "my-app" + appRepo = &testApplication.FakeApplicationRepository{} + appRepo.ReadReturns.App = app + + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + + stagingWatcher = &fakeStagingWatcher{} + }) + + runCommand := func(args ...string) bool { + cmd := NewRestage(ui, configRepo, appRepo, stagingWatcher) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("Requirements", func() { + It("fails when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("my-app")).To(BeFalse()) + }) + + It("fails with usage when no arguments are given", func() { + passed := runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + }) + + It("fails with usage when the app cannot be found", func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("app", "hocus-pocus") + runCommand("hocus-pocus") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"not found"}, + )) + }) + + Context("when the app is found", func() { + BeforeEach(func() { + app = models.Application{} + app.Name = "my-app" + app.Guid = "the-app-guid" + + appRepo.ReadReturns.App = app + }) + + It("sends a restage request", func() { + runCommand("my-app") + Expect(appRepo.CreateRestageRequestArgs.AppGuid).To(Equal("the-app-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Restaging app", "my-app", "my-org", "my-space", "my-user"}, + )) + }) + + It("watches the staging output", func() { + runCommand("my-app") + Expect(stagingWatcher.watched).To(Equal(app)) + Expect(stagingWatcher.orgName).To(Equal(configRepo.OrganizationFields().Name)) + Expect(stagingWatcher.spaceName).To(Equal(configRepo.SpaceFields().Name)) + }) + }) +}) + +type fakeStagingWatcher struct { + watched models.Application + orgName string + spaceName string +} + +func (f *fakeStagingWatcher) ApplicationWatchStaging(app models.Application, orgName, spaceName string, start func(models.Application) (models.Application, error)) (updatedApp models.Application, err error) { + f.watched = app + f.orgName = orgName + f.spaceName = spaceName + return start(app) +} diff --git a/cf/commands/application/restart.go b/cf/commands/application/restart.go new file mode 100644 index 00000000000..5e3b0ed97b3 --- /dev/null +++ b/cf/commands/application/restart.go @@ -0,0 +1,77 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Restart struct { + ui terminal.UI + config core_config.Reader + starter ApplicationStarter + stopper ApplicationStopper + appReq requirements.ApplicationRequirement +} + +type ApplicationRestarter interface { + ApplicationRestart(app models.Application, orgName string, spaceName string) +} + +func NewRestart(ui terminal.UI, config core_config.Reader, starter ApplicationStarter, stopper ApplicationStopper) (cmd *Restart) { + cmd = new(Restart) + cmd.ui = ui + cmd.config = config + cmd.starter = starter + cmd.stopper = stopper + return +} + +func (cmd *Restart) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "restart", + ShortName: "rs", + Description: T("Restart an app"), + Usage: T("CF_NAME restart APP"), + } +} + +func (cmd *Restart) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *Restart) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + cmd.ApplicationRestart(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) +} + +func (cmd *Restart) ApplicationRestart(app models.Application, orgName, spaceName string) { + stoppedApp, err := cmd.stopper.ApplicationStop(app, orgName, spaceName) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + cmd.ui.Say("") + + _, err = cmd.starter.ApplicationStart(stoppedApp, orgName, spaceName) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } +} diff --git a/cf/commands/application/restart_test.go b/cf/commands/application/restart_test.go new file mode 100644 index 00000000000..c4a2b00c667 --- /dev/null +++ b/cf/commands/application/restart_test.go @@ -0,0 +1,89 @@ +package application_test + +import ( + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("restart command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + starter *testcmd.FakeApplicationStarter + stopper *testcmd.FakeApplicationStopper + config core_config.ReadWriter + app models.Application + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + starter = &testcmd.FakeApplicationStarter{} + stopper = &testcmd.FakeApplicationStopper{} + config = testconfig.NewRepositoryWithDefaults() + + app = models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewRestart(ui, config, starter, stopper), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided exactly one arg", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.Application = app + requirementsFactory.TargetedSpaceSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + }) + + Context("when logged in, targeting a space, and an app name is provided", func() { + BeforeEach(func() { + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + + stopper.ApplicationStopReturns(app, nil) + }) + + It("restarts the given app", func() { + runCommand("my-app") + + application, orgName, spaceName := stopper.ApplicationStopArgsForCall(0) + Expect(application).To(Equal(app)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + + application, orgName, spaceName = starter.ApplicationStartArgsForCall(0) + Expect(application).To(Equal(app)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + }) + }) +}) diff --git a/cf/commands/application/scale.go b/cf/commands/application/scale.go new file mode 100644 index 00000000000..d1bb4760479 --- /dev/null +++ b/cf/commands/application/scale.go @@ -0,0 +1,163 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Scale struct { + ui terminal.UI + config core_config.Reader + restarter ApplicationRestarter + appReq requirements.ApplicationRequirement + appRepo applications.ApplicationRepository +} + +func NewScale(ui terminal.UI, config core_config.Reader, restarter ApplicationRestarter, appRepo applications.ApplicationRepository) (cmd *Scale) { + cmd = new(Scale) + cmd.ui = ui + cmd.config = config + cmd.restarter = restarter + cmd.appRepo = appRepo + return +} + +func (cmd *Scale) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "scale", + Description: T("Change or view the instance count, disk space limit, and memory limit for an app"), + Usage: T("CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]"), + Flags: []cli.Flag{ + flag_helpers.NewIntFlag("i", T("Number of instances")), + flag_helpers.NewStringFlag("k", T("Disk limit (e.g. 256M, 1024M, 1G)")), + flag_helpers.NewStringFlag("m", T("Memory limit (e.g. 256M, 1024M, 1G)")), + cli.BoolFlag{Name: "f", Usage: T("Force restart of app without prompt")}, + }, + } +} + +func (cmd *Scale) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +var bytesInAMegabyte int64 = 1024 * 1024 + +func (cmd *Scale) Run(c *cli.Context) { + currentApp := cmd.appReq.GetApplication() + if !anyFlagsSet(c) { + cmd.ui.Say(T("Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(currentApp.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + cmd.ui.Ok() + cmd.ui.Say("") + + cmd.ui.Say("%s %s", terminal.HeaderColor(T("memory:")), formatters.ByteSize(currentApp.Memory*bytesInAMegabyte)) + cmd.ui.Say("%s %s", terminal.HeaderColor(T("disk:")), formatters.ByteSize(currentApp.DiskQuota*bytesInAMegabyte)) + cmd.ui.Say("%s %d", terminal.HeaderColor(T("instances:")), currentApp.InstanceCount) + + return + } + + params := models.AppParams{} + shouldRestart := false + + if c.String("m") != "" { + memory, err := formatters.ToMegabytes(c.String("m")) + if err != nil { + cmd.ui.Failed(T("Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + map[string]interface{}{ + "Memory": c.String("m"), + "ErrorDescription": err, + })) + } + params.Memory = &memory + shouldRestart = true + } + + if c.String("k") != "" { + diskQuota, err := formatters.ToMegabytes(c.String("k")) + if err != nil { + cmd.ui.Failed(T("Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + map[string]interface{}{ + "DiskQuota": c.String("k"), + "ErrorDescription": err, + })) + } + params.DiskQuota = &diskQuota + shouldRestart = true + } + + if c.IsSet("i") { + instances := c.Int("i") + if instances > 0 { + params.InstanceCount = &instances + } else { + cmd.ui.Failed(T("Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + map[string]interface{}{ + "InstanceCount": instances, + })) + } + } + + if shouldRestart && !cmd.confirmRestart(c, currentApp.Name) { + return + } + + cmd.ui.Say(T("Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(currentApp.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + updatedApp, apiErr := cmd.appRepo.Update(currentApp.Guid, params) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + + if shouldRestart { + cmd.restarter.ApplicationRestart(updatedApp, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) + } +} + +func (cmd *Scale) confirmRestart(context *cli.Context, appName string) bool { + if context.Bool("f") { + return true + } else { + result := cmd.ui.Confirm(T("This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + map[string]interface{}{"AppName": terminal.EntityNameColor(appName)})) + cmd.ui.Say("") + return result + } +} + +func anyFlagsSet(context *cli.Context) bool { + return context.IsSet("m") || context.IsSet("k") || context.IsSet("i") +} diff --git a/cf/commands/application/scale_test.go b/cf/commands/application/scale_test.go new file mode 100644 index 00000000000..997e964e49a --- /dev/null +++ b/cf/commands/application/scale_test.go @@ -0,0 +1,168 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("scale command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + restarter *testcmd.FakeApplicationRestarter + appRepo *testApplication.FakeApplicationRepository + ui *testterm.FakeUI + config core_config.ReadWriter + cmd *Scale + app models.Application + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + restarter = &testcmd.FakeApplicationRestarter{} + appRepo = &testApplication.FakeApplicationRepository{} + ui = new(testterm.FakeUI) + config = testconfig.NewRepositoryWithDefaults() + cmd = NewScale(ui, config, restarter, appRepo) + }) + + Describe("requirements", func() { + It("requires the user to be logged in with a targed space", func() { + args := []string{"-m", "1G", "my-app"} + + requirementsFactory.LoginSuccess = false + requirementsFactory.TargetedSpaceSuccess = true + + Expect(testcmd.RunCommand(cmd, args, requirementsFactory)).To(BeFalse()) + + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = false + + Expect(testcmd.RunCommand(cmd, args, requirementsFactory)).To(BeFalse()) + }) + + It("requires an app to be specified", func() { + passed := testcmd.RunCommand(cmd, []string{"-m", "1G"}, requirementsFactory) + + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + + It("does not require any flags", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + + Expect(testcmd.RunCommand(cmd, []string{"my-app"}, requirementsFactory)).To(BeTrue()) + }) + }) + + Describe("scaling an app", func() { + BeforeEach(func() { + app = maker.NewApp(maker.Overrides{"name": "my-app", "guid": "my-app-guid"}) + app.InstanceCount = 42 + app.DiskQuota = 1024 + app.Memory = 256 + + requirementsFactory.Application = app + appRepo.UpdateAppResult = app + }) + + Context("when no flags are specified", func() { + It("prints a description of the app's limits", func() { + testcmd.RunCommand(cmd, []string{"my-app"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Showing", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"memory", "256M"}, + []string{"disk", "1G"}, + []string{"instances", "42"}, + )) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Scaling", "my-app", "my-org", "my-space", "my-user"})) + }) + }) + + Context("when the user does not confirm 'yes'", func() { + It("does not restart the app", func() { + ui.Inputs = []string{"whatever"} + testcmd.RunCommand(cmd, []string{"-i", "5", "-m", "512M", "-k", "2G", "my-app"}, requirementsFactory) + + Expect(restarter.ApplicationRestartCallCount()).To(Equal(0)) + }) + }) + + Context("when the user provides the -f flag", func() { + It("does not prompt the user", func() { + testcmd.RunCommand(cmd, []string{"-f", "-i", "5", "-m", "512M", "-k", "2G", "my-app"}, requirementsFactory) + + application, orgName, spaceName := restarter.ApplicationRestartArgsForCall(0) + Expect(application).To(Equal(app)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + }) + }) + + Context("when the user confirms they want to restart", func() { + BeforeEach(func() { + ui.Inputs = []string{"yes"} + }) + + It("can set an app's instance count, memory limit and disk limit", func() { + testcmd.RunCommand(cmd, []string{"-i", "5", "-m", "512M", "-k", "2G", "my-app"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Scaling", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(ui.Prompts).To(ContainSubstrings([]string{"This will cause the app to restart", "Are you sure", "my-app"})) + + application, orgName, spaceName := restarter.ApplicationRestartArgsForCall(0) + Expect(application).To(Equal(app)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + Expect(*appRepo.UpdateParams.Memory).To(Equal(int64(512))) + Expect(*appRepo.UpdateParams.InstanceCount).To(Equal(5)) + Expect(*appRepo.UpdateParams.DiskQuota).To(Equal(int64(2048))) + }) + + It("does not scale the memory and disk limits if they are not specified", func() { + testcmd.RunCommand(cmd, []string{"-i", "5", "my-app"}, requirementsFactory) + + Expect(restarter.ApplicationRestartCallCount()).To(Equal(0)) + + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + Expect(*appRepo.UpdateParams.InstanceCount).To(Equal(5)) + Expect(appRepo.UpdateParams.DiskQuota).To(BeNil()) + Expect(appRepo.UpdateParams.Memory).To(BeNil()) + }) + + It("does not scale the app's instance count if it is not specified", func() { + testcmd.RunCommand(cmd, []string{"-m", "512M", "my-app"}, requirementsFactory) + + application, orgName, spaceName := restarter.ApplicationRestartArgsForCall(0) + Expect(application).To(Equal(app)) + Expect(orgName).To(Equal(config.OrganizationFields().Name)) + Expect(spaceName).To(Equal(config.SpaceFields().Name)) + + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + Expect(*appRepo.UpdateParams.Memory).To(Equal(int64(512))) + Expect(appRepo.UpdateParams.DiskQuota).To(BeNil()) + Expect(appRepo.UpdateParams.InstanceCount).To(BeNil()) + }) + }) + }) +}) diff --git a/cf/commands/application/set_env.go b/cf/commands/application/set_env.go new file mode 100644 index 00000000000..861781151ad --- /dev/null +++ b/cf/commands/application/set_env.go @@ -0,0 +1,84 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetEnv struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + appReq requirements.ApplicationRequirement +} + +func NewSetEnv(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository) *SetEnv { + return &SetEnv{ + ui: ui, + config: config, + appRepo: appRepo, + } +} + +func (cmd *SetEnv) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-env", + ShortName: "se", + Description: T("Set an env variable for an app"), + Usage: T("CF_NAME set-env APP NAME VALUE"), + SkipFlagParsing: true, + } +} + +func (cmd *SetEnv) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *SetEnv) Run(c *cli.Context) { + varName := c.Args()[1] + varValue := c.Args()[2] + app := cmd.appReq.GetApplication() + + cmd.ui.Say(T("Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "VarName": terminal.EntityNameColor(varName), + "VarValue": terminal.EntityNameColor(varValue), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) + + if len(app.EnvironmentVars) == 0 { + app.EnvironmentVars = map[string]interface{}{} + } + envParams := app.EnvironmentVars + envParams[varName] = varValue + + _, apiErr := cmd.appRepo.Update(app.Guid, models.AppParams{EnvironmentVars: &envParams}) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say(T("TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + map[string]interface{}{"Command": terminal.CommandColor(cf.Name() + " restage")})) +} diff --git a/cf/commands/application/set_env_test.go b/cf/commands/application/set_env_test.go new file mode 100644 index 00000000000..0e2aef942db --- /dev/null +++ b/cf/commands/application/set_env_test.go @@ -0,0 +1,159 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("set-env command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + app models.Application + appRepo *testApplication.FakeApplicationRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + app = models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + appRepo = &testApplication.FakeApplicationRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewSetEnv(ui, configRepo, appRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when login is not successful", func() { + requirementsFactory.Application = app + requirementsFactory.TargetedSpaceSuccess = true + + Expect(runCommand("hey", "gabba", "gabba")).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + + Expect(runCommand("hey", "gabba", "gabba")).To(BeFalse()) + }) + + It("fails with usage when not provided with exactly three args", func() { + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + + runCommand("zomg", "too", "many", "args") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in, a space is targeted and given enough args", func() { + BeforeEach(func() { + app.EnvironmentVars = map[string]interface{}{"foo": "bar"} + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + }) + + Context("when it is new", func() { + It("is created", func() { + runCommand("my-app", "DATABASE_URL", "mysql://new-example.com/my-db") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{ + "Setting env variable", + "DATABASE_URL", + "mysql://new-example.com/my-db", + "my-app", + "my-org", + "my-space", + "my-user", + }, + []string{"OK"}, + []string{"TIP"}, + )) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(appRepo.UpdateAppGuid).To(Equal(app.Guid)) + Expect(*appRepo.UpdateParams.EnvironmentVars).To(Equal(map[string]interface{}{ + "DATABASE_URL": "mysql://new-example.com/my-db", + "foo": "bar", + })) + }) + }) + + Context("when it already exists", func() { + BeforeEach(func() { + app.EnvironmentVars["DATABASE_URL"] = "mysql://old-example.com/my-db" + }) + + It("is updated", func() { + runCommand("my-app", "DATABASE_URL", "mysql://new-example.com/my-db") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{ + "Setting env variable", + "DATABASE_URL", + "mysql://new-example.com/my-db", + "my-app", + "my-org", + "my-space", + "my-user", + }, + []string{"OK"}, + []string{"TIP"}, + )) + }) + }) + + It("allows the variable value to begin with a hyphen", func() { + runCommand("my-app", "MY_VAR", "--has-a-cool-value") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{ + "Setting env variable", + "MY_VAR", + "--has-a-cool-value", + }, + []string{"OK"}, + []string{"TIP"}, + )) + Expect(*appRepo.UpdateParams.EnvironmentVars).To(Equal(map[string]interface{}{ + "MY_VAR": "--has-a-cool-value", + "foo": "bar", + })) + }) + + Context("when setting fails", func() { + BeforeEach(func() { + appRepo.UpdateErr = true + }) + + It("tells the user", func() { + runCommand("please", "dont", "fail") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting env variable"}, + []string{"FAILED"}, + []string{"Error updating app."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/application/start.go b/cf/commands/application/start.go new file mode 100644 index 00000000000..296fc37d5fa --- /dev/null +++ b/cf/commands/application/start.go @@ -0,0 +1,336 @@ +package application + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + "time" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/app_instances" + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/loggregatorlib/logmessage" + "github.com/codegangsta/cli" +) + +const ( + DefaultStagingTimeout = 15 * time.Minute + DefaultStartupTimeout = 5 * time.Minute + DefaultPingerThrottle = 5 * time.Second +) + +const LogMessageTypeStaging = "STG" + +type Start struct { + ui terminal.UI + config core_config.Reader + appDisplayer ApplicationDisplayer + appReq requirements.ApplicationRequirement + appRepo applications.ApplicationRepository + appInstancesRepo app_instances.AppInstancesRepository + logRepo api.LogsRepository + + StartupTimeout time.Duration + StagingTimeout time.Duration + PingerThrottle time.Duration +} + +type ApplicationStarter interface { + SetStartTimeoutInSeconds(timeout int) + ApplicationStart(app models.Application, orgName string, spaceName string) (updatedApp models.Application, err error) +} + +type ApplicationStagingWatcher interface { + ApplicationWatchStaging(app models.Application, orgName string, spaceName string, startCommand func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error) +} + +func NewStart(ui terminal.UI, config core_config.Reader, appDisplayer ApplicationDisplayer, appRepo applications.ApplicationRepository, appInstancesRepo app_instances.AppInstancesRepository, logRepo api.LogsRepository) (cmd *Start) { + cmd = new(Start) + cmd.ui = ui + cmd.config = config + cmd.appDisplayer = appDisplayer + cmd.appRepo = appRepo + cmd.appInstancesRepo = appInstancesRepo + cmd.logRepo = logRepo + + cmd.PingerThrottle = DefaultPingerThrottle + + if os.Getenv("CF_STAGING_TIMEOUT") != "" { + duration, err := strconv.ParseInt(os.Getenv("CF_STAGING_TIMEOUT"), 10, 64) + if err != nil { + cmd.ui.Failed(T("invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + map[string]interface{}{"Err": err})) + } + cmd.StagingTimeout = time.Duration(duration) * time.Minute + } else { + cmd.StagingTimeout = DefaultStagingTimeout + } + + if os.Getenv("CF_STARTUP_TIMEOUT") != "" { + duration, err := strconv.ParseInt(os.Getenv("CF_STARTUP_TIMEOUT"), 10, 64) + if err != nil { + cmd.ui.Failed(T("invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + map[string]interface{}{"Err": err})) + } + cmd.StartupTimeout = time.Duration(duration) * time.Minute + } else { + cmd.StartupTimeout = DefaultStartupTimeout + } + + return +} + +func (cmd *Start) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "start", + ShortName: "st", + Description: T("Start an app"), + Usage: T("CF_NAME start APP"), + } +} + +func (cmd *Start) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq} + return +} + +func (cmd *Start) Run(c *cli.Context) { + cmd.ApplicationStart(cmd.appReq.GetApplication(), cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) +} + +func (cmd *Start) ApplicationStart(app models.Application, orgName, spaceName string) (updatedApp models.Application, err error) { + if app.State == "started" { + cmd.ui.Say(terminal.WarningColor(T("App ") + app.Name + T(" is already started"))) + return + } + + return cmd.ApplicationWatchStaging(app, orgName, spaceName, func(app models.Application) (models.Application, error) { + cmd.ui.Say(T("Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(orgName), + "SpaceName": terminal.EntityNameColor(spaceName), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) + + state := "STARTED" + return cmd.appRepo.Update(app.Guid, models.AppParams{State: &state}) + }) +} + +func (cmd *Start) ApplicationWatchStaging(app models.Application, orgName, spaceName string, start func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error) { + stopLoggingChan := make(chan bool, 1) + loggingStartedChan := make(chan bool) + doneLoggingChan := make(chan bool) + + go cmd.tailStagingLogs(app, loggingStartedChan, doneLoggingChan) + go func() { + <-stopLoggingChan + cmd.logRepo.Close() + }() + <-loggingStartedChan // block until we have established connection to Loggregator + + updatedApp, apiErr := start(app) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.waitForInstancesToStage(updatedApp) + stopLoggingChan <- true + <-doneLoggingChan + + cmd.ui.Say("") + + cmd.waitForOneRunningInstance(updatedApp) + cmd.ui.Say(terminal.HeaderColor(T("\nApp started\n"))) + cmd.ui.Say("") + cmd.ui.Ok() + + //detectedstartcommand on first push is not present until starting completes + startedApp, apiErr := cmd.appRepo.Read(updatedApp.Name) + if err != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + var appStartCommand string + if app.Command == "" { + appStartCommand = startedApp.DetectedStartCommand + } else { + appStartCommand = startedApp.Command + } + + cmd.ui.Say(T("\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(startedApp.Name), + "Command": appStartCommand, + })) + + cmd.appDisplayer.ShowApp(startedApp, orgName, spaceName) + return +} + +func (cmd *Start) SetStartTimeoutInSeconds(timeout int) { + cmd.StartupTimeout = time.Duration(timeout) * time.Second +} + +func simpleLogMessageOutput(logMsg *logmessage.LogMessage) (msgText string) { + msgText = string(logMsg.GetMessage()) + reg, err := regexp.Compile("[\n\r]+$") + if err != nil { + return + } + msgText = reg.ReplaceAllString(msgText, "") + return +} + +func (cmd Start) tailStagingLogs(app models.Application, startChan, doneChan chan bool) { + onConnect := func() { + startChan <- true + } + + err := cmd.logRepo.TailLogsFor(app.Guid, onConnect, func(msg *logmessage.LogMessage) { + if msg.GetSourceName() == LogMessageTypeStaging { + cmd.ui.Say(simpleLogMessageOutput(msg)) + } + }) + + if err != nil { + cmd.ui.Warn(T("Warning: error tailing logs")) + cmd.ui.Say("%s", err) + startChan <- true + } + + close(doneChan) +} + +func isStagingError(err error) bool { + httpError, ok := err.(errors.HttpError) + return ok && httpError.ErrorCode() == errors.APP_NOT_STAGED +} + +func (cmd Start) waitForInstancesToStage(app models.Application) { + stagingStartTime := time.Now() + _, err := cmd.appInstancesRepo.GetInstances(app.Guid) + + for isStagingError(err) && time.Since(stagingStartTime) < cmd.StagingTimeout { + cmd.ui.Wait(cmd.PingerThrottle) + _, err = cmd.appInstancesRepo.GetInstances(app.Guid) + } + + if err != nil && !isStagingError(err) { + cmd.ui.Say("") + cmd.ui.Failed(T("{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + map[string]interface{}{ + "Err": err.Error(), + "Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))})) + } + + return +} + +func (cmd Start) waitForOneRunningInstance(app models.Application) { + startupStartTime := time.Now() + + for { + if time.Since(startupStartTime) > cmd.StartupTimeout { + cmd.ui.Failed(fmt.Sprintf(T("Start app timeout\n\nTIP: use '{{.Command}}' for more information", + map[string]interface{}{ + "Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))}))) + return + } + + count, err := cmd.fetchInstanceCount(app.Guid) + if err != nil { + cmd.ui.Wait(cmd.PingerThrottle) + continue + } + + cmd.ui.Say(instancesDetails(count)) + + if count.running > 0 { + return + } + + if count.flapping > 0 { + cmd.ui.Failed(fmt.Sprintf(T("Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + map[string]interface{}{"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))}))) + return + } + + cmd.ui.Wait(cmd.PingerThrottle) + } +} + +type instanceCount struct { + running int + starting int + flapping int + down int + total int +} + +func (cmd Start) fetchInstanceCount(appGuid string) (instanceCount, error) { + count := instanceCount{} + + instances, apiErr := cmd.appInstancesRepo.GetInstances(appGuid) + if apiErr != nil { + return instanceCount{}, apiErr + } + + count.total = len(instances) + + for _, inst := range instances { + switch inst.State { + case models.InstanceRunning: + count.running++ + case models.InstanceStarting: + count.starting++ + case models.InstanceFlapping: + count.flapping++ + case models.InstanceDown: + count.down++ + } + } + + return count, nil +} + +func instancesDetails(count instanceCount) string { + details := []string{fmt.Sprintf(T("{{.RunningCount}} of {{.TotalCount}} instances running", + map[string]interface{}{"RunningCount": count.running, "TotalCount": count.total}))} + + if count.starting > 0 { + details = append(details, fmt.Sprintf(T("{{.StartingCount}} starting", + map[string]interface{}{"StartingCount": count.starting}))) + } + + if count.down > 0 { + details = append(details, fmt.Sprintf(T("{{.DownCount}} down", + map[string]interface{}{"DownCount": count.down}))) + } + + if count.flapping > 0 { + details = append(details, fmt.Sprintf(T("{{.FlappingCount}} failing", + map[string]interface{}{"FlappingCount": count.flapping}))) + } + + return strings.Join(details, ", ") +} diff --git a/cf/commands/application/start_test.go b/cf/commands/application/start_test.go new file mode 100644 index 00000000000..cb24684f85e --- /dev/null +++ b/cf/commands/application/start_test.go @@ -0,0 +1,499 @@ +package application_test + +import ( + "os" + "time" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/app_instances" + testAppInstanaces "github.com/cloudfoundry/cli/cf/api/app_instances/fakes" + "github.com/cloudfoundry/cli/cf/api/applications" + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/application" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testlogs "github.com/cloudfoundry/cli/testhelpers/logs" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + "github.com/cloudfoundry/loggregatorlib/logmessage" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("start command", func() { + var ( + ui *testterm.FakeUI + defaultAppForStart = models.Application{} + defaultInstanceResponses = [][]models.AppInstanceFields{} + defaultInstanceErrorCodes = []string{"", ""} + requirementsFactory *testreq.FakeReqFactory + logsForTail []*logmessage.LogMessage + logRepo *testapi.FakeLogsRepository + ) + + getInstance := func(appGuid string) (instances []models.AppInstanceFields, apiErr error) { + if len(defaultInstanceResponses) > 0 { + instances = defaultInstanceResponses[0] + if len(defaultInstanceResponses) > 1 { + defaultInstanceResponses = defaultInstanceResponses[1:] + } + } + if len(defaultInstanceErrorCodes) > 0 { + errorCode := defaultInstanceErrorCodes[0] + if len(defaultInstanceErrorCodes) > 1 { + defaultInstanceErrorCodes = defaultInstanceErrorCodes[1:] + } + if errorCode != "" { + apiErr = errors.NewHttpError(400, errorCode, "Error staging app") + } + } + return + } + + BeforeEach(func() { + ui = new(testterm.FakeUI) + requirementsFactory = &testreq.FakeReqFactory{} + + defaultAppForStart.Name = "my-app" + defaultAppForStart.Guid = "my-app-guid" + defaultAppForStart.InstanceCount = 2 + + domain := models.DomainFields{} + domain.Name = "example.com" + + route := models.RouteSummary{} + route.Host = "my-app" + route.Domain = domain + + defaultAppForStart.Routes = []models.RouteSummary{route} + + instance1 := models.AppInstanceFields{} + instance1.State = models.InstanceStarting + + instance2 := models.AppInstanceFields{} + instance2.State = models.InstanceStarting + + instance3 := models.AppInstanceFields{} + instance3.State = models.InstanceRunning + + instance4 := models.AppInstanceFields{} + instance4.State = models.InstanceStarting + + defaultInstanceResponses = [][]models.AppInstanceFields{ + []models.AppInstanceFields{instance1, instance2}, + []models.AppInstanceFields{instance1, instance2}, + []models.AppInstanceFields{instance3, instance4}, + } + + logsForTail = []*logmessage.LogMessage{} + logRepo = new(testapi.FakeLogsRepository) + logRepo.TailLogsForStub = func(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error { + onConnect() + for _, log := range logsForTail { + onMessage(log) + } + return nil + } + }) + + callStart := func(args []string, config core_config.Reader, requirementsFactory *testreq.FakeReqFactory, displayApp ApplicationDisplayer, appRepo applications.ApplicationRepository, appInstancesRepo app_instances.AppInstancesRepository, logRepo api.LogsRepository) (ui *testterm.FakeUI) { + ui = new(testterm.FakeUI) + + cmd := NewStart(ui, config, displayApp, appRepo, appInstancesRepo, logRepo) + cmd.StagingTimeout = 100 * time.Millisecond + cmd.StartupTimeout = 200 * time.Millisecond + cmd.PingerThrottle = 50 * time.Millisecond + + testcmd.RunCommand(cmd, args, requirementsFactory) + return + } + + startAppWithInstancesAndErrors := func(displayApp ApplicationDisplayer, app models.Application, requirementsFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI, appRepo *testApplication.FakeApplicationRepository, appInstancesRepo *testAppInstanaces.FakeAppInstancesRepository) { + configRepo := testconfig.NewRepositoryWithDefaults() + appRepo = &testApplication.FakeApplicationRepository{ + UpdateAppResult: app, + } + appRepo.ReadReturns.App = app + appInstancesRepo = &testAppInstanaces.FakeAppInstancesRepository{} + appInstancesRepo.GetInstancesStub = getInstance + + logsForTail = []*logmessage.LogMessage{ + testlogs.NewLogMessage("Log Line 1", app.Guid, LogMessageTypeStaging, time.Now()), + testlogs.NewLogMessage("Log Line 2", app.Guid, LogMessageTypeStaging, time.Now()), + } + + args := []string{"my-app"} + + requirementsFactory.Application = app + ui = callStart(args, configRepo, requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + return + } + + It("fails requirements when not logged in", func() { + requirementsFactory.LoginSuccess = false + cmd := NewStart(new(testterm.FakeUI), testconfig.NewRepository(), &testcmd.FakeAppDisplayer{}, &testApplication.FakeApplicationRepository{}, &testAppInstanaces.FakeAppInstancesRepository{}, &testapi.FakeLogsRepository{}) + + Expect(testcmd.RunCommand(cmd, []string{"some-app-name"}, requirementsFactory)).To(BeFalse()) + }) + + Describe("timeouts", func() { + It("has sane default timeout values", func() { + cmd := NewStart(new(testterm.FakeUI), testconfig.NewRepository(), &testcmd.FakeAppDisplayer{}, &testApplication.FakeApplicationRepository{}, &testAppInstanaces.FakeAppInstancesRepository{}, &testapi.FakeLogsRepository{}) + Expect(cmd.StagingTimeout).To(Equal(15 * time.Minute)) + Expect(cmd.StartupTimeout).To(Equal(5 * time.Minute)) + }) + + It("can read timeout values from environment variables", func() { + oldStaging := os.Getenv("CF_STAGING_TIMEOUT") + oldStart := os.Getenv("CF_STARTUP_TIMEOUT") + defer func() { + os.Setenv("CF_STAGING_TIMEOUT", oldStaging) + os.Setenv("CF_STARTUP_TIMEOUT", oldStart) + }() + + os.Setenv("CF_STAGING_TIMEOUT", "6") + os.Setenv("CF_STARTUP_TIMEOUT", "3") + cmd := NewStart(new(testterm.FakeUI), testconfig.NewRepository(), &testcmd.FakeAppDisplayer{}, &testApplication.FakeApplicationRepository{}, &testAppInstanaces.FakeAppInstancesRepository{}, &testapi.FakeLogsRepository{}) + Expect(cmd.StagingTimeout).To(Equal(6 * time.Minute)) + Expect(cmd.StartupTimeout).To(Equal(3 * time.Minute)) + }) + + Describe("when the staging timeout is zero seconds", func() { + var ( + app models.Application + cmd *Start + ) + + BeforeEach(func() { + app = defaultAppForStart + + instances := []models.AppInstanceFields{models.AppInstanceFields{}} + appRepo := &testApplication.FakeApplicationRepository{ + UpdateAppResult: app, + } + appRepo.ReadReturns.App = app + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + appInstancesRepo.GetInstancesReturns(instances, errors.New("Error staging app")) + + requirementsFactory.LoginSuccess = true + requirementsFactory.Application = app + config := testconfig.NewRepository() + displayApp := &testcmd.FakeAppDisplayer{} + + cmd = NewStart(ui, config, displayApp, appRepo, appInstancesRepo, logRepo) + cmd.StagingTimeout = 1 + cmd.PingerThrottle = 1 + cmd.StartupTimeout = 1 + }) + + It("can still respond to staging failures", func() { + testcmd.RunCommand(cmd, []string{"my-app"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app"}, + []string{"FAILED"}, + []string{"Error staging app"}, + )) + }) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails with usage when not provided exactly one arg", func() { + config := testconfig.NewRepository() + displayApp := &testcmd.FakeAppDisplayer{} + appRepo := &testApplication.FakeApplicationRepository{} + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + logRepo := &testapi.FakeLogsRepository{} + + ui := callStart([]string{}, config, requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("uses uses proper org name and space name", func() { + config := testconfig.NewRepositoryWithDefaults() + displayApp := &testcmd.FakeAppDisplayer{} + appRepo := &testApplication.FakeApplicationRepository{} + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + + appRepo.ReadReturns.App = defaultAppForStart + appInstancesRepo = &testAppInstanaces.FakeAppInstancesRepository{} + appInstancesRepo.GetInstancesStub = getInstance + + cmd := NewStart(ui, config, displayApp, appRepo, appInstancesRepo, logRepo) + cmd.StagingTimeout = 100 * time.Millisecond + cmd.StartupTimeout = 200 * time.Millisecond + cmd.PingerThrottle = 50 * time.Millisecond + cmd.ApplicationStart(defaultAppForStart, "some-org", "some-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app", "some-org", "some-space", "my-user"}, + []string{"OK"}, + )) + }) + + It("starts an app, when given the app's name", func() { + displayApp := &testcmd.FakeAppDisplayer{} + ui, appRepo, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"0 of 2 instances running", "2 starting"}, + []string{"started"}, + )) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + Expect(displayApp.AppToDisplay).To(Equal(defaultAppForStart)) + }) + + It("displays the command start command instead of the detected start command when set", func() { + defaultAppForStart.Command = "command start command" + defaultAppForStart.DetectedStartCommand = "detected start command" + displayApp := &testcmd.FakeAppDisplayer{} + ui, appRepo, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(appRepo.ReadCalls).To(Equal(1)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"App my-app was started using this command `command start command`"}, + )) + }) + + It("displays the detected start command when no other command is set", func() { + defaultAppForStart.DetectedStartCommand = "detected start command" + defaultAppForStart.Command = "" + displayApp := &testcmd.FakeAppDisplayer{} + ui, appRepo, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(appRepo.ReadCalls).To(Equal(1)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"App my-app was started using this command `detected start command`"}, + )) + }) + + It("only displays staging logs when an app is starting", func() { + displayApp := &testcmd.FakeAppDisplayer{} + requirementsFactory.Application = defaultAppForStart + appRepo := &testApplication.FakeApplicationRepository{ + UpdateAppResult: defaultAppForStart, + } + appRepo.ReadReturns.App = defaultAppForStart + + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + + currentTime := time.Now() + wrongSourceName := "DEA" + correctSourceName := "STG" + + logsForTail = []*logmessage.LogMessage{ + testlogs.NewLogMessage("Log Line 1", defaultAppForStart.Guid, wrongSourceName, currentTime), + testlogs.NewLogMessage("Log Line 2", defaultAppForStart.Guid, correctSourceName, currentTime), + testlogs.NewLogMessage("Log Line 3", defaultAppForStart.Guid, correctSourceName, currentTime), + testlogs.NewLogMessage("Log Line 4", defaultAppForStart.Guid, wrongSourceName, currentTime), + } + + ui := callStart([]string{"my-app"}, testconfig.NewRepository(), requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Log Line 2"}, + []string{"Log Line 3"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"Log Line 1"}, + []string{"Log Line 4"}, + )) + }) + + It("gracefully handles starting an app that is still staging", func() { + displayApp := &testcmd.FakeAppDisplayer{} + + logRepoClosed := make(chan struct{}) + + logRepo.TailLogsForStub = func(appGuid string, onConnect func(), onMessage func(*logmessage.LogMessage)) error { + onConnect() + onMessage(testlogs.NewLogMessage("Before close", appGuid, LogMessageTypeStaging, time.Now())) + + <-logRepoClosed + + time.Sleep(50 * time.Millisecond) + onMessage(testlogs.NewLogMessage("After close 1", appGuid, LogMessageTypeStaging, time.Now())) + onMessage(testlogs.NewLogMessage("After close 2", appGuid, LogMessageTypeStaging, time.Now())) + + return nil + } + + logRepo.CloseStub = func() { + close(logRepoClosed) + } + + defaultInstanceResponses = [][]models.AppInstanceFields{ + []models.AppInstanceFields{}, + []models.AppInstanceFields{}, + []models.AppInstanceFields{{State: models.InstanceDown}, {State: models.InstanceStarting}}, + []models.AppInstanceFields{{State: models.InstanceStarting}, {State: models.InstanceStarting}}, + []models.AppInstanceFields{{State: models.InstanceRunning}, {State: models.InstanceRunning}}, + } + + defaultInstanceErrorCodes = []string{errors.APP_NOT_STAGED, errors.APP_NOT_STAGED, "", "", ""} + + ui, _, appInstancesRepo := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(appInstancesRepo.GetInstancesArgsForCall(0)).To(Equal("my-app-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Before close"}, + []string{"After close 1"}, + []string{"After close 2"}, + []string{"0 of 2 instances running", "2 starting"}, + )) + }) + + It("displays an error message when staging fails", func() { + displayApp := &testcmd.FakeAppDisplayer{} + defaultInstanceResponses = [][]models.AppInstanceFields{[]models.AppInstanceFields{}} + defaultInstanceErrorCodes = []string{"170001"} + + ui, _, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app"}, + []string{"FAILED"}, + []string{"Error staging app"}, + )) + }) + + Context("when an app instance is flapping", func() { + It("fails and alerts the user", func() { + displayApp := &testcmd.FakeAppDisplayer{} + appInstance := models.AppInstanceFields{} + appInstance.State = models.InstanceStarting + appInstance2 := models.AppInstanceFields{} + appInstance2.State = models.InstanceStarting + appInstance3 := models.AppInstanceFields{} + appInstance3.State = models.InstanceStarting + appInstance4 := models.AppInstanceFields{} + appInstance4.State = models.InstanceFlapping + defaultInstanceResponses = [][]models.AppInstanceFields{ + []models.AppInstanceFields{appInstance, appInstance2}, + []models.AppInstanceFields{appInstance3, appInstance4}, + } + + defaultInstanceErrorCodes = []string{"", ""} + + ui, _, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app"}, + []string{"0 of 2 instances running", "1 starting", "1 failing"}, + []string{"FAILED"}, + []string{"Start unsuccessful"}, + )) + }) + }) + + It("tells the user about the failure when waiting for the app to start times out", func() { + displayApp := &testcmd.FakeAppDisplayer{} + appInstance := models.AppInstanceFields{} + appInstance.State = models.InstanceStarting + appInstance2 := models.AppInstanceFields{} + appInstance2.State = models.InstanceStarting + appInstance3 := models.AppInstanceFields{} + appInstance3.State = models.InstanceStarting + appInstance4 := models.AppInstanceFields{} + appInstance4.State = models.InstanceDown + appInstance5 := models.AppInstanceFields{} + appInstance5.State = models.InstanceDown + appInstance6 := models.AppInstanceFields{} + appInstance6.State = models.InstanceDown + defaultInstanceResponses = [][]models.AppInstanceFields{ + []models.AppInstanceFields{appInstance, appInstance2}, + []models.AppInstanceFields{appInstance3, appInstance4}, + []models.AppInstanceFields{appInstance5, appInstance6}, + } + + defaultInstanceErrorCodes = []string{errors.APP_NOT_STAGED, errors.APP_NOT_STAGED, errors.APP_NOT_STAGED} + + ui, _, _ := startAppWithInstancesAndErrors(displayApp, defaultAppForStart, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Starting", "my-app"}, + []string{"FAILED"}, + []string{"Start app timeout"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"instances running"})) + }) + + It("tells the user about the failure when starting the app fails", func() { + config := testconfig.NewRepository() + displayApp := &testcmd.FakeAppDisplayer{} + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + appRepo := &testApplication.FakeApplicationRepository{UpdateErr: true} + appRepo.ReadReturns.App = app + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + args := []string{"my-app"} + requirementsFactory.Application = app + ui := callStart(args, config, requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"my-app"}, + []string{"FAILED"}, + []string{"Error updating app."}, + )) + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + }) + + It("warns the user when the app is already running", func() { + displayApp := &testcmd.FakeAppDisplayer{} + config := testconfig.NewRepository() + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + app.State = "started" + appRepo := &testApplication.FakeApplicationRepository{} + appRepo.ReadReturns.App = app + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + + requirementsFactory.Application = app + + args := []string{"my-app"} + ui := callStart(args, config, requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"my-app", "is already started"})) + + Expect(appRepo.UpdateAppGuid).To(Equal("")) + }) + + It("tells the user when connecting to the log server fails", func() { + configRepo := testconfig.NewRepositoryWithDefaults() + displayApp := &testcmd.FakeAppDisplayer{} + + appRepo := &testApplication.FakeApplicationRepository{} + appRepo.ReadReturns.App = defaultAppForStart + appInstancesRepo := &testAppInstanaces.FakeAppInstancesRepository{} + + logRepo.TailLogsForReturns(errors.New("Ooops")) + + requirementsFactory.Application = defaultAppForStart + + ui := callStart([]string{"my-app"}, configRepo, requirementsFactory, displayApp, appRepo, appInstancesRepo, logRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"error tailing logs"}, + []string{"Ooops"}, + )) + }) + }) +}) diff --git a/cf/commands/application/stop.go b/cf/commands/application/stop.go new file mode 100644 index 00000000000..8de7a9c1ab2 --- /dev/null +++ b/cf/commands/application/stop.go @@ -0,0 +1,89 @@ +package application + +import ( + "errors" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ApplicationStopper interface { + ApplicationStop(app models.Application, orgName string, spaceName string) (updatedApp models.Application, err error) +} + +type Stop struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + appReq requirements.ApplicationRequirement +} + +func NewStop(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository) (cmd *Stop) { + cmd = new(Stop) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + + return +} + +func (cmd *Stop) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "stop", + ShortName: "sp", + Description: T("Stop an app"), + Usage: T("CF_NAME stop APP"), + } +} + +func (cmd *Stop) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq} + return +} + +func (cmd *Stop) ApplicationStop(app models.Application, orgName, spaceName string) (updatedApp models.Application, err error) { + if app.State == "stopped" { + updatedApp = app + return + } + + cmd.ui.Say(T("Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(orgName), + "SpaceName": terminal.EntityNameColor(spaceName), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) + + state := "STOPPED" + updatedApp, apiErr := cmd.appRepo.Update(app.Guid, models.AppParams{State: &state}) + if apiErr != nil { + err = errors.New(apiErr.Error()) + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + return +} + +func (cmd *Stop) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + if app.State == "stopped" { + cmd.ui.Say(terminal.WarningColor(T("App ") + app.Name + T(" is already stopped"))) + } else { + cmd.ApplicationStop(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) + } +} diff --git a/cf/commands/application/stop_test.go b/cf/commands/application/stop_test.go new file mode 100644 index 00000000000..d16c93ad553 --- /dev/null +++ b/cf/commands/application/stop_test.go @@ -0,0 +1,127 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("stop command", func() { + var ( + ui *testterm.FakeUI + app models.Application + appRepo *testApplication.FakeApplicationRepository + requirementsFactory *testreq.FakeReqFactory + config core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + appRepo = &testApplication.FakeApplicationRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewStop(ui, config, appRepo), args, requirementsFactory) + } + + It("fails requirements when not logged in", func() { + Expect(runCommand("some-app-name")).To(BeFalse()) + }) + + Context("when logged in and an app exists", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + app = models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + app.State = "started" + }) + + JustBeforeEach(func() { + appRepo.ReadReturns.App = app + requirementsFactory.Application = app + }) + + It("fails with usage when the app name is not given", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("stops the app with the given name", func() { + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Stopping app", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + }) + + It("warns the user when stopping the app fails", func() { + appRepo.UpdateErr = true + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Stopping", "my-app"}, + []string{"FAILED"}, + []string{"Error updating app."}, + )) + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + }) + + Context("when the app is stopped", func() { + BeforeEach(func() { + app.State = "stopped" + }) + + It("warns the user when the app is already stopped", func() { + runCommand("my-app") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"my-app", "is already stopped"})) + Expect(appRepo.UpdateAppGuid).To(Equal("")) + }) + }) + + Describe(".ApplicationStop()", func() { + It("returns the updated app model from ApplicationStop()", func() { + expectedStoppedApp := app + expectedStoppedApp.State = "stopped" + + appRepo.UpdateAppResult = expectedStoppedApp + stopper := NewStop(ui, config, appRepo) + actualStoppedApp, err := stopper.ApplicationStop(app, config.OrganizationFields().Name, config.SpaceFields().Name) + + Expect(err).NotTo(HaveOccurred()) + Expect(expectedStoppedApp).To(Equal(actualStoppedApp)) + }) + + Context("When the app is already stopped", func() { + BeforeEach(func() { + app.State = "stopped" + }) + + It("returns the app without updating it", func() { + stopper := NewStop(ui, config, appRepo) + updatedApp, err := stopper.ApplicationStop(app, config.OrganizationFields().Name, config.SpaceFields().Name) + + Expect(err).NotTo(HaveOccurred()) + Expect(app).To(Equal(updatedApp)) + }) + }) + }) + }) +}) diff --git a/cf/commands/application/unset_env.go b/cf/commands/application/unset_env.go new file mode 100644 index 00000000000..66ed2abe58c --- /dev/null +++ b/cf/commands/application/unset_env.go @@ -0,0 +1,83 @@ +package application + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnsetEnv struct { + ui terminal.UI + config core_config.Reader + appRepo applications.ApplicationRepository + appReq requirements.ApplicationRequirement +} + +func NewUnsetEnv(ui terminal.UI, config core_config.Reader, appRepo applications.ApplicationRepository) (cmd *UnsetEnv) { + cmd = new(UnsetEnv) + cmd.ui = ui + cmd.config = config + cmd.appRepo = appRepo + return +} + +func (cmd *UnsetEnv) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unset-env", + Description: T("Remove an env variable"), + Usage: T("CF_NAME unset-env APP NAME"), + } +} + +func (cmd *UnsetEnv) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.appReq, + } + return +} + +func (cmd *UnsetEnv) Run(c *cli.Context) { + varName := c.Args()[1] + app := cmd.appReq.GetApplication() + + cmd.ui.Say(T("Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "VarName": terminal.EntityNameColor(varName), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) + + envParams := app.EnvironmentVars + + if _, ok := envParams[varName]; !ok { + cmd.ui.Ok() + cmd.ui.Warn(T("Env variable {{.VarName}} was not set.", map[string]interface{}{"VarName": varName})) + return + } + + delete(envParams, varName) + + _, apiErr := cmd.appRepo.Update(app.Guid, models.AppParams{EnvironmentVars: &envParams}) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say(T("TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + map[string]interface{}{"Command": terminal.CommandColor(cf.Name() + " restage")})) +} diff --git a/cf/commands/application/unset_env_test.go b/cf/commands/application/unset_env_test.go new file mode 100644 index 00000000000..39474a5e52b --- /dev/null +++ b/cf/commands/application/unset_env_test.go @@ -0,0 +1,116 @@ +package application_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/application" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("unset-env command", func() { + var ( + ui *testterm.FakeUI + app models.Application + appRepo *testApplication.FakeApplicationRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + app = models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + appRepo = &testApplication.FakeApplicationRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewUnsetEnv(ui, configRepo, appRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedSpaceSuccess = true + requirementsFactory.Application = app + + Expect(runCommand("foo", "bar")).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.Application = app + + Expect(runCommand("foo", "bar")).To(BeFalse()) + }) + + It("fails with usage when not provided with exactly 2 args", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + requirementsFactory.Application = app + + Expect(runCommand("too", "many", "args")).To(BeFalse()) + }) + }) + + Context("when logged in, a space is targeted and provided enough args", func() { + BeforeEach(func() { + app.EnvironmentVars = map[string]interface{}{"foo": "bar", "DATABASE_URL": "mysql://example.com/my-db"} + + requirementsFactory.Application = app + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + }) + + It("updates the app and tells the user what happened", func() { + runCommand("my-app", "DATABASE_URL") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing env variable", "DATABASE_URL", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(appRepo.UpdateAppGuid).To(Equal("my-app-guid")) + Expect(*appRepo.UpdateParams.EnvironmentVars).To(Equal(map[string]interface{}{ + "foo": "bar", + })) + }) + + Context("when updating the app fails", func() { + BeforeEach(func() { + appRepo.UpdateErr = true + appRepo.ReadReturns.App = app + }) + + It("fails and alerts the user", func() { + runCommand("does-not-exist", "DATABASE_URL") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing env variable"}, + []string{"FAILED"}, + []string{"Error updating app."}, + )) + }) + }) + + It("tells the user if the specified env var was not set", func() { + runCommand("my-app", "CANT_STOP_WONT_STOP_UNSETTIN_THIS_ENV") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing env variable"}, + []string{"OK"}, + []string{"CANT_STOP_WONT_STOP_UNSETTIN_THIS_ENV", "was not set."}, + )) + }) + }) +}) diff --git a/cf/commands/auth.go b/cf/commands/auth.go new file mode 100644 index 00000000000..be3abea14fb --- /dev/null +++ b/cf/commands/auth.go @@ -0,0 +1,63 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Authenticate struct { + ui terminal.UI + config core_config.ReadWriter + authenticator authentication.AuthenticationRepository +} + +func NewAuthenticate(ui terminal.UI, config core_config.ReadWriter, authenticator authentication.AuthenticationRepository) (cmd Authenticate) { + cmd.ui = ui + cmd.config = config + cmd.authenticator = authenticator + return +} + +func (cmd Authenticate) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "auth", + Description: T("Authenticate user non-interactively"), + Usage: T("CF_NAME auth USERNAME PASSWORD\n\n") + + terminal.WarningColor(T("WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n")) + T("EXAMPLE:\n") + T(" CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n") + T(" CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)"), + } +} + +func (cmd Authenticate) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewApiEndpointRequirement()) + return +} + +func (cmd Authenticate) Run(c *cli.Context) { + cmd.config.ClearSession() + cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL() + + cmd.ui.Say(T("API endpoint: {{.ApiEndpoint}}", + map[string]interface{}{"ApiEndpoint": terminal.EntityNameColor(cmd.config.ApiEndpoint())})) + cmd.ui.Say(T("Authenticating...")) + + apiErr := cmd.authenticator.Authenticate(map[string]string{"username": c.Args()[0], "password": c.Args()[1]}) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say(T("Use '{{.Name}}' to view or set your target org and space", + map[string]interface{}{"Name": terminal.CommandColor(cf.Name() + " target")})) + return +} diff --git a/cf/commands/auth_test.go b/cf/commands/auth_test.go new file mode 100644 index 00000000000..43be8177b70 --- /dev/null +++ b/cf/commands/auth_test.go @@ -0,0 +1,104 @@ +package commands_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("auth command", func() { + var ( + ui *testterm.FakeUI + cmd Authenticate + config core_config.ReadWriter + repo *testapi.FakeAuthenticationRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + repo = &testapi.FakeAuthenticationRepository{ + Config: config, + AccessToken: "my-access-token", + RefreshToken: "my-refresh-token", + } + cmd = NewAuthenticate(ui, config, repo) + }) + + Describe("requirements", func() { + It("fails with usage when given too few arguments", func() { + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails if the user has not set an api endpoint", func() { + Expect(testcmd.RunCommand(cmd, []string{"username", "password"}, requirementsFactory)).To(BeFalse()) + }) + }) + + Context("when an api endpoint is targeted", func() { + BeforeEach(func() { + requirementsFactory.ApiEndpointSuccess = true + config.SetApiEndpoint("foo.example.org/authenticate") + }) + + It("authenticates successfully", func() { + requirementsFactory.ApiEndpointSuccess = true + testcmd.RunCommand(cmd, []string{"foo@example.com", "password"}, requirementsFactory) + + Expect(ui.FailedWithUsage).To(BeFalse()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"foo.example.org/authenticate"}, + []string{"OK"}, + )) + + Expect(repo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "foo@example.com", + "password": "password", + }, + })) + }) + + It("gets the UAA endpoint and saves it to the config file", func() { + requirementsFactory.ApiEndpointSuccess = true + testcmd.RunCommand(cmd, []string{"foo@example.com", "password"}, requirementsFactory) + Expect(repo.GetLoginPromptsWasCalled).To(BeTrue()) + }) + + Describe("when authentication fails", func() { + BeforeEach(func() { + repo.AuthError = true + testcmd.RunCommand(cmd, []string{"username", "password"}, requirementsFactory) + }) + + It("does not prompt the user when provided username and password", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{config.ApiEndpoint()}, + []string{"Authenticating..."}, + []string{"FAILED"}, + []string{"Error authenticating"}, + )) + }) + + It("clears the user's session", func() { + Expect(config.AccessToken()).To(BeEmpty()) + Expect(config.RefreshToken()).To(BeEmpty()) + Expect(config.SpaceFields()).To(Equal(models.SpaceFields{})) + Expect(config.OrganizationFields()).To(Equal(models.OrganizationFields{})) + }) + }) + }) +}) diff --git a/cf/commands/buildpack/buildpack_suite_test.go b/cf/commands/buildpack/buildpack_suite_test.go new file mode 100644 index 00000000000..47b14cc28e8 --- /dev/null +++ b/cf/commands/buildpack/buildpack_suite_test.go @@ -0,0 +1,20 @@ +package buildpack_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestBuildpack(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Buildpack Suite") +} diff --git a/cf/commands/buildpack/buildpacks.go b/cf/commands/buildpack/buildpacks.go new file mode 100644 index 00000000000..157861f32b6 --- /dev/null +++ b/cf/commands/buildpack/buildpacks.go @@ -0,0 +1,82 @@ +package buildpack + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "strconv" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListBuildpacks struct { + ui terminal.UI + buildpackRepo api.BuildpackRepository +} + +func NewListBuildpacks(ui terminal.UI, buildpackRepo api.BuildpackRepository) (cmd ListBuildpacks) { + cmd.ui = ui + cmd.buildpackRepo = buildpackRepo + return +} + +func (cmd ListBuildpacks) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "buildpacks", + Description: T("List all buildpacks"), + Usage: T("CF_NAME buildpacks"), + } +} + +func (cmd ListBuildpacks) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd ListBuildpacks) Run(c *cli.Context) { + cmd.ui.Say(T("Getting buildpacks...\n")) + + table := cmd.ui.Table([]string{"buildpack", T("position"), T("enabled"), T("locked"), T("filename")}) + noBuildpacks := true + + apiErr := cmd.buildpackRepo.ListBuildpacks(func(buildpack models.Buildpack) bool { + position := "" + if buildpack.Position != nil { + position = strconv.Itoa(*buildpack.Position) + } + enabled := "" + if buildpack.Enabled != nil { + enabled = strconv.FormatBool(*buildpack.Enabled) + } + locked := "" + if buildpack.Locked != nil { + locked = strconv.FormatBool(*buildpack.Locked) + } + table.Add( + buildpack.Name, + position, + enabled, + locked, + buildpack.Filename, + ) + noBuildpacks = false + return true + }) + table.Print() + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching buildpacks.\n{{.Error}}", map[string]interface{}{"Error": apiErr.Error()})) + } + + if noBuildpacks { + cmd.ui.Say(T("No buildpacks found")) + } +} diff --git a/cf/commands/buildpack/buildpacks_test.go b/cf/commands/buildpack/buildpacks_test.go new file mode 100644 index 00000000000..ac03b642e8d --- /dev/null +++ b/cf/commands/buildpack/buildpacks_test.go @@ -0,0 +1,82 @@ +package buildpack_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/commands/buildpack" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ListBuildpacks", func() { + var ( + ui *testterm.FakeUI + buildpackRepo *testapi.FakeBuildpackRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + buildpackRepo = &testapi.FakeBuildpackRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := buildpack.NewListBuildpacks(ui, buildpackRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when login fails", func() { + Expect(runCommand()).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("lists buildpacks", func() { + p1 := 5 + p2 := 10 + p3 := 15 + t := true + f := false + + buildpackRepo.Buildpacks = []models.Buildpack{ + models.Buildpack{Name: "Buildpack-1", Position: &p1, Enabled: &t, Locked: &f}, + models.Buildpack{Name: "Buildpack-2", Position: &p2, Enabled: &f, Locked: &t}, + models.Buildpack{Name: "Buildpack-3", Position: &p3, Enabled: &t, Locked: &f}, + } + + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting buildpacks"}, + []string{"buildpack", "position", "enabled"}, + []string{"Buildpack-1", "5", "true", "false"}, + []string{"Buildpack-2", "10", "false", "true"}, + []string{"Buildpack-3", "15", "true", "false"}, + )) + }) + + It("tells the user if no build packs exist", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting buildpacks"}, + []string{"No buildpacks found"}, + )) + }) + }) + +}) diff --git a/cf/commands/buildpack/create_buildpack.go b/cf/commands/buildpack/create_buildpack.go new file mode 100644 index 00000000000..c6ac19a747b --- /dev/null +++ b/cf/commands/buildpack/create_buildpack.go @@ -0,0 +1,118 @@ +package buildpack + +import ( + "strconv" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateBuildpack struct { + ui terminal.UI + buildpackRepo api.BuildpackRepository + buildpackBitsRepo api.BuildpackBitsRepository +} + +func NewCreateBuildpack(ui terminal.UI, buildpackRepo api.BuildpackRepository, buildpackBitsRepo api.BuildpackBitsRepository) (cmd CreateBuildpack) { + cmd.ui = ui + cmd.buildpackRepo = buildpackRepo + cmd.buildpackBitsRepo = buildpackBitsRepo + return +} + +func (cmd CreateBuildpack) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-buildpack", + Description: T("Create a buildpack"), + Usage: T("CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]") + + T("\n\nTIP:\n") + T(" Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest."), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "enable", Usage: T("Enable the buildpack to be used for staging")}, + cli.BoolFlag{Name: "disable", Usage: T("Disable the buildpack from being used for staging")}, + }, + } +} + +func (cmd CreateBuildpack) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd CreateBuildpack) Run(c *cli.Context) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + return + } + + buildpackName := c.Args()[0] + + cmd.ui.Say(T("Creating buildpack {{.BuildpackName}}...", map[string]interface{}{"BuildpackName": terminal.EntityNameColor(buildpackName)})) + + buildpack, err := cmd.createBuildpack(buildpackName, c) + + if err != nil { + if httpErr, ok := err.(errors.HttpError); ok && httpErr.ErrorCode() == errors.BUILDPACK_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Buildpack {{.BuildpackName}} already exists", map[string]interface{}{"BuildpackName": buildpackName})) + cmd.ui.Say(T("TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", map[string]interface{}{"CfUpdateBuildpackCommand": terminal.CommandColor(cf.Name() + " " + "update-buildpack")})) + } else { + cmd.ui.Failed(err.Error()) + } + return + } + cmd.ui.Ok() + cmd.ui.Say("") + + cmd.ui.Say(T("Uploading buildpack {{.BuildpackName}}...", map[string]interface{}{"BuildpackName": terminal.EntityNameColor(buildpackName)})) + + dir := c.Args()[1] + + err = cmd.buildpackBitsRepo.UploadBuildpack(buildpack, dir) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + cmd.ui.Ok() +} + +func (cmd CreateBuildpack) createBuildpack(buildpackName string, c *cli.Context) (buildpack models.Buildpack, apiErr error) { + position, err := strconv.Atoi(c.Args()[2]) + if err != nil { + apiErr = errors.NewWithFmt(T("Invalid position. {{.ErrorDescription}}", map[string]interface{}{"ErrorDescription": err.Error()})) + return + } + + enabled := c.Bool("enable") + disabled := c.Bool("disable") + if enabled && disabled { + apiErr = errors.New(T("Cannot specify both {{.Enabled}} and {{.Disabled}}.", map[string]interface{}{ + "Enabled": "enabled", + "Disabled": "disabled", + })) + return + } + + var enableOption *bool = nil + if enabled { + enableOption = &enabled + } + if disabled { + disabled = false + enableOption = &disabled + } + + buildpack, apiErr = cmd.buildpackRepo.Create(buildpackName, &position, enableOption, nil) + + return +} diff --git a/cf/commands/buildpack/create_buildpack_test.go b/cf/commands/buildpack/create_buildpack_test.go new file mode 100644 index 00000000000..6ece176bfec --- /dev/null +++ b/cf/commands/buildpack/create_buildpack_test.go @@ -0,0 +1,92 @@ +package buildpack_test + +import ( + "github.com/cloudfoundry/cli/cf" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/buildpack" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-buildpack command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + repo *testapi.FakeBuildpackRepository + bitsRepo *testapi.FakeBuildpackBitsRepository + ui *testterm.FakeUI + cmd CreateBuildpack + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + repo = &testapi.FakeBuildpackRepository{} + bitsRepo = &testapi.FakeBuildpackBitsRepository{} + ui = &testterm.FakeUI{} + cmd = NewCreateBuildpack(ui, repo, bitsRepo) + }) + + It("fails requirements when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(testcmd.RunCommand(cmd, []string{"my-buildpack", "my-dir", "0"}, requirementsFactory)).To(BeFalse()) + }) + + It("fails with usage when given fewer than three arguments", func() { + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("creates and uploads buildpacks", func() { + testcmd.RunCommand(cmd, []string{"my-buildpack", "my.war", "5"}, requirementsFactory) + + Expect(repo.CreateBuildpack.Enabled).To(BeNil()) + Expect(ui.FailedWithUsage).To(BeFalse()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating buildpack", "my-buildpack"}, + []string{"OK"}, + []string{"Uploading buildpack", "my-buildpack"}, + []string{"OK"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("warns the user when the buildpack already exists", func() { + repo.CreateBuildpackExists = true + testcmd.RunCommand(cmd, []string{"my-buildpack", "my.war", "5"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating buildpack", "my-buildpack"}, + []string{"OK"}, + []string{"my-buildpack", "already exists"}, + []string{"TIP", "use", cf.Name(), "update-buildpack"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("enables the buildpack when given the --enabled flag", func() { + testcmd.RunCommand(cmd, []string{"--enable", "my-buildpack", "my.war", "5"}, requirementsFactory) + Expect(*repo.CreateBuildpack.Enabled).To(Equal(true)) + }) + + It("disables the buildpack when given the --disable flag", func() { + testcmd.RunCommand(cmd, []string{"--disable", "my-buildpack", "my.war", "5"}, requirementsFactory) + Expect(*repo.CreateBuildpack.Enabled).To(Equal(false)) + }) + + It("alerts the user when uploading the buildpack bits fails", func() { + bitsRepo.UploadBuildpackErr = true + testcmd.RunCommand(cmd, []string{"my-buildpack", "bogus/path", "5"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating buildpack", "my-buildpack"}, + []string{"OK"}, + []string{"Uploading buildpack"}, + []string{"FAILED"}, + )) + }) +}) diff --git a/cf/commands/buildpack/delete_buildpack.go b/cf/commands/buildpack/delete_buildpack.go new file mode 100644 index 00000000000..9f127431002 --- /dev/null +++ b/cf/commands/buildpack/delete_buildpack.go @@ -0,0 +1,86 @@ +package buildpack + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteBuildpack struct { + ui terminal.UI + buildpackRepo api.BuildpackRepository +} + +func NewDeleteBuildpack(ui terminal.UI, repo api.BuildpackRepository) (cmd *DeleteBuildpack) { + cmd = new(DeleteBuildpack) + cmd.ui = ui + cmd.buildpackRepo = repo + return +} + +func (cmd *DeleteBuildpack) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-buildpack", + Description: T("Delete a buildpack"), + Usage: T("CF_NAME delete-buildpack BUILDPACK [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteBuildpack) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + loginReq := requirementsFactory.NewLoginRequirement() + + reqs = []requirements.Requirement{ + loginReq, + } + + return +} + +func (cmd *DeleteBuildpack) Run(c *cli.Context) { + buildpackName := c.Args()[0] + + force := c.Bool("f") + + if !force { + answer := cmd.ui.ConfirmDelete("buildpack", buildpackName) + if !answer { + return + } + } + + cmd.ui.Say(T("Deleting buildpack {{.BuildpackName}}...", map[string]interface{}{"BuildpackName": terminal.EntityNameColor(buildpackName)})) + buildpack, apiErr := cmd.buildpackRepo.FindByName(buildpackName) + + switch apiErr.(type) { + case nil: //do nothing + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Buildpack {{.BuildpackName}} does not exist.", map[string]interface{}{"BuildpackName": buildpackName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + + } + + apiErr = cmd.buildpackRepo.Delete(buildpack.Guid) + if apiErr != nil { + cmd.ui.Failed(T("Error deleting buildpack {{.Name}}\n{{.Error}}", map[string]interface{}{ + "Name": terminal.EntityNameColor(buildpack.Name), + "Error": apiErr.Error(), + })) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/buildpack/delete_buildpack_test.go b/cf/commands/buildpack/delete_buildpack_test.go new file mode 100644 index 00000000000..fb67b9405ce --- /dev/null +++ b/cf/commands/buildpack/delete_buildpack_test.go @@ -0,0 +1,133 @@ +package buildpack_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/buildpack" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-buildpack command", func() { + var ( + ui *testterm.FakeUI + buildpackRepo *testapi.FakeBuildpackRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + buildpackRepo = &testapi.FakeBuildpackRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteBuildpack(ui, buildpackRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when the user is not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("-f", "my-buildpack")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when the buildpack exists", func() { + BeforeEach(func() { + buildpackRepo.FindByNameBuildpack = models.Buildpack{ + Name: "my-buildpack", + Guid: "my-buildpack-guid", + } + }) + + It("deletes the buildpack", func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + + runCommand("my-buildpack") + + Expect(buildpackRepo.DeleteBuildpackGuid).To(Equal("my-buildpack-guid")) + + Expect(ui.Prompts).To(ContainSubstrings([]string{"delete the buildpack my-buildpack"})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting buildpack", "my-buildpack"}, + []string{"OK"}, + )) + }) + + Context("when the force flag is provided", func() { + It("does not prompt the user to delete the buildback", func() { + runCommand("-f", "my-buildpack") + + Expect(buildpackRepo.DeleteBuildpackGuid).To(Equal("my-buildpack-guid")) + + Expect(len(ui.Prompts)).To(Equal(0)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting buildpack", "my-buildpack"}, + []string{"OK"}, + )) + }) + }) + }) + + Context("when the buildpack provided is not found", func() { + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + buildpackRepo.FindByNameNotFound = true + }) + + It("warns the user", func() { + runCommand("my-buildpack") + + Expect(buildpackRepo.FindByNameName).To(Equal("my-buildpack")) + Expect(buildpackRepo.FindByNameNotFound).To(BeTrue()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "my-buildpack"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"my-buildpack", "does not exist"})) + }) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + + buildpackRepo.FindByNameBuildpack = models.Buildpack{ + Name: "my-buildpack", + Guid: "my-buildpack-guid", + } + buildpackRepo.DeleteApiResponse = errors.New("failed badly") + }) + + It("fails with the error", func() { + runCommand("my-buildpack") + + Expect(buildpackRepo.DeleteBuildpackGuid).To(Equal("my-buildpack-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting buildpack", "my-buildpack"}, + []string{"FAILED"}, + []string{"my-buildpack"}, + []string{"failed badly"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/buildpack/rename_buildpack.go b/cf/commands/buildpack/rename_buildpack.go new file mode 100644 index 00000000000..a183c21b211 --- /dev/null +++ b/cf/commands/buildpack/rename_buildpack.go @@ -0,0 +1,63 @@ +package buildpack + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameBuildpack struct { + ui terminal.UI + buildpackRepo api.BuildpackRepository +} + +func NewRenameBuildpack(ui terminal.UI, repo api.BuildpackRepository) (cmd *RenameBuildpack) { + cmd = new(RenameBuildpack) + cmd.ui = ui + cmd.buildpackRepo = repo + return +} + +func (cmd *RenameBuildpack) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename-buildpack", + Description: T("Rename a buildpack"), + Usage: T("CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME"), + } +} + +func (cmd *RenameBuildpack) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func (cmd *RenameBuildpack) Run(c *cli.Context) { + buildpackName := c.Args()[0] + newBuildpackName := c.Args()[1] + + cmd.ui.Say(T("Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", map[string]interface{}{"OldBuildpackName": terminal.EntityNameColor(buildpackName), "NewBuildpackName": terminal.EntityNameColor(newBuildpackName)})) + + buildpack, apiErr := cmd.buildpackRepo.FindByName(buildpackName) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + buildpack.Name = newBuildpackName + buildpack, apiErr = cmd.buildpackRepo.Update(buildpack) + if apiErr != nil { + cmd.ui.Failed(T("Error renaming buildpack {{.Name}}\n{{.Error}}", map[string]interface{}{ + "Name": terminal.EntityNameColor(buildpack.Name), + "Error": apiErr.Error(), + })) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/buildpack/rename_buildpack_test.go b/cf/commands/buildpack/rename_buildpack_test.go new file mode 100644 index 00000000000..74d7310b0d0 --- /dev/null +++ b/cf/commands/buildpack/rename_buildpack_test.go @@ -0,0 +1,85 @@ +package buildpack_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/buildpack" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("rename-buildpack command", func() { + var ( + cmd *RenameBuildpack + fakeRepo *testapi.FakeBuildpackRepository + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} + ui = new(testterm.FakeUI) + fakeRepo = &testapi.FakeBuildpackRepository{} + cmd = NewRenameBuildpack(ui, fakeRepo) + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails requirements when called without the current name and the new name to use", func() { + passed := runCommand("my-buildpack-name") + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("renames a buildpack", func() { + fakeRepo.FindByNameBuildpack = models.Buildpack{ + Name: "my-buildpack", + Guid: "my-buildpack-guid", + } + + runCommand("my-buildpack", "new-buildpack") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming buildpack", "my-buildpack"}, + []string{"OK"}, + )) + }) + + It("fails when the buildpack does not exist", func() { + fakeRepo.FindByNameNotFound = true + + runCommand("my-buildpack1", "new-buildpack") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming buildpack", "my-buildpack"}, + []string{"FAILED"}, + []string{"Buildpack my-buildpack1 not found"}, + )) + }) + + It("fails when there is an error updating the buildpack", func() { + fakeRepo.FindByNameBuildpack = models.Buildpack{ + Name: "my-buildpack", + Guid: "my-buildpack-guid", + } + fakeRepo.UpdateBuildpackReturns.Error = errors.New("SAD TROMBONE") + + runCommand("my-buildpack1", "new-buildpack") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming buildpack", "my-buildpack"}, + []string{"SAD TROMBONE"}, + )) + }) + }) +}) diff --git a/cf/commands/buildpack/update_buildpack.go b/cf/commands/buildpack/update_buildpack.go new file mode 100644 index 00000000000..e9fc7857913 --- /dev/null +++ b/cf/commands/buildpack/update_buildpack.go @@ -0,0 +1,137 @@ +package buildpack + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateBuildpack struct { + ui terminal.UI + buildpackRepo api.BuildpackRepository + buildpackBitsRepo api.BuildpackBitsRepository + buildpackReq requirements.BuildpackRequirement +} + +func NewUpdateBuildpack(ui terminal.UI, repo api.BuildpackRepository, bitsRepo api.BuildpackBitsRepository) (cmd *UpdateBuildpack) { + cmd = new(UpdateBuildpack) + cmd.ui = ui + cmd.buildpackRepo = repo + cmd.buildpackBitsRepo = bitsRepo + return +} + +func (cmd *UpdateBuildpack) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-buildpack", + Description: T("Update a buildpack"), + Usage: T("CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]") + + T("\n\nTIP:\n") + T(" Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest."), + Flags: []cli.Flag{ + flag_helpers.NewIntFlag("i", T("The order in which the buildpacks are checked during buildpack auto-detection")), + flag_helpers.NewStringFlag("p", T("Path to directory or zip file")), + cli.BoolFlag{Name: "enable", Usage: T("Enable the buildpack to be used for staging")}, + cli.BoolFlag{Name: "disable", Usage: T("Disable the buildpack from being used for staging")}, + cli.BoolFlag{Name: "lock", Usage: T("Lock the buildpack to prevent updates")}, + cli.BoolFlag{Name: "unlock", Usage: T("Unlock the buildpack to enable updates")}, + }, + } +} + +func (cmd *UpdateBuildpack) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + loginReq := requirementsFactory.NewLoginRequirement() + cmd.buildpackReq = requirementsFactory.NewBuildpackRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + loginReq, + cmd.buildpackReq, + } + + return +} + +func (cmd *UpdateBuildpack) Run(c *cli.Context) { + buildpack := cmd.buildpackReq.GetBuildpack() + + cmd.ui.Say(T("Updating buildpack {{.BuildpackName}}...", map[string]interface{}{"BuildpackName": terminal.EntityNameColor(buildpack.Name)})) + + updateBuildpack := false + + if c.IsSet("i") { + position := c.Int("i") + + buildpack.Position = &position + updateBuildpack = true + } + + enabled := c.Bool("enable") + disabled := c.Bool("disable") + if enabled && disabled { + cmd.ui.Failed(T("Cannot specify both {{.Enabled}} and {{.Disabled}}.", map[string]interface{}{ + "Enabled": "enabled", + "Disabled": "disabled", + })) + } + + if enabled { + buildpack.Enabled = &enabled + updateBuildpack = true + } + if disabled { + disabled = false + buildpack.Enabled = &disabled + updateBuildpack = true + } + + lock := c.Bool("lock") + unlock := c.Bool("unlock") + if lock && unlock { + cmd.ui.Failed(T("Cannot specify both lock and unlock options.")) + return + } + + dir := c.String("p") + if dir != "" && (lock || unlock) { + cmd.ui.Failed(T("Cannot specify buildpack bits and lock/unlock.")) + } + + if lock { + buildpack.Locked = &lock + updateBuildpack = true + } + if unlock { + unlock = false + buildpack.Locked = &unlock + updateBuildpack = true + } + + if updateBuildpack { + newBuildpack, apiErr := cmd.buildpackRepo.Update(buildpack) + if apiErr != nil { + cmd.ui.Failed(T("Error updating buildpack {{.Name}}\n{{.Error}}", map[string]interface{}{ + "Name": terminal.EntityNameColor(buildpack.Name), + "Error": apiErr.Error(), + })) + } + buildpack = newBuildpack + } + + if dir != "" { + apiErr := cmd.buildpackBitsRepo.UploadBuildpack(buildpack, dir) + if apiErr != nil { + cmd.ui.Failed(T("Error uploading buildpack {{.Name}}\n{{.Error}}", map[string]interface{}{ + "Name": terminal.EntityNameColor(buildpack.Name), + "Error": apiErr.Error(), + })) + } + } + cmd.ui.Ok() +} diff --git a/cf/commands/buildpack/update_buildpack_test.go b/cf/commands/buildpack/update_buildpack_test.go new file mode 100644 index 00000000000..048bbe86b31 --- /dev/null +++ b/cf/commands/buildpack/update_buildpack_test.go @@ -0,0 +1,166 @@ +package buildpack_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/buildpack" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func successfulUpdate(ui *testterm.FakeUI, buildpackName string) { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating buildpack", buildpackName}, + []string{"OK"}, + )) +} + +func failedUpdate(ui *testterm.FakeUI, buildpackName string) { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating buildpack", buildpackName}, + []string{"FAILED"}, + )) +} + +var _ = Describe("Updating buildpack command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + ui *testterm.FakeUI + repo *testapi.FakeBuildpackRepository + bitsRepo *testapi.FakeBuildpackBitsRepository + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} + ui = new(testterm.FakeUI) + repo = &testapi.FakeBuildpackRepository{} + bitsRepo = &testapi.FakeBuildpackBitsRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUpdateBuildpack(ui, repo, bitsRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("is only successful on login and buildpack success", func() { + It("returns success when both are true", func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} + + Expect(runCommand("my-buildpack")).To(BeTrue()) + }) + + It("returns failure when at least one is false", func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: false} + Expect(runCommand("my-buildpack", "-p", "buildpack.zip", "extraArg")).To(BeFalse()) + + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: false} + + Expect(runCommand("my-buildpack")).To(BeFalse()) + + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: false, BuildpackSuccess: true} + + Expect(runCommand("my-buildpack")).To(BeFalse()) + }) + }) + + It("updates buildpack", func() { + runCommand("my-buildpack") + + successfulUpdate(ui, "my-buildpack") + }) + + Context("updates buildpack when passed the proper flags", func() { + Context("position flag", func() { + It("sets the position when passed a value", func() { + runCommand("-i", "999", "my-buildpack") + + Expect(*repo.UpdateBuildpackArgs.Buildpack.Position).To(Equal(999)) + successfulUpdate(ui, "my-buildpack") + }) + + It("defaults to nil when not passed", func() { + runCommand("my-buildpack") + + Expect(repo.UpdateBuildpackArgs.Buildpack.Position).To(BeNil()) + }) + }) + + Context("enabling/disabling buildpacks", func() { + It("can enable buildpack", func() { + runCommand("--enable", "my-buildpack") + + Expect(repo.UpdateBuildpackArgs.Buildpack.Enabled).NotTo(BeNil()) + Expect(*repo.UpdateBuildpackArgs.Buildpack.Enabled).To(Equal(true)) + + successfulUpdate(ui, "my-buildpack") + }) + + It("can disable buildpack", func() { + runCommand("--disable", "my-buildpack") + + Expect(repo.UpdateBuildpackArgs.Buildpack.Enabled).NotTo(BeNil()) + Expect(*repo.UpdateBuildpackArgs.Buildpack.Enabled).To(Equal(false)) + }) + + It("defaults to nil when not passed", func() { + runCommand("my-buildpack") + + Expect(repo.UpdateBuildpackArgs.Buildpack.Enabled).To(BeNil()) + }) + }) + + Context("buildpack path", func() { + It("uploads buildpack when passed", func() { + runCommand("-p", "buildpack.zip", "my-buildpack") + + Expect(bitsRepo.UploadBuildpackPath).To(Equal("buildpack.zip")) + + successfulUpdate(ui, "my-buildpack") + }) + + It("errors when passed invalid path", func() { + bitsRepo.UploadBuildpackErr = true + + runCommand("-p", "bogus/path", "my-buildpack") + + failedUpdate(ui, "my-buildpack") + }) + }) + + Context("locking buildpack", func() { + It("can lock a buildpack", func() { + runCommand("--lock", "my-buildpack") + + Expect(repo.UpdateBuildpackArgs.Buildpack.Locked).NotTo(BeNil()) + Expect(*repo.UpdateBuildpackArgs.Buildpack.Locked).To(Equal(true)) + + successfulUpdate(ui, "my-buildpack") + }) + + It("can unlock a buildpack", func() { + runCommand("--unlock", "my-buildpack") + + successfulUpdate(ui, "my-buildpack") + }) + + Context("Unsuccessful locking", func() { + It("lock fails when passed invalid path", func() { + runCommand("--lock", "-p", "buildpack.zip", "my-buildpack") + + failedUpdate(ui, "my-buildpack") + }) + + It("unlock fails when passed invalid path", func() { + runCommand("--unlock", "-p", "buildpack.zip", "my-buildpack") + + failedUpdate(ui, "my-buildpack") + }) + }) + }) + }) + +}) diff --git a/cf/commands/commands_suite_test.go b/cf/commands/commands_suite_test.go new file mode 100644 index 00000000000..b0001ef7b85 --- /dev/null +++ b/cf/commands/commands_suite_test.go @@ -0,0 +1,19 @@ +package commands_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCommands(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Commands Suite") +} diff --git a/cf/commands/config.go b/cf/commands/config.go new file mode 100644 index 00000000000..0a8854f61bc --- /dev/null +++ b/cf/commands/config.go @@ -0,0 +1,99 @@ +package commands + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ConfigCommands struct { + ui terminal.UI + config core_config.ReadWriter +} + +func NewConfig(ui terminal.UI, config core_config.ReadWriter) ConfigCommands { + return ConfigCommands{ui: ui, config: config} +} + +func (cmd ConfigCommands) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "config", + Description: T("write default values to the config"), + Usage: T("CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]"), + Flags: []cli.Flag{ + flag_helpers.NewIntFlag("async-timeout", T("Timeout for async HTTP requests")), + flag_helpers.NewStringFlag("trace", T("Trace HTTP requests")), + flag_helpers.NewStringFlag("color", T("Enable or disable color")), + flag_helpers.NewStringFlag("locale", "Set default locale. If LOCALE is CLEAR, previous locale is deleted."), + }, + } +} + +func (cmd ConfigCommands) GetRequirements(_ requirements.Factory, _ *cli.Context) ([]requirements.Requirement, error) { + return nil, nil +} + +func (cmd ConfigCommands) Run(context *cli.Context) { + if !context.IsSet("trace") && !context.IsSet("async-timeout") && !context.IsSet("color") && !context.IsSet("locale") { + cmd.ui.FailWithUsage(context) + return + } + + if context.IsSet("async-timeout") { + asyncTimeout := context.Int("async-timeout") + if asyncTimeout < 0 { + cmd.ui.FailWithUsage(context) + } + + cmd.config.SetAsyncTimeout(uint(asyncTimeout)) + } + + if context.IsSet("trace") { + cmd.config.SetTrace(context.String("trace")) + } + + if context.IsSet("color") { + value := context.String("color") + switch value { + case "true": + cmd.config.SetColorEnabled("true") + case "false": + cmd.config.SetColorEnabled("false") + default: + cmd.ui.FailWithUsage(context) + } + } + + if context.IsSet("locale") { + locale := context.String("locale") + + if locale == "CLEAR" { + cmd.config.SetLocale("") + return + } + + foundLocale := false + + for _, value := range SUPPORTED_LOCALES { + if value == locale { + cmd.config.SetLocale(locale) + foundLocale = true + break + } + } + + if !foundLocale { + cmd.ui.Say(fmt.Sprintf("Could not find locale %s. The known locales are:", locale)) + cmd.ui.Say("") + for _, locale := range SUPPORTED_LOCALES { + cmd.ui.Say(locale) + } + } + } +} diff --git a/cf/commands/config_test.go b/cf/commands/config_test.go new file mode 100644 index 00000000000..438eb2855be --- /dev/null +++ b/cf/commands/config_test.go @@ -0,0 +1,112 @@ +package commands_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("config command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) { + cmd := NewConfig(ui, configRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + } + It("fails requirements when no flags are provided", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("--async-timeout flag", func() { + + It("stores the timeout in minutes when the --async-timeout flag is provided", func() { + runCommand("--async-timeout", "12") + Expect(configRepo.AsyncTimeout()).Should(Equal(uint(12))) + }) + + It("fails with usage when a invalid async timeout value is passed", func() { + runCommand("--async-timeout", "-1") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails with usage when a negative timout is passed", func() { + runCommand("--async-timeout", "-555") + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(configRepo.AsyncTimeout()).To(Equal(uint(0))) + }) + }) + + Context("--trace flag", func() { + It("stores the trace value when --trace flag is provided", func() { + runCommand("--trace", "true") + Expect(configRepo.Trace()).Should(Equal("true")) + + runCommand("--trace", "false") + Expect(configRepo.Trace()).Should(Equal("false")) + + runCommand("--trace", "some/file/lol") + Expect(configRepo.Trace()).Should(Equal("some/file/lol")) + }) + }) + + Context("--color flag", func() { + It("stores the color value when --color flag is provided", func() { + runCommand("--color", "true") + Expect(configRepo.ColorEnabled()).Should(Equal("true")) + + runCommand("--color", "false") + Expect(configRepo.ColorEnabled()).Should(Equal("false")) + }) + + It("fails with usage when a non-bool value is provided", func() { + runCommand("--color", "plaid") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("--locale flag", func() { + It("stores the locale value when --locale [locale] is provided", func() { + runCommand("--locale", "zh_Hans") + Expect(configRepo.Locale()).Should(Equal("zh_Hans")) + }) + + It("informs the user of known locales if an unknown locale is provided", func() { + runCommand("--locale", "foo_BAR") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"known locales are"}, + []string{"en_US"}, + []string{"fr_FR"}, + )) + }) + + Context("when the locale is already set", func() { + BeforeEach(func() { + configRepo.SetLocale("fr_FR") + Expect(configRepo.Locale()).Should(Equal("fr_FR")) + }) + + It("clears the locale when the '--locale clear' flag is provided", func() { + runCommand("--locale", "CLEAR") + Expect(configRepo.Locale()).Should(Equal("")) + }) + }) + }) +}) diff --git a/cf/commands/curl.go b/cf/commands/curl.go new file mode 100644 index 00000000000..c0552a91255 --- /dev/null +++ b/cf/commands/curl.go @@ -0,0 +1,121 @@ +package commands + +import ( + "bytes" + "encoding/json" + "errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/trace" + "github.com/codegangsta/cli" +) + +type Curl struct { + ui terminal.UI + config core_config.Reader + curlRepo api.CurlRepository +} + +func NewCurl(ui terminal.UI, config core_config.Reader, curlRepo api.CurlRepository) (cmd *Curl) { + cmd = new(Curl) + cmd.ui = ui + cmd.config = config + cmd.curlRepo = curlRepo + return +} + +func (cmd *Curl) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "curl", + Description: T("Executes a raw request, content-type set to application/json by default"), + Usage: T("CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "i", Usage: T("Include response headers in the output")}, + cli.BoolFlag{Name: "v", Usage: T("Enable CF_TRACE output for all requests and responses")}, + cli.StringFlag{Name: "X", Value: "GET", Usage: T("HTTP method (GET,POST,PUT,DELETE,etc)")}, + flag_helpers.NewStringSliceFlag("H", T("Custom headers to include in the request, flag can be specified multiple times")), + flag_helpers.NewStringFlag("d", T("HTTP data to include in the request body")), + flag_helpers.NewStringFlag("output", T("Write curl body to FILE instead of stdout")), + }, + } +} + +func (cmd *Curl) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + err = errors.New(T("Incorrect number of arguments")) + cmd.ui.FailWithUsage(c) + return + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *Curl) Run(c *cli.Context) { + path := c.Args()[0] + method := c.String("X") + headers := c.StringSlice("H") + body := c.String("d") + verbose := c.Bool("v") + + reqHeader := strings.Join(headers, "\n") + + if verbose { + trace.EnableTrace() + } + + responseHeader, responseBody, apiErr := cmd.curlRepo.Request(method, path, reqHeader, body) + if apiErr != nil { + cmd.ui.Failed(T("Error creating request:\n{{.Err}}", map[string]interface{}{"Err": apiErr.Error()})) + } + + if verbose { + return + } + + if c.Bool("i") { + cmd.ui.Say(responseHeader) + } + + if c.String("output") != "" { + err := cmd.writeToFile(responseBody, c.String("output")) + if err != nil { + cmd.ui.Failed(T("Error creating request:\n{{.Err}}", map[string]interface{}{"Err": err})) + } + } else { + if strings.Contains(responseHeader, "application/json") { + buffer := bytes.Buffer{} + err := json.Indent(&buffer, []byte(responseBody), "", " ") + if err == nil { + responseBody = buffer.String() + } + } + + cmd.ui.Say(responseBody) + } + return +} + +func (cmd Curl) writeToFile(responseBody, filePath string) (err error) { + if _, err = os.Stat(filePath); os.IsNotExist(err) { + err = os.MkdirAll(filepath.Dir(filePath), 0755) + } + + if err != nil { + return + } + + return ioutil.WriteFile(filePath, []byte(responseBody), 0644) +} diff --git a/cf/commands/curl_test.go b/cf/commands/curl_test.go new file mode 100644 index 00000000000..e24a0ca8719 --- /dev/null +++ b/cf/commands/curl_test.go @@ -0,0 +1,208 @@ +package commands_test + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/trace" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + "github.com/cloudfoundry/gofileutils/fileutils" + + . "github.com/cloudfoundry/cli/cf/commands" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("curl command", func() { + var deps curlDependencies + + BeforeEach(func() { + deps = newCurlDependencies() + }) + + It("does not pass requirements when not logged in", func() { + Expect(runCurlWithInputs(deps, []string{"/foo"})).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + deps.requirementsFactory.LoginSuccess = true + }) + + It("fails with usage when not given enough input", func() { + runCurlWithInputs(deps, []string{}) + Expect(deps.ui.FailedWithUsage).To(BeTrue()) + }) + + It("passes requirements", func() { + Expect(runCurlWithInputs(deps, []string{"/foo"})).To(BeTrue()) + }) + + It("makes a get request given an endpoint", func() { + deps.curlRepo.ResponseHeader = "Content-Size:1024" + deps.curlRepo.ResponseBody = "response for get" + runCurlWithInputs(deps, []string{"/foo"}) + + Expect(deps.curlRepo.Method).To(Equal("GET")) + Expect(deps.curlRepo.Path).To(Equal("/foo")) + Expect(deps.ui.Outputs).To(ContainSubstrings([]string{"response for get"})) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings( + []string{"FAILED"}, + []string{"Content-Size:1024"}, + )) + }) + + Context("when the --output flag is provided", func() { + It("saves the body of the response to the given filepath if it exists", func() { + fileutils.TempFile("poor-mans-pipe", func(tempFile *os.File, err error) { + Expect(err).ToNot(HaveOccurred()) + deps.curlRepo.ResponseBody = "hai" + + runCurlWithInputs(deps, []string{"--output", tempFile.Name(), "/foo"}) + contents, err := ioutil.ReadAll(tempFile) + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("hai")) + }) + }) + + It("saves the body of the response to the given filepath if it doesn't exists", func() { + fileutils.TempDir("poor-mans-dir", func(tmpDir string, err error) { + Expect(err).ToNot(HaveOccurred()) + deps.curlRepo.ResponseBody = "hai" + + filePath := filepath.Join(tmpDir, "subdir1", "banana.txt") + runCurlWithInputs(deps, []string{"--output", filePath, "/foo"}) + + file, err := os.Open(filePath) + Expect(err).ToNot(HaveOccurred()) + + contents, err := ioutil.ReadAll(file) + Expect(err).ToNot(HaveOccurred()) + Expect(string(contents)).To(Equal("hai")) + }) + }) + }) + + It("makes a post request given -X", func() { + runCurlWithInputs(deps, []string{"-X", "post", "/foo"}) + + Expect(deps.curlRepo.Method).To(Equal("post")) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("sends headers given -H", func() { + runCurlWithInputs(deps, []string{"-H", "Content-Type:cat", "/foo"}) + + Expect(deps.curlRepo.Header).To(Equal("Content-Type:cat")) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("sends multiple headers given multiple -H flags", func() { + runCurlWithInputs(deps, []string{"-H", "Content-Type:cat", "-H", "Content-Length:12", "/foo"}) + + Expect(deps.curlRepo.Header).To(Equal("Content-Type:cat\nContent-Length:12")) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("prints out the response headers given -i", func() { + deps.curlRepo.ResponseHeader = "Content-Size:1024" + deps.curlRepo.ResponseBody = "response for get" + runCurlWithInputs(deps, []string{"-i", "/foo"}) + + Expect(deps.ui.Outputs).To(ContainSubstrings( + []string{"Content-Size:1024"}, + []string{"response for get"}, + )) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("sets the request body given -d", func() { + runCurlWithInputs(deps, []string{"-d", "body content to upload", "/foo"}) + + Expect(deps.curlRepo.Body).To(Equal("body content to upload")) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + + It("prints verbose output given the -v flag", func() { + output := bytes.NewBuffer(make([]byte, 1024)) + trace.SetStdout(output) + + runCurlWithInputs(deps, []string{"-v", "/foo"}) + trace.Logger.Print("logging enabled") + + Expect([]string{output.String()}).To(ContainSubstrings([]string{"logging enabled"})) + }) + + It("prints a failure message when the response is not success", func() { + deps.curlRepo.Error = errors.New("ooops") + runCurlWithInputs(deps, []string{"/foo"}) + + Expect(deps.ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"ooops"}, + )) + }) + + Context("Whent the content type is JSON", func() { + BeforeEach(func() { + deps.curlRepo.ResponseHeader = "Content-Type: application/json;charset=utf-8" + deps.curlRepo.ResponseBody = `{"total_results":0,"total_pages":1,"prev_url":null,"next_url":null,"resources":[]}` + }) + + It("pretty-prints the response body", func() { + runCurlWithInputs(deps, []string{"/ugly-printed-json-endpoint"}) + + Expect(deps.ui.Outputs).To(ContainSubstrings( + []string{"{"}, + []string{" \"total_results", "0"}, + []string{" \"total_pages", "1"}, + []string{" \"prev_url", "null"}, + []string{" \"next_url", "null"}, + []string{" \"resources", "[]"}, + []string{"}"}, + )) + }) + + Context("But the body is not JSON", func() { + BeforeEach(func() { + deps.curlRepo.ResponseBody = "FAIL: crumpets need MOAR butterz" + }) + + It("regular-prints the response body", func() { + runCurlWithInputs(deps, []string{"/whateverz"}) + + Expect(deps.ui.Outputs).To(Equal([]string{"FAIL: crumpets need MOAR butterz"})) + }) + }) + }) + }) +}) + +type curlDependencies struct { + ui *testterm.FakeUI + config core_config.Reader + requirementsFactory *testreq.FakeReqFactory + curlRepo *testapi.FakeCurlRepository +} + +func newCurlDependencies() (deps curlDependencies) { + deps.ui = &testterm.FakeUI{} + deps.config = testconfig.NewRepository() + deps.requirementsFactory = &testreq.FakeReqFactory{} + deps.curlRepo = &testapi.FakeCurlRepository{} + return +} + +func runCurlWithInputs(deps curlDependencies, args []string) bool { + cmd := NewCurl(deps.ui, deps.config, deps.curlRepo) + return testcmd.RunCommand(cmd, args, deps.requirementsFactory) +} diff --git a/cf/commands/domain/create_domain.go b/cf/commands/domain/create_domain.go new file mode 100644 index 00000000000..e1dd47c1f20 --- /dev/null +++ b/cf/commands/domain/create_domain.go @@ -0,0 +1,66 @@ +package domain + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateDomain struct { + ui terminal.UI + config core_config.Reader + domainRepo api.DomainRepository + orgReq requirements.OrganizationRequirement +} + +func NewCreateDomain(ui terminal.UI, config core_config.Reader, domainRepo api.DomainRepository) (cmd *CreateDomain) { + cmd = new(CreateDomain) + cmd.ui = ui + cmd.config = config + cmd.domainRepo = domainRepo + return +} + +func (cmd *CreateDomain) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-domain", + Description: T("Create a domain in an org for later use"), + Usage: T("CF_NAME create-domain ORG DOMAIN"), + } +} + +func (cmd *CreateDomain) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.orgReq, + } + return +} + +func (cmd *CreateDomain) Run(c *cli.Context) { + domainName := c.Args()[1] + owningOrg := cmd.orgReq.GetOrganization() + + cmd.ui.Say(T("Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "DomainName": terminal.EntityNameColor(domainName), + "OrgName": terminal.EntityNameColor(owningOrg.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + _, apiErr := cmd.domainRepo.Create(domainName, owningOrg.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/domain/create_domain_test.go b/cf/commands/domain/create_domain_test.go new file mode 100644 index 00000000000..a5cc8abd25b --- /dev/null +++ b/cf/commands/domain/create_domain_test.go @@ -0,0 +1,77 @@ +package domain_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/commands/domain" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create domain command", func() { + + var ( + requirementsFactory *testreq.FakeReqFactory + ui *testterm.FakeUI + domainRepo *testapi.FakeDomainRepository + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + domainRepo = &testapi.FakeDomainRepository{} + configRepo = testconfig.NewRepositoryWithAccessToken(core_config.TokenInfo{Username: "my-user"}) + }) + + runCommand := func(args ...string) bool { + ui = new(testterm.FakeUI) + cmd := domain.NewCreateDomain(ui, configRepo, domainRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails with usage", func() { + runCommand("") + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("org1") + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("org1", "example.com") + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + + Context("checks login", func() { + It("passes when logged in", func() { + Expect(runCommand("my-org", "example.com")).To(BeTrue()) + Expect(requirementsFactory.OrganizationName).To(Equal("my-org")) + }) + + It("fails when not logged in", func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: false} + + Expect(runCommand("my-org", "example.com")).To(BeFalse()) + }) + }) + + It("creates a domain", func() { + org := models.Organization{} + org.Name = "myOrg" + org.Guid = "myOrg-guid" + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, Organization: org} + runCommand("myOrg", "example.com") + + Expect(domainRepo.CreateDomainName).To(Equal("example.com")) + Expect(domainRepo.CreateDomainOwningOrgGuid).To(Equal("myOrg-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating domain", "example.com", "myOrg", "my-user"}, + []string{"OK"}, + )) + }) +}) diff --git a/cf/commands/domain/create_shared_domain.go b/cf/commands/domain/create_shared_domain.go new file mode 100644 index 00000000000..8392a76aae8 --- /dev/null +++ b/cf/commands/domain/create_shared_domain.go @@ -0,0 +1,62 @@ +package domain + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateSharedDomain struct { + ui terminal.UI + config core_config.Reader + domainRepo api.DomainRepository + orgReq requirements.OrganizationRequirement +} + +func NewCreateSharedDomain(ui terminal.UI, config core_config.Reader, domainRepo api.DomainRepository) (cmd *CreateSharedDomain) { + cmd = new(CreateSharedDomain) + cmd.ui = ui + cmd.config = config + cmd.domainRepo = domainRepo + return +} + +func (cmd *CreateSharedDomain) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-shared-domain", + Description: T("Create a domain that can be used by all orgs (admin-only)"), + Usage: T("CF_NAME create-shared-domain DOMAIN"), + } +} + +func (cmd *CreateSharedDomain) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *CreateSharedDomain) Run(c *cli.Context) { + domainName := c.Args()[0] + + cmd.ui.Say(T("Creating shared domain {{.DomainName}} as {{.Username}}...", + map[string]interface{}{ + "DomainName": terminal.EntityNameColor(domainName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.domainRepo.CreateSharedDomain(domainName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/domain/create_shared_domain_test.go b/cf/commands/domain/create_shared_domain_test.go new file mode 100644 index 00000000000..a289267ba1c --- /dev/null +++ b/cf/commands/domain/create_shared_domain_test.go @@ -0,0 +1,61 @@ +package domain_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/domain" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Testing with ginkgo", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + ui *testterm.FakeUI + domainRepo *testapi.FakeDomainRepository + configRepo core_config.ReadWriter + ) + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + domainRepo = &testapi.FakeDomainRepository{} + configRepo = testconfig.NewRepositoryWithAccessToken(core_config.TokenInfo{Username: "my-user"}) + }) + + runCommand := func(args ...string) bool { + ui = new(testterm.FakeUI) + cmd := NewCreateSharedDomain(ui, configRepo, domainRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("TestShareDomainRequirements", func() { + Expect(runCommand("example.com")).To(BeTrue()) + + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: false} + + Expect(runCommand("example.com")).To(BeFalse()) + }) + + It("TestShareDomainFailsWithUsage", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("example.com") + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + + It("TestShareDomain", func() { + runCommand("example.com") + + Expect(domainRepo.CreateSharedDomainName).To(Equal("example.com")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating shared domain", "example.com", "my-user"}, + []string{"OK"}, + )) + }) +}) diff --git a/cf/commands/domain/delete_domain.go b/cf/commands/domain/delete_domain.go new file mode 100644 index 00000000000..deb4cd90491 --- /dev/null +++ b/cf/commands/domain/delete_domain.go @@ -0,0 +1,91 @@ +package domain + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteDomain struct { + ui terminal.UI + config core_config.Reader + orgReq requirements.TargetedOrgRequirement + domainRepo api.DomainRepository +} + +func NewDeleteDomain(ui terminal.UI, config core_config.Reader, repo api.DomainRepository) (cmd *DeleteDomain) { + cmd = new(DeleteDomain) + cmd.ui = ui + cmd.config = config + cmd.domainRepo = repo + return +} + +func (cmd *DeleteDomain) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-domain", + Description: T("Delete a domain"), + Usage: T("CF_NAME delete-domain DOMAIN [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteDomain) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + loginReq := requirementsFactory.NewLoginRequirement() + cmd.orgReq = requirementsFactory.NewTargetedOrgRequirement() + + reqs = []requirements.Requirement{ + loginReq, + cmd.orgReq, + } + + return +} + +func (cmd *DeleteDomain) Run(c *cli.Context) { + domainName := c.Args()[0] + domain, apiErr := cmd.domainRepo.FindByNameInOrg(domainName, cmd.orgReq.GetOrganizationFields().Guid) + + switch apiErr.(type) { + case nil: //do nothing + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(apiErr.Error()) + return + default: + cmd.ui.Failed(T("Error finding domain {{.DomainName}}\n{{.ApiErr}}", + map[string]interface{}{"DomainName": domainName, "ApiErr": apiErr.Error()})) + return + } + + if !c.Bool("f") { + if !cmd.ui.ConfirmDelete(T("domain"), domainName) { + return + } + } + + cmd.ui.Say(T("Deleting domain {{.DomainName}} as {{.Username}}...", + map[string]interface{}{ + "DomainName": terminal.EntityNameColor(domainName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr = cmd.domainRepo.Delete(domain.Guid) + if apiErr != nil { + cmd.ui.Failed(T("Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + map[string]interface{}{"DomainName": domainName, "ApiErr": apiErr.Error()})) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/domain/delete_domain_test.go b/cf/commands/domain/delete_domain_test.go new file mode 100644 index 00000000000..60c5547f7e1 --- /dev/null +++ b/cf/commands/domain/delete_domain_test.go @@ -0,0 +1,167 @@ +package domain_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/domain" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-domain command", func() { + var ( + cmd *DeleteDomain + ui *testterm.FakeUI + configRepo core_config.ReadWriter + domainRepo *testapi.FakeDomainRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{ + Inputs: []string{"yes"}, + } + + domainRepo = &testapi.FakeDomainRepository{} + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedOrgSuccess: true, + } + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd = NewDeleteDomain(ui, configRepo, domainRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("foo.com")).To(BeFalse()) + }) + + It("fails when the an org is not targetted", func() { + requirementsFactory.TargetedOrgSuccess = false + + Expect(runCommand("foo.com")).To(BeFalse()) + }) + }) + + Context("when the domain exists", func() { + BeforeEach(func() { + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "foo.com", + Guid: "foo-guid", + } + }) + + It("deletes domains", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("foo-guid")) + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the domain foo.com"})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com", "my-user"}, + []string{"OK"}, + )) + }) + + Context("when there is an error deleting the domain", func() { + BeforeEach(func() { + domainRepo.DeleteApiResponse = errors.New("failed badly") + }) + + It("show the error the user", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("foo-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"FAILED"}, + []string{"foo.com"}, + []string{"failed badly"}, + )) + }) + }) + + Context("when the user does not confirm", func() { + BeforeEach(func() { + ui.Inputs = []string{"no"} + }) + + It("does nothing", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("")) + + Expect(ui.Prompts).To(ContainSubstrings([]string{"delete", "foo.com"})) + + Expect(ui.Outputs).To(BeEmpty()) + }) + }) + + Context("when the user provides the -f flag", func() { + BeforeEach(func() { + ui.Inputs = []string{} + }) + + It("skips confirmation", func() { + runCommand("-f", "foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("foo-guid")) + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"OK"}, + )) + }) + }) + }) + + Context("when a domain with the given name doesn't exist", func() { + BeforeEach(func() { + domainRepo.FindByNameInOrgApiResponse = errors.NewModelNotFoundError("Domain", "foo.com") + }) + + It("fails", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"OK"}, + []string{"foo.com", "not found"}, + )) + }) + }) + + Context("when there is an error finding the domain", func() { + BeforeEach(func() { + domainRepo.FindByNameInOrgApiResponse = errors.New("failed badly") + }) + + It("shows the error to the user", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteDomainGuid).To(Equal("")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"foo.com"}, + []string{"failed badly"}, + )) + }) + }) +}) diff --git a/cf/commands/domain/delete_shared_domain.go b/cf/commands/domain/delete_shared_domain.go new file mode 100644 index 00000000000..7eeefad59eb --- /dev/null +++ b/cf/commands/domain/delete_shared_domain.go @@ -0,0 +1,96 @@ +package domain + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteSharedDomain struct { + ui terminal.UI + config core_config.Reader + orgReq requirements.TargetedOrgRequirement + domainRepo api.DomainRepository +} + +func NewDeleteSharedDomain(ui terminal.UI, config core_config.Reader, repo api.DomainRepository) (cmd *DeleteSharedDomain) { + cmd = new(DeleteSharedDomain) + cmd.ui = ui + cmd.config = config + cmd.domainRepo = repo + return +} + +func (cmd *DeleteSharedDomain) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-shared-domain", + Description: T("Delete a shared domain"), + Usage: T("CF_NAME delete-shared-domain DOMAIN [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteSharedDomain) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + loginReq := requirementsFactory.NewLoginRequirement() + cmd.orgReq = requirementsFactory.NewTargetedOrgRequirement() + + reqs = []requirements.Requirement{ + loginReq, + cmd.orgReq, + } + + return +} + +func (cmd *DeleteSharedDomain) Run(c *cli.Context) { + domainName := c.Args()[0] + force := c.Bool("f") + + cmd.ui.Say(T("Deleting domain {{.DomainName}} as {{.Username}}...", + map[string]interface{}{ + "DomainName": terminal.EntityNameColor(domainName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + domain, apiErr := cmd.domainRepo.FindByNameInOrg(domainName, cmd.orgReq.GetOrganizationFields().Guid) + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(apiErr.Error()) + return + default: + cmd.ui.Failed(T("Error finding domain {{.DomainName}}\n{{.ApiErr}}", + map[string]interface{}{ + "DomainName": domainName, + "ApiErr": apiErr.Error()})) + return + } + + if !force { + answer := cmd.ui.Confirm(T("This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", map[string]interface{}{"DomainName": domainName})) + + if !answer { + return + } + } + + apiErr = cmd.domainRepo.DeleteSharedDomain(domain.Guid) + if apiErr != nil { + cmd.ui.Failed(T("Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + map[string]interface{}{"DomainName": domainName, "ApiErr": apiErr.Error()})) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/domain/delete_shared_domain_test.go b/cf/commands/domain/delete_shared_domain_test.go new file mode 100644 index 00000000000..d1d802b83db --- /dev/null +++ b/cf/commands/domain/delete_shared_domain_test.go @@ -0,0 +1,127 @@ +package domain_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/domain" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-shared-domain command", func() { + var ( + ui *testterm.FakeUI + domainRepo *testapi.FakeDomainRepository + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + domainRepo = &testapi.FakeDomainRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewDeleteSharedDomain(ui, configRepo, domainRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails if you are not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand("foo.com")).To(BeFalse()) + }) + + It("fails if an organiztion is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("foo.com")).To(BeFalse()) + }) + }) + + Context("when logged in and targeted an organiztion", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + domainRepo.FindByNameInOrgDomain = models.DomainFields{ + Name: "foo.com", + Guid: "foo-guid", + Shared: true, + } + }) + + Describe("and the command is invoked interactively", func() { + BeforeEach(func() { + ui.Inputs = []string{"y"} + }) + + It("when the domain is not found it tells the user", func() { + domainRepo.FindByNameInOrgApiResponse = errors.NewModelNotFoundError("Domain", "foo.com") + runCommand("foo.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"OK"}, + []string{"foo.com", "not found"}, + )) + }) + + It("fails when the api returns an error", func() { + domainRepo.FindByNameInOrgApiResponse = errors.New("couldn't find the droids you're lookin for") + runCommand("foo.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"FAILED"}, + []string{"foo.com"}, + []string{"couldn't find the droids you're lookin for"}, + )) + }) + + It("fails when deleting the domain encounters an error", func() { + domainRepo.DeleteSharedApiResponse = errors.New("failed badly") + runCommand("foo.com") + + Expect(domainRepo.DeleteSharedDomainGuid).To(Equal("foo-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"FAILED"}, + []string{"foo.com"}, + []string{"failed badly"}, + )) + }) + + It("Prompts a user to delete the shared domain", func() { + runCommand("foo.com") + + Expect(domainRepo.DeleteSharedDomainGuid).To(Equal("foo-guid")) + Expect(ui.Prompts).To(ContainSubstrings([]string{"delete", "domain", "foo.com"})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"OK"}, + )) + }) + }) + + It("skips confirmation if the force flag is passed", func() { + runCommand("-f", "foo.com") + + Expect(domainRepo.DeleteSharedDomainGuid).To(Equal("foo-guid")) + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting domain", "foo.com"}, + []string{"OK"}, + )) + }) + }) +}) diff --git a/cf/commands/domain/domain_suite_test.go b/cf/commands/domain/domain_suite_test.go new file mode 100644 index 00000000000..7ae32d6297d --- /dev/null +++ b/cf/commands/domain/domain_suite_test.go @@ -0,0 +1,20 @@ +package domain_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestDomain(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Domain Suite") +} diff --git a/cf/commands/domain/domains.go b/cf/commands/domain/domains.go new file mode 100644 index 00000000000..5fd192ad215 --- /dev/null +++ b/cf/commands/domain/domains.go @@ -0,0 +1,92 @@ +package domain + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListDomains struct { + ui terminal.UI + config core_config.Reader + orgReq requirements.TargetedOrgRequirement + domainRepo api.DomainRepository +} + +func NewListDomains(ui terminal.UI, config core_config.Reader, domainRepo api.DomainRepository) (cmd *ListDomains) { + cmd = new(ListDomains) + cmd.ui = ui + cmd.config = config + cmd.domainRepo = domainRepo + return +} + +func (cmd *ListDomains) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "domains", + Description: T("List domains in the target org"), + Usage: "CF_NAME domains", + } +} + +func (cmd *ListDomains) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) > 0 { + cmd.ui.FailWithUsage(c) + } + + cmd.orgReq = requirementsFactory.NewTargetedOrgRequirement() + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.orgReq, + } + return +} + +func (cmd *ListDomains) Run(c *cli.Context) { + org := cmd.orgReq.GetOrganizationFields() + + cmd.ui.Say(T("Getting domains in org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(org.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + domains := cmd.fetchAllDomains(org.Guid) + cmd.printDomainsTable(domains) + + if len(domains) == 0 { + cmd.ui.Say(T("No domains found")) + } +} + +func (cmd *ListDomains) fetchAllDomains(orgGuid string) (domains []models.DomainFields) { + apiErr := cmd.domainRepo.ListDomainsForOrg(orgGuid, func(domain models.DomainFields) bool { + domains = append(domains, domain) + return true + }) + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching domains.\n{{.ApiErr}}", map[string]interface{}{"ApiErr": apiErr.Error()})) + } + return +} + +func (cmd *ListDomains) printDomainsTable(domains []models.DomainFields) { + table := cmd.ui.Table([]string{T("name"), T("status")}) + + for _, domain := range domains { + if domain.Shared { + table.Add(domain.Name, T("shared")) + } + } + + for _, domain := range domains { + if !domain.Shared { + table.Add(domain.Name, T("owned")) + } + } + table.Print() +} diff --git a/cf/commands/domain/domains_test.go b/cf/commands/domain/domains_test.go new file mode 100644 index 00000000000..cc23155af32 --- /dev/null +++ b/cf/commands/domain/domains_test.go @@ -0,0 +1,122 @@ +package domain_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/domain" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("domains command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + domainRepo *testapi.FakeDomainRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + domainRepo = &testapi.FakeDomainRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewListDomains(ui, configRepo, domainRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when an org is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + + It("fails when not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + + It("fails with usage when invoked with any args what so ever ", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + + runCommand("whoops") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in and an org is targeted", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + requirementsFactory.OrganizationFields = models.OrganizationFields{ + Name: "my-org", + Guid: "my-org-guid", + } + }) + + Context("when there is at least one domain", func() { + BeforeEach(func() { + domainRepo.ListDomainsForOrgDomains = []models.DomainFields{ + models.DomainFields{ + Shared: false, + Name: "Private-domain1", + }, + models.DomainFields{ + Shared: true, + Name: "The-shared-domain", + }, + models.DomainFields{ + Shared: false, + Name: "Private-domain2", + }, + } + }) + + It("lists domains", func() { + runCommand() + + Expect(domainRepo.ListDomainsForOrgGuid).To(Equal("my-org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting domains in org", "my-org", "my-user"}, + []string{"name", "status"}, + []string{"The-shared-domain", "shared"}, + []string{"Private-domain1", "owned"}, + []string{"Private-domain2", "owned"}, + )) + }) + }) + + It("displays a message when no domains are found", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting domains in org", "my-org", "my-user"}, + []string{"No domains found"}, + )) + }) + + It("fails when the domains API returns an error", func() { + domainRepo.ListDomainsForOrgApiResponse = errors.New("borked!") + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting domains in org", "my-org", "my-user"}, + []string{"FAILED"}, + []string{"Failed fetching domains"}, + []string{"borked!"}, + )) + }) + }) +}) diff --git a/cf/commands/environmentvariablegroup/environmentvariablegroup_suite_test.go b/cf/commands/environmentvariablegroup/environmentvariablegroup_suite_test.go new file mode 100644 index 00000000000..f82c0ab40a9 --- /dev/null +++ b/cf/commands/environmentvariablegroup/environmentvariablegroup_suite_test.go @@ -0,0 +1,19 @@ +package environmentvariablegroup_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestEnvironmentvariablegroup(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Environmentvariablegroup Suite") +} diff --git a/cf/commands/environmentvariablegroup/running_environment_variable_group.go b/cf/commands/environmentvariablegroup/running_environment_variable_group.go new file mode 100644 index 00000000000..b1b7115d771 --- /dev/null +++ b/cf/commands/environmentvariablegroup/running_environment_variable_group.go @@ -0,0 +1,61 @@ +package environmentvariablegroup + +import ( + "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RunningEnvironmentVariableGroup struct { + ui terminal.UI + config core_config.ReadWriter + environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository +} + +func NewRunningEnvironmentVariableGroup(ui terminal.UI, config core_config.ReadWriter, environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository) (cmd RunningEnvironmentVariableGroup) { + cmd.ui = ui + cmd.config = config + cmd.environmentVariableGroupRepo = environmentVariableGroupRepo + return +} + +func (cmd RunningEnvironmentVariableGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "running-environment-variable-group", + Description: T("Retrieve the contents of the running environment variable group"), + ShortName: "revg", + Usage: T("CF_NAME running-environment-variable-group"), + } +} + +func (cmd RunningEnvironmentVariableGroup) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd RunningEnvironmentVariableGroup) Run(c *cli.Context) { + cmd.ui.Say(T("Retrieving the contents of the running environment variable group as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + runningEnvVars, err := cmd.environmentVariableGroupRepo.ListRunning() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + + table := terminal.NewTable(cmd.ui, []string{T("Variable Name"), T("Assigned Value")}) + for _, envVar := range runningEnvVars { + table.Add(envVar.Name, envVar.Value) + } + table.Print() +} diff --git a/cf/commands/environmentvariablegroup/running_environment_variable_group_test.go b/cf/commands/environmentvariablegroup/running_environment_variable_group_test.go new file mode 100644 index 00000000000..434f6ca5a99 --- /dev/null +++ b/cf/commands/environmentvariablegroup/running_environment_variable_group_test.go @@ -0,0 +1,68 @@ +package environmentvariablegroup_test + +import ( + test_environmentVariableGroups "github.com/cloudfoundry/cli/cf/api/environment_variable_groups/fakes" + . "github.com/cloudfoundry/cli/cf/commands/environmentvariablegroup" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("running-environment-variable-group command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + environmentVariableGroupRepo *test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + environmentVariableGroupRepo = &test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewRunningEnvironmentVariableGroup(ui, testconfig.NewRepositoryWithDefaults(), environmentVariableGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + environmentVariableGroupRepo.ListRunningReturns( + []models.EnvironmentVariable{ + models.EnvironmentVariable{Name: "abc", Value: "123"}, + models.EnvironmentVariable{Name: "def", Value: "456"}, + }, nil) + }) + + It("Displays the running environment variable group", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Retrieving the contents of the running environment variable group as my-user..."}, + []string{"OK"}, + []string{"Variable Name", "Assigned Value"}, + []string{"abc", "123"}, + []string{"def", "456"}, + )) + }) + }) +}) diff --git a/cf/commands/environmentvariablegroup/set_running_environment_variable_group.go b/cf/commands/environmentvariablegroup/set_running_environment_variable_group.go new file mode 100644 index 00000000000..3f986db72f2 --- /dev/null +++ b/cf/commands/environmentvariablegroup/set_running_environment_variable_group.go @@ -0,0 +1,65 @@ +package environmentvariablegroup + +import ( + "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + cf_errors "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetRunningEnvironmentVariableGroup struct { + ui terminal.UI + config core_config.ReadWriter + environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository +} + +func NewSetRunningEnvironmentVariableGroup(ui terminal.UI, config core_config.ReadWriter, environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository) (cmd SetRunningEnvironmentVariableGroup) { + cmd.ui = ui + cmd.config = config + cmd.environmentVariableGroupRepo = environmentVariableGroupRepo + return +} + +func (cmd SetRunningEnvironmentVariableGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-running-environment-variable-group", + Description: T("Pass parameters as JSON to create a running environment variable group"), + ShortName: "srevg", + Usage: T(`CF_NAME set-running-environment-variable-group '{"name":"value","name":"value"}'`), + } +} + +func (cmd SetRunningEnvironmentVariableGroup) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd SetRunningEnvironmentVariableGroup) Run(c *cli.Context) { + cmd.ui.Say(T("Setting the contents of the running environment variable group as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + err := cmd.environmentVariableGroupRepo.SetRunning(c.Args()[0]) + if err != nil { + suggestionText := "" + + httpError, ok := err.(cf_errors.HttpError) + if ok && httpError.ErrorCode() == cf_errors.PARSE_ERROR { + suggestionText = T(` + +Your JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{"name":"value","name":"value"}'`) + } + cmd.ui.Failed(err.Error() + suggestionText) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/environmentvariablegroup/set_running_environment_variable_group_test.go b/cf/commands/environmentvariablegroup/set_running_environment_variable_group_test.go new file mode 100644 index 00000000000..2839df2761f --- /dev/null +++ b/cf/commands/environmentvariablegroup/set_running_environment_variable_group_test.go @@ -0,0 +1,72 @@ +package environmentvariablegroup_test + +import ( + test_environmentVariableGroups "github.com/cloudfoundry/cli/cf/api/environment_variable_groups/fakes" + . "github.com/cloudfoundry/cli/cf/commands/environmentvariablegroup" + cf_errors "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("set-running-environment-variable-group command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + environmentVariableGroupRepo *test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + environmentVariableGroupRepo = &test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewSetRunningEnvironmentVariableGroup(ui, testconfig.NewRepositoryWithDefaults(), environmentVariableGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + + It("fails with usage when it does not receive any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("Sets the running environment variable group", func() { + runCommand(`{"abc":"123", "def": "456"}`) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting the contents of the running environment variable group as my-user..."}, + []string{"OK"}, + )) + Expect(environmentVariableGroupRepo.SetRunningArgsForCall(0)).To(Equal(`{"abc":"123", "def": "456"}`)) + }) + + It("Fails with a reasonable message when invalid JSON is passed", func() { + environmentVariableGroupRepo.SetRunningReturns(cf_errors.NewHttpError(400, cf_errors.PARSE_ERROR, "Request invalid due to parse error")) + runCommand(`{"abc":"123", "invalid : "json"}`) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting the contents of the running environment variable group as my-user..."}, + []string{"FAILED"}, + []string{`Your JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{"name":"value","name":"value"}'`}, + )) + }) + }) +}) diff --git a/cf/commands/environmentvariablegroup/set_staging_environment_variable_group.go b/cf/commands/environmentvariablegroup/set_staging_environment_variable_group.go new file mode 100644 index 00000000000..bb1f2cfc845 --- /dev/null +++ b/cf/commands/environmentvariablegroup/set_staging_environment_variable_group.go @@ -0,0 +1,65 @@ +package environmentvariablegroup + +import ( + "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + cf_errors "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetStagingEnvironmentVariableGroup struct { + ui terminal.UI + config core_config.ReadWriter + environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository +} + +func NewSetStagingEnvironmentVariableGroup(ui terminal.UI, config core_config.ReadWriter, environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository) (cmd SetStagingEnvironmentVariableGroup) { + cmd.ui = ui + cmd.config = config + cmd.environmentVariableGroupRepo = environmentVariableGroupRepo + return +} + +func (cmd SetStagingEnvironmentVariableGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-staging-environment-variable-group", + Description: T("Pass parameters as JSON to create a staging environment variable group"), + ShortName: "ssevg", + Usage: T(`CF_NAME set-staging-environment-variable-group '{"name":"value","name":"value"}'`), + } +} + +func (cmd SetStagingEnvironmentVariableGroup) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd SetStagingEnvironmentVariableGroup) Run(c *cli.Context) { + cmd.ui.Say(T("Setting the contents of the staging environment variable group as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + err := cmd.environmentVariableGroupRepo.SetStaging(c.Args()[0]) + if err != nil { + suggestionText := "" + + httpError, ok := err.(cf_errors.HttpError) + if ok && httpError.ErrorCode() == cf_errors.PARSE_ERROR { + suggestionText = T(` + +Your JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{"name":"value","name":"value"}'`) + } + cmd.ui.Failed(err.Error() + suggestionText) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/environmentvariablegroup/set_staging_environment_variable_group_test.go b/cf/commands/environmentvariablegroup/set_staging_environment_variable_group_test.go new file mode 100644 index 00000000000..2ce6ef772ac --- /dev/null +++ b/cf/commands/environmentvariablegroup/set_staging_environment_variable_group_test.go @@ -0,0 +1,72 @@ +package environmentvariablegroup_test + +import ( + test_environmentVariableGroups "github.com/cloudfoundry/cli/cf/api/environment_variable_groups/fakes" + . "github.com/cloudfoundry/cli/cf/commands/environmentvariablegroup" + cf_errors "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("set-staging-environment-variable-group command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + environmentVariableGroupRepo *test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + environmentVariableGroupRepo = &test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewSetStagingEnvironmentVariableGroup(ui, testconfig.NewRepositoryWithDefaults(), environmentVariableGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + + It("fails with usage when it does not receive any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("Sets the staging environment variable group", func() { + runCommand(`{"abc":"123", "def": "456"}`) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting the contents of the staging environment variable group as my-user..."}, + []string{"OK"}, + )) + Expect(environmentVariableGroupRepo.SetStagingArgsForCall(0)).To(Equal(`{"abc":"123", "def": "456"}`)) + }) + + It("Fails with a reasonable message when invalid JSON is passed", func() { + environmentVariableGroupRepo.SetStagingReturns(cf_errors.NewHttpError(400, cf_errors.PARSE_ERROR, "Request invalid due to parse error")) + runCommand(`{"abc":"123", "invalid : "json"}`) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting the contents of the staging environment variable group as my-user..."}, + []string{"FAILED"}, + []string{`Your JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{"name":"value","name":"value"}'`}, + )) + }) + }) +}) diff --git a/cf/commands/environmentvariablegroup/staging_environment_variable_group.go b/cf/commands/environmentvariablegroup/staging_environment_variable_group.go new file mode 100644 index 00000000000..af0fc4b2388 --- /dev/null +++ b/cf/commands/environmentvariablegroup/staging_environment_variable_group.go @@ -0,0 +1,61 @@ +package environmentvariablegroup + +import ( + "github.com/cloudfoundry/cli/cf/api/environment_variable_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type StagingEnvironmentVariableGroup struct { + ui terminal.UI + config core_config.ReadWriter + environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository +} + +func NewStagingEnvironmentVariableGroup(ui terminal.UI, config core_config.ReadWriter, environmentVariableGroupRepo environment_variable_groups.EnvironmentVariableGroupsRepository) (cmd StagingEnvironmentVariableGroup) { + cmd.ui = ui + cmd.config = config + cmd.environmentVariableGroupRepo = environmentVariableGroupRepo + return +} + +func (cmd StagingEnvironmentVariableGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "staging-environment-variable-group", + Description: T("Retrieve the contents of the staging environment variable group"), + ShortName: "sevg", + Usage: T("CF_NAME staging-environment-variable-group"), + } +} + +func (cmd StagingEnvironmentVariableGroup) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd StagingEnvironmentVariableGroup) Run(c *cli.Context) { + cmd.ui.Say(T("Retrieving the contents of the staging environment variable group as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + stagingEnvVars, err := cmd.environmentVariableGroupRepo.ListStaging() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + + table := terminal.NewTable(cmd.ui, []string{T("Variable Name"), T("Assigned Value")}) + for _, envVar := range stagingEnvVars { + table.Add(envVar.Name, envVar.Value) + } + table.Print() +} diff --git a/cf/commands/environmentvariablegroup/staging_environment_variable_group_test.go b/cf/commands/environmentvariablegroup/staging_environment_variable_group_test.go new file mode 100644 index 00000000000..cdd8e98533c --- /dev/null +++ b/cf/commands/environmentvariablegroup/staging_environment_variable_group_test.go @@ -0,0 +1,68 @@ +package environmentvariablegroup_test + +import ( + test_environmentVariableGroups "github.com/cloudfoundry/cli/cf/api/environment_variable_groups/fakes" + . "github.com/cloudfoundry/cli/cf/commands/environmentvariablegroup" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("staging-environment-variable-group command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + environmentVariableGroupRepo *test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + environmentVariableGroupRepo = &test_environmentVariableGroups.FakeEnvironmentVariableGroupsRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewStagingEnvironmentVariableGroup(ui, testconfig.NewRepositoryWithDefaults(), environmentVariableGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + environmentVariableGroupRepo.ListStagingReturns( + []models.EnvironmentVariable{ + models.EnvironmentVariable{Name: "abc", Value: "123"}, + models.EnvironmentVariable{Name: "def", Value: "456"}, + }, nil) + }) + + It("Displays the staging environment variable group", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Retrieving the contents of the staging environment variable group as my-user..."}, + []string{"OK"}, + []string{"Variable Name", "Assigned Value"}, + []string{"abc", "123"}, + []string{"def", "456"}, + )) + }) + }) +}) diff --git a/cf/commands/featureflag/disable_feature_flag.go b/cf/commands/featureflag/disable_feature_flag.go new file mode 100644 index 00000000000..8b273c2da2d --- /dev/null +++ b/cf/commands/featureflag/disable_feature_flag.go @@ -0,0 +1,67 @@ +package featureflag + +import ( + "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DisableFeatureFlag struct { + ui terminal.UI + config core_config.ReadWriter + flagRepo feature_flags.FeatureFlagRepository +} + +func NewDisableFeatureFlag( + ui terminal.UI, + config core_config.ReadWriter, + flagRepo feature_flags.FeatureFlagRepository) (cmd DisableFeatureFlag) { + return DisableFeatureFlag{ + ui: ui, + config: config, + flagRepo: flagRepo, + } +} + +func (cmd DisableFeatureFlag) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "disable-feature-flag", + Description: T("Disable the use of a feature so that users have access to and can use the feature."), + Usage: T("CF_NAME disable-feature-flag FEATURE_NAME"), + } +} + +func (cmd DisableFeatureFlag) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd DisableFeatureFlag) Run(c *cli.Context) { + flag := c.Args()[0] + + cmd.ui.Say(T("Setting status of {{.FeatureFlag}} as {{.Username}}...", map[string]interface{}{ + "FeatureFlag": terminal.EntityNameColor(flag), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.flagRepo.Update(flag, false) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Say("") + cmd.ui.Ok() + cmd.ui.Say("") + cmd.ui.Say(T("Feature {{.FeatureFlag}} Disabled.", map[string]interface{}{ + "FeatureFlag": terminal.EntityNameColor(flag)})) + return +} diff --git a/cf/commands/featureflag/disable_feature_flag_test.go b/cf/commands/featureflag/disable_feature_flag_test.go new file mode 100644 index 00000000000..b1db57a45e6 --- /dev/null +++ b/cf/commands/featureflag/disable_feature_flag_test.go @@ -0,0 +1,80 @@ +package featureflag_test + +import ( + "errors" + + fakeflag "github.com/cloudfoundry/cli/cf/api/feature_flags/fakes" + . "github.com/cloudfoundry/cli/cf/commands/featureflag" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("disable-feature-flag command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + flagRepo *fakeflag.FakeFeatureFlagRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + flagRepo = &fakeflag.FakeFeatureFlagRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewDisableFeatureFlag(ui, testconfig.NewRepositoryWithDefaults(), flagRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + + It("fails with usage if a single feature is not specified", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + flagRepo.UpdateReturns(nil) + }) + + It("Sets the flag", func() { + runCommand("user_org_creation") + + flag, set := flagRepo.UpdateArgsForCall(0) + Expect(flag).To(Equal("user_org_creation")) + Expect(set).To(BeFalse()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting status of user_org_creation as my-user..."}, + []string{"OK"}, + []string{"Feature user_org_creation Disabled."}, + )) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + flagRepo.UpdateReturns(errors.New("An error occurred.")) + }) + + It("fails with an error", func() { + runCommand("i-dont-exist") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"An error occurred."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/featureflag/enable_feature_flag.go b/cf/commands/featureflag/enable_feature_flag.go new file mode 100644 index 00000000000..66d7ed7409a --- /dev/null +++ b/cf/commands/featureflag/enable_feature_flag.go @@ -0,0 +1,67 @@ +package featureflag + +import ( + "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type EnableFeatureFlag struct { + ui terminal.UI + config core_config.ReadWriter + flagRepo feature_flags.FeatureFlagRepository +} + +func NewEnableFeatureFlag( + ui terminal.UI, + config core_config.ReadWriter, + flagRepo feature_flags.FeatureFlagRepository) (cmd EnableFeatureFlag) { + return EnableFeatureFlag{ + ui: ui, + config: config, + flagRepo: flagRepo, + } +} + +func (cmd EnableFeatureFlag) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "enable-feature-flag", + Description: T("Enable the use of a feature so that users have access to and can use the feature."), + Usage: T("CF_NAME enable-feature-flag FEATURE_NAME"), + } +} + +func (cmd EnableFeatureFlag) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd EnableFeatureFlag) Run(c *cli.Context) { + flag := c.Args()[0] + + cmd.ui.Say(T("Setting status of {{.FeatureFlag}} as {{.Username}}...", map[string]interface{}{ + "FeatureFlag": terminal.EntityNameColor(flag), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.flagRepo.Update(flag, true) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Say("") + cmd.ui.Ok() + cmd.ui.Say("") + cmd.ui.Say(T("Feature {{.FeatureFlag}} Enabled.", map[string]interface{}{ + "FeatureFlag": terminal.EntityNameColor(flag)})) + return +} diff --git a/cf/commands/featureflag/enable_feature_flag_test.go b/cf/commands/featureflag/enable_feature_flag_test.go new file mode 100644 index 00000000000..106e92c7469 --- /dev/null +++ b/cf/commands/featureflag/enable_feature_flag_test.go @@ -0,0 +1,80 @@ +package featureflag_test + +import ( + "errors" + + fakeflag "github.com/cloudfoundry/cli/cf/api/feature_flags/fakes" + . "github.com/cloudfoundry/cli/cf/commands/featureflag" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("enable-feature-flag command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + flagRepo *fakeflag.FakeFeatureFlagRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + flagRepo = &fakeflag.FakeFeatureFlagRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewEnableFeatureFlag(ui, testconfig.NewRepositoryWithDefaults(), flagRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + + It("fails with usage if a single feature is not specified", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + flagRepo.UpdateReturns(nil) + }) + + It("Sets the flag", func() { + runCommand("user_org_creation") + + flag, set := flagRepo.UpdateArgsForCall(0) + Expect(flag).To(Equal("user_org_creation")) + Expect(set).To(BeTrue()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting status of user_org_creation as my-user..."}, + []string{"OK"}, + []string{"Feature user_org_creation Enabled."}, + )) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + flagRepo.UpdateReturns(errors.New("An error occurred.")) + }) + + It("fails with an error", func() { + runCommand("i-dont-exist") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"An error occurred."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/featureflag/feature_flag.go b/cf/commands/featureflag/feature_flag.go new file mode 100644 index 00000000000..f516b4afe2f --- /dev/null +++ b/cf/commands/featureflag/feature_flag.go @@ -0,0 +1,78 @@ +package featureflag + +import ( + "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ShowFeatureFlag struct { + ui terminal.UI + config core_config.ReadWriter + flagRepo feature_flags.FeatureFlagRepository +} + +func NewShowFeatureFlag( + ui terminal.UI, + config core_config.ReadWriter, + flagRepo feature_flags.FeatureFlagRepository) (cmd ShowFeatureFlag) { + return ShowFeatureFlag{ + ui: ui, + config: config, + flagRepo: flagRepo, + } +} + +func (cmd ShowFeatureFlag) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "feature-flag", + Description: T("Retrieve an individual feature flag with status"), + Usage: T("CF_NAME feature-flag FEATURE_NAME"), + } +} + +func (cmd ShowFeatureFlag) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd ShowFeatureFlag) Run(c *cli.Context) { + flagName := c.Args()[0] + + cmd.ui.Say(T("Retrieving status of {{.FeatureFlag}} as {{.Username}}...", map[string]interface{}{ + "FeatureFlag": terminal.EntityNameColor(flagName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + flag, err := cmd.flagRepo.FindByName(flagName) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("Features"), T("State")}) + table.Add(flag.Name, cmd.flagBoolToString(flag.Enabled)) + + table.Print() + return +} + +func (cmd ShowFeatureFlag) flagBoolToString(enabled bool) string { + if enabled { + return "enabled" + } else { + return "disabled" + } +} diff --git a/cf/commands/featureflag/feature_flag_test.go b/cf/commands/featureflag/feature_flag_test.go new file mode 100644 index 00000000000..90ae4024b81 --- /dev/null +++ b/cf/commands/featureflag/feature_flag_test.go @@ -0,0 +1,80 @@ +package featureflag_test + +import ( + "errors" + + fakeflag "github.com/cloudfoundry/cli/cf/api/feature_flags/fakes" + . "github.com/cloudfoundry/cli/cf/commands/featureflag" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("feature-flag command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + flagRepo *fakeflag.FakeFeatureFlagRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + flagRepo = &fakeflag.FakeFeatureFlagRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewShowFeatureFlag(ui, testconfig.NewRepositoryWithDefaults(), flagRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("foo")).ToNot(HavePassedRequirements()) + }) + + It("requires the user to provide a feature flag", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + flag := models.FeatureFlag{ + Name: "route_creation", + Enabled: false, + } + flagRepo.FindByNameReturns(flag, nil) + }) + + It("lists the state of the specified feature flag", func() { + runCommand("route_creation") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Retrieving status of route_creation as my-user..."}, + []string{"Feature", "State"}, + []string{"route_creation", "disabled"}, + )) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + flagRepo.FindByNameReturns(models.FeatureFlag{}, errors.New("An error occurred.")) + }) + + It("fails with an error", func() { + runCommand("route_creation") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"An error occurred."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/featureflag/feature_flags.go b/cf/commands/featureflag/feature_flags.go new file mode 100644 index 00000000000..8a0b4d6e762 --- /dev/null +++ b/cf/commands/featureflag/feature_flags.go @@ -0,0 +1,80 @@ +package featureflag + +import ( + "github.com/cloudfoundry/cli/cf/api/feature_flags" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListFeatureFlags struct { + ui terminal.UI + config core_config.ReadWriter + flagRepo feature_flags.FeatureFlagRepository +} + +func NewListFeatureFlags( + ui terminal.UI, + config core_config.ReadWriter, + flagRepo feature_flags.FeatureFlagRepository) (cmd ListFeatureFlags) { + return ListFeatureFlags{ + ui: ui, + config: config, + flagRepo: flagRepo, + } +} + +func (cmd ListFeatureFlags) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "feature-flags", + Description: T("Retrieve list of feature flags with status of each flag-able feature"), + Usage: T("CF_NAME feature-flags"), + } +} + +func (cmd ListFeatureFlags) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs := []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return reqs, nil +} + +func (cmd ListFeatureFlags) Run(c *cli.Context) { + cmd.ui.Say(T("Retrieving status of all flagged features as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + flags, err := cmd.flagRepo.List() + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("Features"), T("State")}) + + for _, flag := range flags { + table.Add( + flag.Name, + cmd.flagBoolToString(flag.Enabled), + ) + } + + table.Print() + return +} + +func (cmd ListFeatureFlags) flagBoolToString(enabled bool) string { + if enabled { + return "enabled" + } else { + return "disabled" + } +} diff --git a/cf/commands/featureflag/feature_flags_test.go b/cf/commands/featureflag/feature_flags_test.go new file mode 100644 index 00000000000..ec04d32a80f --- /dev/null +++ b/cf/commands/featureflag/feature_flags_test.go @@ -0,0 +1,103 @@ +package featureflag_test + +import ( + "errors" + + fakeflag "github.com/cloudfoundry/cli/cf/api/feature_flags/fakes" + . "github.com/cloudfoundry/cli/cf/commands/featureflag" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("feature-flags command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + flagRepo *fakeflag.FakeFeatureFlagRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + flagRepo = &fakeflag.FakeFeatureFlagRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewListFeatureFlags(ui, testconfig.NewRepositoryWithDefaults(), flagRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + flags := []models.FeatureFlag{ + models.FeatureFlag{ + Name: "user_org_creation", + Enabled: true, + ErrorMessage: "error", + }, + models.FeatureFlag{ + Name: "private_domain_creation", + Enabled: false, + }, + models.FeatureFlag{ + Name: "app_bits_upload", + Enabled: true, + }, + models.FeatureFlag{ + Name: "app_scaling", + Enabled: true, + }, + models.FeatureFlag{ + Name: "route_creation", + Enabled: false, + }, + } + flagRepo.ListReturns(flags, nil) + }) + + It("lists the state of all feature flags", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Retrieving status of all flagged features as my-user..."}, + []string{"Feature", "State"}, + []string{"user_org_creation", "enabled"}, + []string{"private_domain_creation", "disabled"}, + []string{"app_bits_upload", "enabled"}, + []string{"app_scaling", "enabled"}, + []string{"route_creation", "disabled"}, + )) + }) + + Context("when an error occurs", func() { + BeforeEach(func() { + flagRepo.ListReturns(nil, errors.New("An error occurred.")) + }) + + It("fails with an error", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"An error occurred."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/featureflag/featureflag_suite_test.go b/cf/commands/featureflag/featureflag_suite_test.go new file mode 100644 index 00000000000..8121c2985e2 --- /dev/null +++ b/cf/commands/featureflag/featureflag_suite_test.go @@ -0,0 +1,19 @@ +package featureflag_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFeatureflag(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "FeatureFlag Suite") +} diff --git a/cf/commands/login.go b/cf/commands/login.go new file mode 100644 index 00000000000..978757de3aa --- /dev/null +++ b/cf/commands/login.go @@ -0,0 +1,351 @@ +package commands + +import ( + "strconv" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +const maxLoginTries = 3 +const maxChoices = 50 + +type Login struct { + ui terminal.UI + config core_config.ReadWriter + authenticator authentication.AuthenticationRepository + endpointRepo api.EndpointRepository + orgRepo organizations.OrganizationRepository + spaceRepo spaces.SpaceRepository +} + +func NewLogin(ui terminal.UI, + config core_config.ReadWriter, + authenticator authentication.AuthenticationRepository, + endpointRepo api.EndpointRepository, + orgRepo organizations.OrganizationRepository, + spaceRepo spaces.SpaceRepository) (cmd Login) { + return Login{ + ui: ui, + config: config, + authenticator: authenticator, + endpointRepo: endpointRepo, + orgRepo: orgRepo, + spaceRepo: spaceRepo, + } +} + +func (cmd Login) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "login", + ShortName: "l", + Description: T("Log user in"), + Usage: T("CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n") + + terminal.WarningColor(T("WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n")) + T("EXAMPLE:\n") + T(" CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n") + T(" CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n") + T(" CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n") + T(" CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)") + T(" CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("a", T("API endpoint (e.g. https://api.example.com)")), + flag_helpers.NewStringFlag("u", T("Username")), + flag_helpers.NewStringFlag("p", T("Password")), + flag_helpers.NewStringFlag("o", T("Org")), + flag_helpers.NewStringFlag("s", T("Space")), + cli.BoolFlag{Name: "sso", Usage: T("Use a one-time password to login")}, + cli.BoolFlag{Name: "skip-ssl-validation", Usage: T("Please don't")}, + }, + } +} + +func (cmd Login) GetRequirements(_ requirements.Factory, _ *cli.Context) (reqs []requirements.Requirement, err error) { + return +} + +func (cmd Login) Run(c *cli.Context) { + cmd.config.ClearSession() + + endpoint, skipSSL := cmd.decideEndpoint(c) + NewApi(cmd.ui, cmd.config, cmd.endpointRepo).setApiEndpoint(endpoint, skipSSL, cmd.Metadata().Name) + + defer func() { + cmd.ui.Say("") + cmd.ui.ShowConfiguration(cmd.config) + }() + + // We thought we would never need to explicitly branch in this code + // for anything as simple as authentication, but it turns out that our + // assumptions did not match reality. + + // When SAML is enabled (but not configured) then the UAA/Login server + // will always returns password prompts that includes the Passcode field. + // Users can authenticate with: + // EITHER username and password + // OR a one-time passcode + + if c.Bool("sso") { + cmd.authenticateSSO(c) + } else { + cmd.authenticate(c) + } + + orgIsSet := cmd.setOrganization(c) + + if orgIsSet { + cmd.setSpace(c) + } +} + +func (cmd Login) decideEndpoint(c *cli.Context) (string, bool) { + endpoint := c.String("a") + skipSSL := c.Bool("skip-ssl-validation") + if endpoint == "" { + endpoint = cmd.config.ApiEndpoint() + skipSSL = cmd.config.IsSSLDisabled() || skipSSL + } + + if endpoint == "" { + endpoint = cmd.ui.Ask(T("API endpoint")) + } else { + cmd.ui.Say(T("API endpoint: {{.Endpoint}}", map[string]interface{}{"Endpoint": terminal.EntityNameColor(endpoint)})) + } + + return endpoint, skipSSL +} + +func (cmd Login) authenticateSSO(c *cli.Context) { + prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + credentials := make(map[string]string) + passcode := prompts["passcode"] + + for i := 0; i < maxLoginTries; i++ { + credentials["passcode"] = cmd.ui.AskForPassword("%s", passcode.DisplayName) + + cmd.ui.Say(T("Authenticating...")) + err = cmd.authenticator.Authenticate(credentials) + + if err == nil { + cmd.ui.Ok() + cmd.ui.Say("") + break + } + + cmd.ui.Say(err.Error()) + } + + if err != nil { + cmd.ui.Failed(T("Unable to authenticate.")) + } +} + +func (cmd Login) authenticate(c *cli.Context) { + usernameFlagValue := c.String("u") + passwordFlagValue := c.String("p") + + prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL() + if err != nil { + cmd.ui.Failed(err.Error()) + } + passwordKeys := []string{} + credentials := make(map[string]string) + + if value, ok := prompts["username"]; ok { + if prompts["username"].Type == core_config.AuthPromptTypeText && usernameFlagValue != "" { + credentials["username"] = usernameFlagValue + } else { + credentials["username"] = cmd.ui.Ask("%s", value.DisplayName) + } + } + + for key, prompt := range prompts { + if prompt.Type == core_config.AuthPromptTypePassword { + if key == "passcode" { + continue + } + + passwordKeys = append(passwordKeys, key) + } else if key == "username" { + continue + } else { + credentials[key] = cmd.ui.Ask("%s", prompt.DisplayName) + } + } + + for i := 0; i < maxLoginTries; i++ { + for _, key := range passwordKeys { + if key == "password" && passwordFlagValue != "" { + credentials[key] = passwordFlagValue + passwordFlagValue = "" + } else { + credentials[key] = cmd.ui.AskForPassword("%s", prompts[key].DisplayName) + } + } + + cmd.ui.Say(T("Authenticating...")) + err = cmd.authenticator.Authenticate(credentials) + + if err == nil { + cmd.ui.Ok() + cmd.ui.Say("") + break + } + + cmd.ui.Say(err.Error()) + } + + if err != nil { + cmd.ui.Failed(T("Unable to authenticate.")) + } +} + +func (cmd Login) setOrganization(c *cli.Context) (isOrgSet bool) { + orgName := c.String("o") + + if orgName == "" { + availableOrgs := []models.Organization{} + orgs, apiErr := cmd.orgRepo.ListOrgs() + if apiErr != nil { + cmd.ui.Failed(T("Error finding available orgs\n{{.ApiErr}}", + map[string]interface{}{"ApiErr": apiErr.Error()})) + } + for _, org := range orgs { + if len(availableOrgs) < maxChoices { + availableOrgs = append(availableOrgs, org) + } + } + + if len(availableOrgs) == 0 { + return false + } else if len(availableOrgs) == 1 { + cmd.targetOrganization(availableOrgs[0]) + return true + } else { + orgName = cmd.promptForOrgName(availableOrgs) + if orgName == "" { + cmd.ui.Say("") + return false + } + } + } + + org, err := cmd.orgRepo.FindByName(orgName) + if err != nil { + cmd.ui.Failed(T("Error finding org {{.OrgName}}\n{{.Err}}", + map[string]interface{}{"OrgName": terminal.EntityNameColor(orgName), "Err": err.Error()})) + } + + cmd.targetOrganization(org) + return true +} + +func (cmd Login) promptForOrgName(orgs []models.Organization) string { + orgNames := []string{} + for _, org := range orgs { + orgNames = append(orgNames, org.Name) + } + + return cmd.promptForName(orgNames, T("Select an org (or press enter to skip):"), "Org") +} + +func (cmd Login) targetOrganization(org models.Organization) { + cmd.config.SetOrganizationFields(org.OrganizationFields) + cmd.ui.Say(T("Targeted org {{.OrgName}}\n", + map[string]interface{}{"OrgName": terminal.EntityNameColor(org.Name)})) +} + +func (cmd Login) setSpace(c *cli.Context) { + spaceName := c.String("s") + + if spaceName == "" { + var availableSpaces []models.Space + err := cmd.spaceRepo.ListSpaces(func(space models.Space) bool { + availableSpaces = append(availableSpaces, space) + return (len(availableSpaces) < maxChoices) + }) + if err != nil { + cmd.ui.Failed(T("Error finding available spaces\n{{.Err}}", + map[string]interface{}{"Err": err.Error()})) + } + + if len(availableSpaces) == 0 { + return + } else if len(availableSpaces) == 1 { + cmd.targetSpace(availableSpaces[0]) + return + } else { + spaceName = cmd.promptForSpaceName(availableSpaces) + if spaceName == "" { + cmd.ui.Say("") + return + } + } + } + + space, err := cmd.spaceRepo.FindByName(spaceName) + if err != nil { + cmd.ui.Failed(T("Error finding space {{.SpaceName}}\n{{.Err}}", + map[string]interface{}{"SpaceName": terminal.EntityNameColor(spaceName), "Err": err.Error()})) + } + + cmd.targetSpace(space) +} + +func (cmd Login) promptForSpaceName(spaces []models.Space) string { + spaceNames := []string{} + for _, space := range spaces { + spaceNames = append(spaceNames, space.Name) + } + + return cmd.promptForName(spaceNames, T("Select a space (or press enter to skip):"), "Space") +} + +func (cmd Login) targetSpace(space models.Space) { + cmd.config.SetSpaceFields(space.SpaceFields) + cmd.ui.Say(T("Targeted space {{.SpaceName}}\n", + map[string]interface{}{"SpaceName": terminal.EntityNameColor(space.Name)})) +} + +func (cmd Login) promptForName(names []string, listPrompt, itemPrompt string) string { + nameIndex := 0 + var nameString string + for nameIndex < 1 || nameIndex > len(names) { + var err error + + // list header + cmd.ui.Say(listPrompt) + + // only display list if it is shorter than maxChoices + if len(names) < maxChoices { + for i, name := range names { + cmd.ui.Say("%d. %s", i+1, name) + } + } else { + cmd.ui.Say(T("There are too many options to display, please type in the name.")) + } + + nameString = cmd.ui.Ask("%s", itemPrompt) + if nameString == "" { + return "" + } + + nameIndex, err = strconv.Atoi(nameString) + + if err != nil { + nameIndex = 1 + return nameString + } + } + + return names[nameIndex-1] +} diff --git a/cf/commands/login_test.go b/cf/commands/login_test.go new file mode 100644 index 00000000000..1ed92aa6869 --- /dev/null +++ b/cf/commands/login_test.go @@ -0,0 +1,732 @@ +package commands_test + +import ( + "strconv" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + fake_organizations "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("Login Command", func() { + var ( + Flags []string + Config core_config.ReadWriter + ui *testterm.FakeUI + authRepo *testapi.FakeAuthenticationRepository + endpointRepo *testapi.FakeEndpointRepo + orgRepo *fake_organizations.FakeOrganizationRepository + spaceRepo *testapi.FakeSpaceRepository + + org models.Organization + ) + + BeforeEach(func() { + Flags = []string{} + Config = testconfig.NewRepository() + ui = &testterm.FakeUI{} + authRepo = &testapi.FakeAuthenticationRepository{ + AccessToken: "my_access_token", + RefreshToken: "my_refresh_token", + Config: Config, + } + endpointRepo = &testapi.FakeEndpointRepo{} + + org = models.Organization{} + org.Name = "my-new-org" + org.Guid = "my-new-org-guid" + + orgRepo = &fake_organizations.FakeOrganizationRepository{} + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + + space := models.Space{} + space.Guid = "my-space-guid" + space.Name = "my-space" + + spaceRepo = &testapi.FakeSpaceRepository{ + Spaces: []models.Space{space}, + } + + authRepo.GetLoginPromptsReturns.Prompts = map[string]core_config.AuthPrompt{ + "username": core_config.AuthPrompt{ + DisplayName: "Username", + Type: core_config.AuthPromptTypeText, + }, + "password": core_config.AuthPrompt{ + DisplayName: "Password", + Type: core_config.AuthPromptTypePassword, + }, + } + }) + + Context("interactive usage", func() { + Describe("when there are a small number of organizations and spaces", func() { + var org2 models.Organization + var space2 models.Space + + BeforeEach(func() { + org1 := models.Organization{} + org1.Guid = "some-org-guid" + org1.Name = "some-org" + + org2 = models.Organization{} + org2.Guid = "my-new-org-guid" + org2.Name = "my-new-org" + + space1 := models.Space{} + space1.Guid = "my-space-guid" + space1.Name = "my-space" + + space2 = models.Space{} + space2.Guid = "some-space-guid" + space2.Name = "some-space" + + orgRepo.ListOrgsReturns([]models.Organization{org1, org2}, nil) + spaceRepo.Spaces = []models.Space{space1, space2} + }) + + It("lets the user select an org and space by number", func() { + orgRepo.FindByNameReturns(org2, nil) + OUT_OF_RANGE_CHOICE := "3" + ui.Inputs = []string{"api.example.com", "user@example.com", "password", OUT_OF_RANGE_CHOICE, "2", OUT_OF_RANGE_CHOICE, "1"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Select an org"}, + []string{"1. some-org"}, + []string{"2. my-new-org"}, + []string{"Select a space"}, + []string{"1. my-space"}, + []string{"2. some-space"}, + )) + + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("api.example.com")) + + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-new-org")) + Expect(spaceRepo.FindByNameName).To(Equal("my-space")) + + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("lets the user select an org and space by name", func() { + ui.Inputs = []string{"api.example.com", "user@example.com", "password", "my-new-org", "my-space"} + orgRepo.FindByNameReturns(org2, nil) + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Select an org"}, + []string{"1. some-org"}, + []string{"2. my-new-org"}, + []string{"Select a space"}, + []string{"1. my-space"}, + []string{"2. some-space"}, + )) + + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("api.example.com")) + + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-new-org")) + Expect(spaceRepo.FindByNameName).To(Equal("my-space")) + + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("lets the user specify an org and space using flags", func() { + Flags = []string{"-a", "api.example.com", "-u", "user@example.com", "-p", "password", "-o", "my-new-org", "-s", "my-space"} + + orgRepo.FindByNameReturns(org2, nil) + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("api.example.com")) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "user@example.com", + "password": "password", + }, + })) + + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("doesn't ask the user for the API url if they have it in their config", func() { + orgRepo.FindByNameReturns(org, nil) + Config.SetApiEndpoint("http://api.example.com") + + Flags = []string{"-o", "my-new-org", "-s", "my-space"} + ui.Inputs = []string{"user@example.com", "password"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(Config.ApiEndpoint()).To(Equal("http://api.example.com")) + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("http://api.example.com")) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + }) + + Describe("when there are too many orgs to show", func() { + BeforeEach(func() { + organizations := []models.Organization{} + for i := 0; i < 60; i++ { + id := strconv.Itoa(i) + org := models.Organization{} + org.Guid = "my-org-guid-" + id + org.Name = "my-org-" + id + organizations = append(organizations, org) + } + orgRepo.ListOrgsReturns(organizations, nil) + orgRepo.FindByNameReturns(organizations[1], nil) + + space1 := models.Space{} + space1.Guid = "my-space-guid" + space1.Name = "my-space" + + space2 := models.Space{} + space2.Guid = "some-space-guid" + space2.Name = "some-space" + + spaceRepo.Spaces = []models.Space{space1, space2} + }) + + It("doesn't display a list of orgs (the user must type the name)", func() { + ui.Inputs = []string{"api.example.com", "user@example.com", "password", "my-org-1", "my-space"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"my-org-2"})) + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-org-1")) + Expect(Config.OrganizationFields().Guid).To(Equal("my-org-guid-1")) + }) + }) + + Describe("when there is only a single org and space", func() { + It("does not ask the user to select an org/space", func() { + ui.Inputs = []string{"http://api.example.com", "user@example.com", "password"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("http://api.example.com")) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "user@example.com", + "password": "password", + }, + })) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + }) + + Describe("where there are no available orgs", func() { + BeforeEach(func() { + orgRepo.ListOrgsReturns([]models.Organization{}, nil) + spaceRepo.Spaces = []models.Space{} + }) + + It("does not as the user to select an org", func() { + ui.Inputs = []string{"http://api.example.com", "user@example.com", "password"} + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(Config.OrganizationFields().Guid).To(Equal("")) + Expect(Config.SpaceFields().Guid).To(Equal("")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("http://api.example.com")) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "user@example.com", + "password": "password", + }, + })) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + }) + + Describe("when there is only a single org and no spaces", func() { + BeforeEach(func() { + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + spaceRepo.Spaces = []models.Space{} + }) + + It("does not ask the user to select a space", func() { + ui.Inputs = []string{"http://api.example.com", "user@example.com", "password"} + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("")) + Expect(Config.AccessToken()).To(Equal("my_access_token")) + Expect(Config.RefreshToken()).To(Equal("my_refresh_token")) + + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("http://api.example.com")) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "user@example.com", + "password": "password", + }, + })) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + }) + + Describe("login prompts", func() { + BeforeEach(func() { + authRepo.GetLoginPromptsReturns.Prompts = map[string]core_config.AuthPrompt{ + "account_number": core_config.AuthPrompt{ + DisplayName: "Account Number", + Type: core_config.AuthPromptTypeText, + }, + "username": core_config.AuthPrompt{ + DisplayName: "Username", + Type: core_config.AuthPromptTypeText, + }, + "passcode": core_config.AuthPrompt{ + DisplayName: "It's a passcode, what you want it to be???", + Type: core_config.AuthPromptTypePassword, + }, + "password": core_config.AuthPrompt{ + DisplayName: "Your Password", + Type: core_config.AuthPromptTypePassword, + }, + } + }) + + Context("when the user does not provide the --sso flag", func() { + It("prompts the user for 'password' prompt and any text type prompt", func() { + ui.Inputs = []string{"api.example.com", "the-username", "the-account-number", "the-password"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"API endpoint"}, + []string{"Account Number"}, + []string{"Username"}, + )) + Expect(ui.PasswordPrompts).To(ContainSubstrings([]string{"Your Password"})) + Expect(ui.PasswordPrompts).ToNot(ContainSubstrings( + []string{"passcode"}, + )) + + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "account_number": "the-account-number", + "username": "the-username", + "password": "the-password", + }, + })) + }) + }) + + Context("when the user does provide the --sso flag", func() { + It("only prompts the user for the passcode type prompts", func() { + Flags = []string{"--sso", "-a", "api.example.com"} + ui.Inputs = []string{"the-one-time-code"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.PasswordPrompts).To(ContainSubstrings([]string{"passcode"})) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "passcode": "the-one-time-code", + }, + })) + }) + }) + + It("takes the password from the -p flag", func() { + Flags = []string{"-p", "the-password"} + ui.Inputs = []string{"api.example.com", "the-username", "the-account-number", "the-pin"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(ui.PasswordPrompts).ToNot(ContainSubstrings([]string{"Your Password"})) + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "account_number": "the-account-number", + "username": "the-username", + "password": "the-password", + }, + })) + }) + + It("tries 3 times for the password-type prompts", func() { + authRepo.AuthError = true + ui.Inputs = []string{"api.example.com", "the-username", "the-account-number", + "the-password-1", "the-password-2", "the-password-3"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "username": "the-username", + "account_number": "the-account-number", + "password": "the-password-1", + }, + { + "username": "the-username", + "account_number": "the-account-number", + "password": "the-password-2", + }, + { + "username": "the-username", + "account_number": "the-account-number", + "password": "the-password-3", + }, + })) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + + It("prompts user for password again if password given on the cmd line fails", func() { + authRepo.AuthError = true + + Flags = []string{"-p", "the-password-1"} + + ui.Inputs = []string{"api.example.com", "the-username", "the-account-number", + "the-password-2", "the-password-3"} + + l := NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + testcmd.RunCommand(l, Flags, nil) + + Expect(authRepo.AuthenticateArgs.Credentials).To(Equal([]map[string]string{ + { + "account_number": "the-account-number", + "username": "the-username", + "password": "the-password-1", + }, + { + "account_number": "the-account-number", + "username": "the-username", + "password": "the-password-2", + }, + { + "account_number": "the-account-number", + "username": "the-username", + "password": "the-password-3", + }, + })) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + }) + + Describe("updates to the config", func() { + var l Login + + BeforeEach(func() { + Config.SetApiEndpoint("api.the-old-endpoint.com") + Config.SetAccessToken("the-old-access-token") + Config.SetRefreshToken("the-old-refresh-token") + l = NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + }) + + JustBeforeEach(func() { + testcmd.RunCommand(l, Flags, nil) + }) + + var ItShowsTheTarget = func() { + It("shows the target", func() { + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + } + + var ItDoesntShowTheTarget = func() { + It("does not show the target info", func() { + Expect(ui.ShowConfigurationCalled).To(BeFalse()) + }) + } + + var ItFails = func() { + It("fails", func() { + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + } + + var ItSucceeds = func() { + It("runs successfully", func() { + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"OK"})) + }) + } + + Describe("when the user is setting an API", func() { + BeforeEach(func() { + l = NewLogin(ui, Config, authRepo, endpointRepo, orgRepo, spaceRepo) + Flags = []string{"-a", "https://api.the-server.com", "-u", "the-user-name", "-p", "the-password"} + }) + + Describe("when the --skip-ssl-validation flag is provided", func() { + BeforeEach(func() { + Flags = append(Flags, "--skip-ssl-validation") + }) + + Describe("setting api endpoint is successful", func() { + BeforeEach(func() { + Config.SetSSLDisabled(false) + }) + + ItSucceeds() + ItShowsTheTarget() + + It("stores the API endpoint and the skip-ssl flag", func() { + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("https://api.the-server.com")) + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + }) + + Describe("setting api endpoint failed", func() { + BeforeEach(func() { + Config.SetSSLDisabled(true) + endpointRepo.UpdateEndpointError = errors.New("API endpoint not found") + }) + + ItFails() + ItDoesntShowTheTarget() + + It("clears the entire config", func() { + Expect(Config.ApiEndpoint()).To(BeEmpty()) + Expect(Config.IsSSLDisabled()).To(BeFalse()) + Expect(Config.AccessToken()).To(BeEmpty()) + Expect(Config.RefreshToken()).To(BeEmpty()) + Expect(Config.OrganizationFields().Guid).To(BeEmpty()) + Expect(Config.SpaceFields().Guid).To(BeEmpty()) + }) + }) + }) + + Describe("when the --skip-ssl-validation flag is not provided", func() { + Describe("setting api endpoint is successful", func() { + BeforeEach(func() { + Config.SetSSLDisabled(true) + }) + + ItSucceeds() + ItShowsTheTarget() + + It("updates the API endpoint and enables SSL validation", func() { + Expect(endpointRepo.UpdateEndpointReceived).To(Equal("https://api.the-server.com")) + Expect(Config.IsSSLDisabled()).To(BeFalse()) + }) + }) + + Describe("setting api endpoint failed", func() { + BeforeEach(func() { + Config.SetSSLDisabled(true) + endpointRepo.UpdateEndpointError = errors.New("API endpoint not found") + }) + + ItFails() + ItDoesntShowTheTarget() + + It("clears the entire config", func() { + Expect(Config.ApiEndpoint()).To(BeEmpty()) + Expect(Config.IsSSLDisabled()).To(BeFalse()) + Expect(Config.AccessToken()).To(BeEmpty()) + Expect(Config.RefreshToken()).To(BeEmpty()) + Expect(Config.OrganizationFields().Guid).To(BeEmpty()) + Expect(Config.SpaceFields().Guid).To(BeEmpty()) + }) + }) + }) + + Describe("when there is an invalid SSL cert", func() { + BeforeEach(func() { + endpointRepo.UpdateEndpointError = errors.NewInvalidSSLCert("https://bobs-burgers.com", "SELF SIGNED SADNESS") + ui.Inputs = []string{"bobs-burgers.com"} + }) + + It("fails and suggests the user skip SSL validation", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"SSL Cert", "https://bobs-burgers.com"}, + []string{"TIP", "login", "--skip-ssl-validation"}, + )) + }) + + ItDoesntShowTheTarget() + }) + }) + + Describe("when user is logging in and not setting the api endpoint", func() { + BeforeEach(func() { + Flags = []string{"-u", "the-user-name", "-p", "the-password"} + }) + + Describe("when the --skip-ssl-validation flag is provided", func() { + BeforeEach(func() { + Flags = append(Flags, "--skip-ssl-validation") + Config.SetSSLDisabled(false) + }) + + It("disables SSL validation", func() { + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + }) + + Describe("when the --skip-ssl-validation flag is not provided", func() { + BeforeEach(func() { + Config.SetSSLDisabled(true) + }) + + It("should not change config's SSLDisabled flag", func() { + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + }) + + Describe("and the login fails authenticaton", func() { + BeforeEach(func() { + authRepo.AuthError = true + + Config.SetSSLDisabled(true) + + Flags = []string{"-u", "user@example.com"} + ui.Inputs = []string{"password", "password2", "password3", "password4"} + }) + + ItFails() + ItShowsTheTarget() + + It("does not change the api endpoint or SSL setting in the config", func() { + Expect(Config.ApiEndpoint()).To(Equal("api.the-old-endpoint.com")) + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + + It("clears Access Token, Refresh Token, Org, and Space in the config", func() { + Expect(Config.AccessToken()).To(BeEmpty()) + Expect(Config.RefreshToken()).To(BeEmpty()) + Expect(Config.OrganizationFields().Guid).To(BeEmpty()) + Expect(Config.SpaceFields().Guid).To(BeEmpty()) + }) + }) + }) + + Describe("and the login fails to target an org", func() { + BeforeEach(func() { + Flags = []string{"-u", "user@example.com", "-p", "password", "-o", "nonexistentorg", "-s", "my-space"} + orgRepo.FindByNameReturns(models.Organization{}, errors.New("No org")) + Config.SetSSLDisabled(true) + }) + + ItFails() + ItShowsTheTarget() + + It("does not update the api endpoint or ssl setting in the config", func() { + Expect(Config.ApiEndpoint()).To(Equal("api.the-old-endpoint.com")) + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + + It("clears Org, and Space in the config", func() { + Expect(Config.OrganizationFields().Guid).To(BeEmpty()) + Expect(Config.SpaceFields().Guid).To(BeEmpty()) + }) + }) + + Describe("and the login fails to target a space", func() { + BeforeEach(func() { + Flags = []string{"-u", "user@example.com", "-p", "password", "-o", "my-new-org", "-s", "nonexistent"} + orgRepo.FindByNameReturns(org, nil) + + Config.SetSSLDisabled(true) + }) + + ItFails() + ItShowsTheTarget() + + It("does not update the api endpoint or ssl setting in the config", func() { + Expect(Config.ApiEndpoint()).To(Equal("api.the-old-endpoint.com")) + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + + It("updates the org in the config", func() { + Expect(Config.OrganizationFields().Guid).To(Equal("my-new-org-guid")) + }) + + It("clears the space in the config", func() { + Expect(Config.SpaceFields().Guid).To(BeEmpty()) + }) + }) + + Describe("and the login succeeds", func() { + BeforeEach(func() { + orgRepo.FindByNameReturns(models.Organization{ + OrganizationFields: models.OrganizationFields{ + Name: "new-org", + Guid: "new-org-guid", + }, + }, nil) + spaceRepo.Spaces[0].Name = "new-space" + spaceRepo.Spaces[0].Guid = "new-space-guid" + authRepo.AccessToken = "new_access_token" + authRepo.RefreshToken = "new_refresh_token" + + Flags = []string{"-u", "user@example.com", "-p", "password", "-o", "new-org", "-s", "new-space"} + + Config.SetApiEndpoint("api.the-old-endpoint.com") + Config.SetSSLDisabled(true) + }) + + ItSucceeds() + ItShowsTheTarget() + + It("does not update the api endpoint or SSL setting", func() { + Expect(Config.ApiEndpoint()).To(Equal("api.the-old-endpoint.com")) + Expect(Config.IsSSLDisabled()).To(BeTrue()) + }) + + It("updates Access Token, Refresh Token, Org, and Space in the config", func() { + Expect(Config.AccessToken()).To(Equal("new_access_token")) + Expect(Config.RefreshToken()).To(Equal("new_refresh_token")) + Expect(Config.OrganizationFields().Guid).To(Equal("new-org-guid")) + Expect(Config.SpaceFields().Guid).To(Equal("new-space-guid")) + }) + }) + }) +}) diff --git a/cf/commands/logout.go b/cf/commands/logout.go new file mode 100644 index 00000000000..de7c57e7b23 --- /dev/null +++ b/cf/commands/logout.go @@ -0,0 +1,40 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Logout struct { + ui terminal.UI + config core_config.ReadWriter +} + +func NewLogout(ui terminal.UI, config core_config.ReadWriter) (cmd Logout) { + cmd.ui = ui + cmd.config = config + return +} + +func (cmd Logout) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "logout", + ShortName: "lo", + Description: T("Log user out"), + Usage: T("CF_NAME logout"), + } +} + +func (cmd Logout) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + return +} + +func (cmd Logout) Run(c *cli.Context) { + cmd.ui.Say(T("Logging out...")) + cmd.config.ClearSession() + cmd.ui.Ok() +} diff --git a/cf/commands/logout_test.go b/cf/commands/logout_test.go new file mode 100644 index 00000000000..8f42ce0eb11 --- /dev/null +++ b/cf/commands/logout_test.go @@ -0,0 +1,43 @@ +package commands_test + +import ( + "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("logout command", func() { + var config core_config.Repository + BeforeEach(func() { + org := models.OrganizationFields{} + org.Name = "MyOrg" + + space := models.SpaceFields{} + space.Name = "MySpace" + + config = testconfig.NewRepository() + config.SetAccessToken("MyAccessToken") + config.SetOrganizationFields(org) + config.SetSpaceFields(space) + ui := new(testterm.FakeUI) + + l := commands.NewLogout(ui, config) + l.Run(nil) + }) + + It("clears access token from the config", func() { + Expect(config.AccessToken()).To(Equal("")) + }) + + It("clears organization fields from the config", func() { + Expect(config.OrganizationFields()).To(Equal(models.OrganizationFields{})) + }) + + It("clears space fields from the config", func() { + Expect(config.SpaceFields()).To(Equal(models.SpaceFields{})) + }) +}) diff --git a/cf/commands/oauth_token.go b/cf/commands/oauth_token.go new file mode 100644 index 00000000000..7a4f64f44ff --- /dev/null +++ b/cf/commands/oauth_token.go @@ -0,0 +1,53 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type OAuthToken struct { + ui terminal.UI + config core_config.ReadWriter + authRepo authentication.AuthenticationRepository +} + +func NewOAuthToken(ui terminal.UI, config core_config.ReadWriter, authRepo authentication.AuthenticationRepository) *OAuthToken { + return &OAuthToken{ + ui: ui, + config: config, + authRepo: authRepo, + } +} + +func (cmd *OAuthToken) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *OAuthToken) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "oauth-token", + Description: T("Retrieve and display the OAuth token for the current session"), + Usage: T("CF_NAME oauth-token"), + } +} + +func (cmd *OAuthToken) Run(c *cli.Context) { + cmd.ui.Say(T("Getting OAuth token...")) + + token, err := cmd.authRepo.RefreshAuthToken() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + cmd.ui.Say(token) +} diff --git a/cf/commands/oauth_token_test.go b/cf/commands/oauth_token_test.go new file mode 100644 index 00000000000..f86925ec0b6 --- /dev/null +++ b/cf/commands/oauth_token_test.go @@ -0,0 +1,72 @@ +package commands_test + +import ( + "errors" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("OauthToken", func() { + var ( + ui *testterm.FakeUI + authRepo *testapi.FakeAuthenticationRepository + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + authRepo = &testapi.FakeAuthenticationRepository{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func() bool { + cmd := NewOAuthToken(ui, configRepo, authRepo) + return testcmd.RunCommand(cmd, []string{}, requirementsFactory) + } + + Describe("requirments", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Describe("When logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails if oauth refresh fails", func() { + authRepo.RefreshTokenError = errors.New("Could not refresh") + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Could not refresh"}, + )) + }) + + It("returns to the user the oauth token after a refresh", func() { + authRepo.RefreshToken = "1234567890" + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting OAuth token..."}, + []string{"OK"}, + []string{"1234567890"}, + )) + }) + }) + +}) diff --git a/cf/commands/organization/create_org.go b/cf/commands/organization/create_org.go new file mode 100644 index 00000000000..5fff7d38812 --- /dev/null +++ b/cf/commands/organization/create_org.go @@ -0,0 +1,90 @@ +package organization + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateOrg struct { + ui terminal.UI + config core_config.Reader + orgRepo organizations.OrganizationRepository + quotaRepo quotas.QuotaRepository +} + +func NewCreateOrg(ui terminal.UI, config core_config.Reader, orgRepo organizations.OrganizationRepository, quotaRepo quotas.QuotaRepository) (cmd CreateOrg) { + cmd.ui = ui + cmd.config = config + cmd.orgRepo = orgRepo + cmd.quotaRepo = quotaRepo + return +} + +func (cmd CreateOrg) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-org", + ShortName: "co", + Description: T("Create an org"), + Usage: T("CF_NAME create-org ORG"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("q", T("Quota to assign to the newly created org (excluding this option results in assignment of default quota)")), + }, + } +} + +func (cmd CreateOrg) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd CreateOrg) Run(c *cli.Context) { + name := c.Args()[0] + cmd.ui.Say(T("Creating org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + org := models.Organization{OrganizationFields: models.OrganizationFields{Name: name}} + + quotaName := c.String("q") + if quotaName != "" { + quota, err := cmd.quotaRepo.FindByName(quotaName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + org.QuotaDefinition.Guid = quota.Guid + } + + err := cmd.orgRepo.Create(org) + if err != nil { + if apiErr, ok := err.(errors.HttpError); ok && apiErr.ErrorCode() == errors.ORG_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Org {{.OrgName}} already exists", + map[string]interface{}{"OrgName": name})) + return + } else { + cmd.ui.Failed(err.Error()) + } + } + + cmd.ui.Ok() + cmd.ui.Say(T("\nTIP: Use '{{.Command}}' to target new org", + map[string]interface{}{"Command": terminal.CommandColor(cf.Name() + " target -o " + name)})) +} diff --git a/cf/commands/organization/create_org_test.go b/cf/commands/organization/create_org_test.go new file mode 100644 index 00000000000..84de7fa33e4 --- /dev/null +++ b/cf/commands/organization/create_org_test.go @@ -0,0 +1,117 @@ +package organization_test + +import ( + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + test_quota "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/organization" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-org command", func() { + var ( + config core_config.ReadWriter + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + orgRepo *test_org.FakeOrganizationRepository + quotaRepo *test_quota.FakeQuotaRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + orgRepo = &test_org.FakeOrganizationRepository{} + quotaRepo = &test_quota.FakeQuotaRepository{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewCreateOrg(ui, config, orgRepo, quotaRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided exactly one arg", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("my-org")).To(BeFalse()) + }) + }) + + Context("when logged in and provided the name of an org to create", func() { + BeforeEach(func() { + orgRepo.CreateReturns(nil) + requirementsFactory.LoginSuccess = true + }) + + It("creates an org", func() { + runCommand("my-org") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating org", "my-org", "my-user"}, + []string{"OK"}, + )) + Expect(orgRepo.CreateArgsForCall(0).Name).To(Equal("my-org")) + }) + + It("fails and warns the user when the org already exists", func() { + err := errors.NewHttpError(400, errors.ORG_EXISTS, "org already exists") + orgRepo.CreateReturns(err) + runCommand("my-org") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating org", "my-org"}, + []string{"OK"}, + []string{"my-org", "already exists"}, + )) + }) + + Context("when allowing a non-defualt quota", func() { + var ( + quota models.QuotaFields + ) + + BeforeEach(func() { + quota = models.QuotaFields{ + Name: "non-default-quota", + Guid: "non-default-quota-guid", + } + }) + + It("creates an org with a non-default quota", func() { + quotaRepo.FindByNameReturns(quota, nil) + runCommand("-q", "non-default-quota", "my-org") + + Expect(quotaRepo.FindByNameArgsForCall(0)).To(Equal("non-default-quota")) + Expect(orgRepo.CreateArgsForCall(0).QuotaDefinition.Guid).To(Equal("non-default-quota-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating org", "my-org"}, + []string{"OK"}, + )) + }) + + It("fails and warns the user when the quota cannot be found", func() { + quotaRepo.FindByNameReturns(models.QuotaFields{}, errors.New("Could not find quota")) + runCommand("-q", "non-default-quota", "my-org") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating org", "my-org"}, + []string{"Could not find quota"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/organization/delete_org.go b/cf/commands/organization/delete_org.go new file mode 100644 index 00000000000..6dc51ad8c30 --- /dev/null +++ b/cf/commands/organization/delete_org.go @@ -0,0 +1,91 @@ +package organization + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteOrg struct { + ui terminal.UI + config core_config.ReadWriter + orgRepo organizations.OrganizationRepository + orgReq requirements.OrganizationRequirement +} + +func NewDeleteOrg(ui terminal.UI, config core_config.ReadWriter, orgRepo organizations.OrganizationRepository) (cmd *DeleteOrg) { + cmd = new(DeleteOrg) + cmd.ui = ui + cmd.config = config + cmd.orgRepo = orgRepo + return +} + +func (cmd *DeleteOrg) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-org", + Description: T("Delete an org"), + Usage: T("CF_NAME delete-org ORG [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteOrg) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func (cmd *DeleteOrg) Run(c *cli.Context) { + orgName := c.Args()[0] + + if !c.Bool("f") { + if !cmd.ui.ConfirmDeleteWithAssociations(T("org"), orgName) { + return + } + } + + cmd.ui.Say(T("Deleting org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(orgName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + org, apiErr := cmd.orgRepo.FindByName(orgName) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Org {{.OrgName}} does not exist.", + map[string]interface{}{"OrgName": orgName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + apiErr = cmd.orgRepo.Delete(org.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + if org.Guid == cmd.config.OrganizationFields().Guid { + cmd.config.SetOrganizationFields(models.OrganizationFields{}) + cmd.config.SetSpaceFields(models.SpaceFields{}) + } + + cmd.ui.Ok() + return +} diff --git a/cf/commands/organization/delete_org_test.go b/cf/commands/organization/delete_org_test.go new file mode 100644 index 00000000000..f34a2375b98 --- /dev/null +++ b/cf/commands/organization/delete_org_test.go @@ -0,0 +1,133 @@ +package organization_test + +import ( + "github.com/cloudfoundry/cli/cf/errors" + + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/organization" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-org command", func() { + var ( + config core_config.ReadWriter + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + orgRepo *test_org.FakeOrganizationRepository + org models.Organization + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{ + Inputs: []string{"y"}, + } + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + + org = models.Organization{} + org.Name = "org-to-delete" + org.Guid = "org-to-delete-guid" + orgRepo = &test_org.FakeOrganizationRepository{} + + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + orgRepo.FindByNameReturns(org, nil) + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteOrg(ui, config, orgRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails requirements when not logged in", func() { + Expect(runCommand("some-org-name")).To(BeFalse()) + }) + + It("fails with usage if no arguments are given", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when deleting the currently targeted org", func() { + It("untargets the deleted org", func() { + config.SetOrganizationFields(org.OrganizationFields) + runCommand("org-to-delete") + + Expect(config.OrganizationFields()).To(Equal(models.OrganizationFields{})) + Expect(config.SpaceFields()).To(Equal(models.SpaceFields{})) + }) + }) + + Context("when deleting an org that is not targeted", func() { + BeforeEach(func() { + otherOrgFields := models.OrganizationFields{} + otherOrgFields.Guid = "some-other-org-guid" + otherOrgFields.Name = "some-other-org" + config.SetOrganizationFields(otherOrgFields) + + spaceFields := models.SpaceFields{} + spaceFields.Name = "some-other-space" + config.SetSpaceFields(spaceFields) + }) + + It("deletes the org with the given name", func() { + runCommand("org-to-delete") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the org org-to-delete"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "org-to-delete"}, + []string{"OK"}, + )) + + Expect(orgRepo.DeleteArgsForCall(0)).To(Equal("org-to-delete-guid")) + }) + + It("does not untarget the org and space", func() { + runCommand("org-to-delete") + + Expect(config.OrganizationFields().Name).To(Equal("some-other-org")) + Expect(config.SpaceFields().Name).To(Equal("some-other-space")) + }) + }) + + It("does not prompt when the -f flag is given", func() { + ui.Inputs = []string{} + runCommand("-f", "org-to-delete") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "org-to-delete"}, + []string{"OK"}, + )) + + Expect(orgRepo.DeleteArgsForCall(0)).To(Equal("org-to-delete-guid")) + }) + + It("warns the user when the org does not exist", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.NewModelNotFoundError("Organization", "org org-to-delete does not exist")) + + runCommand("org-to-delete") + + Expect(orgRepo.DeleteCallCount()).To(Equal(0)) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "org-to-delete"}, + []string{"OK"}, + )) + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"org-to-delete", "does not exist."})) + }) + }) +}) diff --git a/cf/commands/organization/org.go b/cf/commands/organization/org.go new file mode 100644 index 00000000000..bf7cb1e6d9d --- /dev/null +++ b/cf/commands/organization/org.go @@ -0,0 +1,92 @@ +package organization + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ShowOrg struct { + ui terminal.UI + config core_config.Reader + orgReq requirements.OrganizationRequirement +} + +func NewShowOrg(ui terminal.UI, config core_config.Reader) (cmd *ShowOrg) { + cmd = new(ShowOrg) + cmd.ui = ui + cmd.config = config + return +} + +func (cmd *ShowOrg) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "org", + Description: T("Show org info"), + Usage: T("CF_NAME org ORG"), + } +} + +func (cmd *ShowOrg) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.orgReq, + } + + return +} + +func (cmd *ShowOrg) Run(c *cli.Context) { + org := cmd.orgReq.GetOrganization() + cmd.ui.Say(T("Getting info for org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(org.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{terminal.EntityNameColor(org.Name) + ":", "", ""}) + + domains := []string{} + for _, domain := range org.Domains { + domains = append(domains, domain.Name) + } + + spaces := []string{} + for _, space := range org.Spaces { + spaces = append(spaces, space.Name) + } + + spaceQuotas := []string{} + for _, spaceQuota := range org.SpaceQuotas { + spaceQuotas = append(spaceQuotas, spaceQuota.Name) + } + + quota := org.QuotaDefinition + orgQuota := fmt.Sprintf(T("{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + map[string]interface{}{ + "QuotaName": quota.Name, + "MemoryLimit": quota.MemoryLimit, + "RoutesLimit": quota.RoutesLimit, + "ServicesLimit": quota.ServicesLimit, + "NonBasicServicesAllowed": formatters.Allowed(quota.NonBasicServicesAllowed)})) + + table.Add("", T("domains:"), terminal.EntityNameColor(strings.Join(domains, ", "))) + table.Add("", T("quota:"), terminal.EntityNameColor(orgQuota)) + table.Add("", T("spaces:"), terminal.EntityNameColor(strings.Join(spaces, ", "))) + table.Add("", T("space quotas:"), terminal.EntityNameColor(strings.Join(spaceQuotas, ", "))) + + table.Print() +} diff --git a/cf/commands/organization/org_test.go b/cf/commands/organization/org_test.go new file mode 100644 index 00000000000..a090591b8ae --- /dev/null +++ b/cf/commands/organization/org_test.go @@ -0,0 +1,107 @@ +package organization_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/organization" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func callShowOrg(args []string, requirementsFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { + ui = new(testterm.FakeUI) + + token := core_config.TokenInfo{Username: "my-user"} + + spaceFields := models.SpaceFields{} + spaceFields.Name = "my-space" + + orgFields := models.OrganizationFields{} + orgFields.Name = "my-org" + + configRepo := testconfig.NewRepositoryWithAccessToken(token) + configRepo.SetSpaceFields(spaceFields) + configRepo.SetOrganizationFields(orgFields) + + cmd := NewShowOrg(ui, configRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} + +var _ = Describe("org command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewShowOrg(ui, configRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + Expect(runCommand("whoops")).To(BeFalse()) + }) + + It("fails with usage when not provided exactly one arg", func() { + requirementsFactory.LoginSuccess = true + runCommand("too", "much") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in, and provided the name of an org", func() { + BeforeEach(func() { + developmentSpaceFields := models.SpaceFields{} + developmentSpaceFields.Name = "development" + stagingSpaceFields := models.SpaceFields{} + stagingSpaceFields.Name = "staging" + domainFields := models.DomainFields{} + domainFields.Name = "cfapps.io" + cfAppDomainFields := models.DomainFields{} + cfAppDomainFields.Name = "cf-app.com" + + org := models.Organization{} + org.Name = "my-org" + org.Guid = "my-org-guid" + org.QuotaDefinition = models.NewQuotaFields("cantina-quota", 512, 2, 5, true) + org.Spaces = []models.SpaceFields{developmentSpaceFields, stagingSpaceFields} + org.Domains = []models.DomainFields{domainFields, cfAppDomainFields} + org.SpaceQuotas = []models.SpaceQuota{ + {Name: "space-quota-1"}, + {Name: "space-quota-2"}, + } + + requirementsFactory.LoginSuccess = true + requirementsFactory.Organization = org + }) + + It("shows the org with the given name", func() { + runCommand("my-org") + + Expect(requirementsFactory.OrganizationName).To(Equal("my-org")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting info for org", "my-org", "my-user"}, + []string{"OK"}, + []string{"my-org"}, + []string{"domains:", "cfapps.io", "cf-app.com"}, + []string{"quota: ", "cantina-quota", "512M", "2 routes", "5 services", "paid services allowed"}, + []string{"spaces:", "development", "staging"}, + []string{"space quotas:", "space-quota-1", "space-quota-2"}, + )) + }) + }) +}) diff --git a/cf/commands/organization/organization_suite_test.go b/cf/commands/organization/organization_suite_test.go new file mode 100644 index 00000000000..21b00ae0750 --- /dev/null +++ b/cf/commands/organization/organization_suite_test.go @@ -0,0 +1,19 @@ +package organization_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestOrganization(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Organization Suite") +} diff --git a/cf/commands/organization/orgs.go b/cf/commands/organization/orgs.go new file mode 100644 index 00000000000..a9624c4a241 --- /dev/null +++ b/cf/commands/organization/orgs.go @@ -0,0 +1,73 @@ +package organization + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListOrgs struct { + ui terminal.UI + config core_config.Reader + orgRepo organizations.OrganizationRepository +} + +func NewListOrgs(ui terminal.UI, config core_config.Reader, orgRepo organizations.OrganizationRepository) (cmd ListOrgs) { + cmd.ui = ui + cmd.config = config + cmd.orgRepo = orgRepo + return +} + +func (cmd ListOrgs) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "orgs", + ShortName: "o", + Description: T("List all orgs"), + Usage: "CF_NAME orgs", + } +} + +func (cmd ListOrgs) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd ListOrgs) Run(c *cli.Context) { + cmd.ui.Say(T("Getting orgs as {{.Username}}...\n", + map[string]interface{}{"Username": terminal.EntityNameColor(cmd.config.Username())})) + + noOrgs := true + table := cmd.ui.Table([]string{T("name")}) + + orgs, apiErr := cmd.orgRepo.ListOrgs() + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + for _, org := range orgs { + table.Add(org.Name) + noOrgs = false + } + + table.Print() + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching orgs.\n{{.ApiErr}}", + map[string]interface{}{"ApiErr": apiErr})) + return + } + + if noOrgs { + cmd.ui.Say(T("No orgs found")) + } +} diff --git a/cf/commands/organization/orgs_test.go b/cf/commands/organization/orgs_test.go new file mode 100644 index 00000000000..4dcd7ce4793 --- /dev/null +++ b/cf/commands/organization/orgs_test.go @@ -0,0 +1,87 @@ +package organization_test + +import ( + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/commands/organization" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("org command", func() { + var ( + ui *testterm.FakeUI + orgRepo *test_org.FakeOrganizationRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + runCommand := func(args ...string) bool { + cmd := organization.NewListOrgs(ui, configRepo, orgRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + orgRepo = &test_org.FakeOrganizationRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand()).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + }) + + Context("when there are orgs to be listed", func() { + BeforeEach(func() { + org1 := models.Organization{} + org1.Name = "Organization-1" + + org2 := models.Organization{} + org2.Name = "Organization-2" + + org3 := models.Organization{} + org3.Name = "Organization-3" + + orgRepo.ListOrgsReturns([]models.Organization{org1, org2, org3}, nil) + }) + + It("lists orgs", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting orgs as my-user"}, + []string{"Organization-1"}, + []string{"Organization-2"}, + []string{"Organization-3"}, + )) + }) + }) + + It("tells the user when no orgs were found", func() { + orgRepo.ListOrgsReturns([]models.Organization{}, nil) + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting orgs as my-user"}, + []string{"No orgs found"}, + )) + }) +}) diff --git a/cf/commands/organization/rename_org.go b/cf/commands/organization/rename_org.go new file mode 100644 index 00000000000..11a6ea9bbb7 --- /dev/null +++ b/cf/commands/organization/rename_org.go @@ -0,0 +1,70 @@ +package organization + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameOrg struct { + ui terminal.UI + config core_config.ReadWriter + orgRepo organizations.OrganizationRepository + orgReq requirements.OrganizationRequirement +} + +func NewRenameOrg(ui terminal.UI, config core_config.ReadWriter, orgRepo organizations.OrganizationRepository) (cmd *RenameOrg) { + cmd = new(RenameOrg) + cmd.ui = ui + cmd.config = config + cmd.orgRepo = orgRepo + return +} + +func (cmd *RenameOrg) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename-org", + Description: T("Rename an org"), + Usage: T("CF_NAME rename-org ORG NEW_ORG"), + } +} + +func (cmd *RenameOrg) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.orgReq, + } + return +} + +func (cmd *RenameOrg) Run(c *cli.Context) { + org := cmd.orgReq.GetOrganization() + newName := c.Args()[1] + + cmd.ui.Say(T("Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(org.Name), + "NewName": terminal.EntityNameColor(newName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.orgRepo.Rename(org.Guid, newName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + cmd.ui.Ok() + + if org.Guid == cmd.config.OrganizationFields().Guid { + org.Name = newName + cmd.config.SetOrganizationFields(org.OrganizationFields) + } +} diff --git a/cf/commands/organization/rename_org_test.go b/cf/commands/organization/rename_org_test.go new file mode 100644 index 00000000000..d2dec83d975 --- /dev/null +++ b/cf/commands/organization/rename_org_test.go @@ -0,0 +1,90 @@ +package organization_test + +import ( + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/commands/organization" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("rename-org command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + orgRepo *test_org.FakeOrganizationRepository + ui *testterm.FakeUI + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{} + orgRepo = &test_org.FakeOrganizationRepository{} + ui = new(testterm.FakeUI) + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + var callRenameOrg = func(args []string) bool { + cmd := organization.NewRenameOrg(ui, configRepo, orgRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails with usage when given less than two args", func() { + callRenameOrg([]string{}) + Expect(ui.FailedWithUsage).To(BeTrue()) + + callRenameOrg([]string{"foo"}) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when not logged in", func() { + Expect(callRenameOrg([]string{"my-org", "my-new-org"})).To(BeFalse()) + }) + + Context("when logged in and given an org to rename", func() { + BeforeEach(func() { + org := models.Organization{} + org.Name = "the-old-org-name" + org.Guid = "the-old-org-guid" + requirementsFactory.Organization = org + requirementsFactory.LoginSuccess = true + }) + + It("passes requirements", func() { + Expect(callRenameOrg([]string{"the-old-org-name", "the-new-org-name"})).To(BeTrue()) + }) + + It("renames an organization", func() { + targetedOrgName := configRepo.OrganizationFields().Name + callRenameOrg([]string{"the-old-org-name", "the-new-org-name"}) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming org", "the-old-org-name", "the-new-org-name", "my-user"}, + []string{"OK"}, + )) + + guid, name := orgRepo.RenameArgsForCall(0) + + Expect(requirementsFactory.OrganizationName).To(Equal("the-old-org-name")) + Expect(guid).To(Equal("the-old-org-guid")) + Expect(name).To(Equal("the-new-org-name")) + Expect(configRepo.OrganizationFields().Name).To(Equal(targetedOrgName)) + }) + + Describe("when the organization is currently targeted", func() { + It("updates the name of the org in the config", func() { + configRepo.SetOrganizationFields(models.OrganizationFields{ + Guid: "the-old-org-guid", + Name: "the-old-org-name", + }) + callRenameOrg([]string{"the-old-org-name", "the-new-org-name"}) + Expect(configRepo.OrganizationFields().Name).To(Equal("the-new-org-name")) + }) + }) + }) +}) diff --git a/cf/commands/organization/set_quota.go b/cf/commands/organization/set_quota.go new file mode 100644 index 00000000000..78d43c5c125 --- /dev/null +++ b/cf/commands/organization/set_quota.go @@ -0,0 +1,73 @@ +package organization + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository + orgReq requirements.OrganizationRequirement +} + +func NewSetQuota(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) (cmd *SetQuota) { + cmd = new(SetQuota) + cmd.ui = ui + cmd.config = config + cmd.quotaRepo = quotaRepo + return +} + +func (cmd *SetQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-quota", + Description: T("Assign a quota to an org"), + Usage: T("CF_NAME set-quota ORG QUOTA\n\n") + T("TIP:\n") + T(" View allowable quotas with 'CF_NAME quotas'"), + } +} + +func (cmd *SetQuota) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.orgReq, + } + return +} + +func (cmd *SetQuota) Run(c *cli.Context) { + org := cmd.orgReq.GetOrganization() + quotaName := c.Args()[1] + quota, apiErr := cmd.quotaRepo.FindByName(quotaName) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Say(T("Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(quota.Name), + "OrgName": terminal.EntityNameColor(org.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr = cmd.quotaRepo.AssignQuotaToOrg(org.Guid, quota.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/organization/set_quota_test.go b/cf/commands/organization/set_quota_test.go new file mode 100644 index 00000000000..8ae8a249c23 --- /dev/null +++ b/cf/commands/organization/set_quota_test.go @@ -0,0 +1,82 @@ +package organization_test + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/organization" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("set-quota command", func() { + var ( + cmd *SetQuota + ui *testterm.FakeUI + quotaRepo *fakes.FakeQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = new(testterm.FakeUI) + quotaRepo = &fakes.FakeQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + cmd = NewSetQuota(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + }) + + It("fails with usage when provided too many or two few args", func() { + runCommand("org") + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("org", "quota", "extra-stuff") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when not logged in", func() { + Expect(runCommand("my-org", "my-quota")).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("passes requirements when provided two args", func() { + passed := runCommand("my-org", "my-quota") + Expect(passed).To(BeTrue()) + Expect(requirementsFactory.OrganizationName).To(Equal("my-org")) + }) + + It("assigns a quota to an org", func() { + org := models.Organization{} + org.Name = "my-org" + org.Guid = "my-org-guid" + + quota := models.QuotaFields{Name: "my-quota", Guid: "my-quota-guid"} + + quotaRepo.FindByNameReturns(quota, nil) + requirementsFactory.Organization = org + + runCommand("my-org", "my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Setting quota", "my-quota", "my-org", "my-user"}, + []string{"OK"}, + )) + + Expect(quotaRepo.FindByNameArgsForCall(0)).To(Equal("my-quota")) + orgGuid, quotaGuid := quotaRepo.AssignQuotaToOrgArgsForCall(0) + Expect(orgGuid).To(Equal("my-org-guid")) + Expect(quotaGuid).To(Equal("my-quota-guid")) + }) + }) +}) diff --git a/cf/commands/passwd.go b/cf/commands/passwd.go new file mode 100644 index 00000000000..8d3d1a48075 --- /dev/null +++ b/cf/commands/passwd.go @@ -0,0 +1,69 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf/api/password" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Password struct { + ui terminal.UI + pwdRepo password.PasswordRepository + config core_config.ReadWriter +} + +func NewPassword(ui terminal.UI, pwdRepo password.PasswordRepository, config core_config.ReadWriter) (cmd Password) { + cmd.ui = ui + cmd.pwdRepo = pwdRepo + cmd.config = config + return +} + +func (cmd Password) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "passwd", + ShortName: "pw", + Description: T("Change user password"), + Usage: T("CF_NAME passwd"), + } +} + +func (cmd Password) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func (cmd Password) Run(c *cli.Context) { + oldPassword := cmd.ui.AskForPassword(T("Current Password")) + newPassword := cmd.ui.AskForPassword(T("New Password")) + verifiedPassword := cmd.ui.AskForPassword(T("Verify Password")) + + if verifiedPassword != newPassword { + cmd.ui.Failed(T("Password verification does not match")) + return + } + + cmd.ui.Say(T("Changing password...")) + apiErr := cmd.pwdRepo.UpdatePassword(oldPassword, newPassword) + + switch typedErr := apiErr.(type) { + case nil: + case errors.HttpError: + if typedErr.StatusCode() == 401 { + cmd.ui.Failed(T("Current password did not match")) + } else { + cmd.ui.Failed(apiErr.Error()) + } + default: + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() + cmd.config.ClearSession() + cmd.ui.Say(T("Please log in again")) +} diff --git a/cf/commands/passwd_test.go b/cf/commands/passwd_test.go new file mode 100644 index 00000000000..f9c98161ce2 --- /dev/null +++ b/cf/commands/passwd_test.go @@ -0,0 +1,124 @@ +package commands_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("password command", func() { + var deps passwordDeps + + BeforeEach(func() { + deps = getPasswordDeps() + }) + + It("does not pass requirements if you are not logged in", func() { + deps.ReqFactory.LoginSuccess = false + _, passed := callPassword([]string{}, deps) + Expect(passed).To(BeFalse()) + }) + + Context("when logged in successfully", func() { + BeforeEach(func() { + deps.ReqFactory.LoginSuccess = true + }) + + It("passes requirements", func() { + _, passed := callPassword([]string{"", "", ""}, deps) + Expect(passed).To(BeTrue()) + }) + + It("changes your password when given a new password", func() { + deps.PwdRepo.UpdateUnauthorized = false + ui, _ := callPassword([]string{"old-password", "new-password", "new-password"}, deps) + + Expect(ui.PasswordPrompts).To(ContainSubstrings( + []string{"Current Password"}, + []string{"New Password"}, + []string{"Verify Password"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Changing password..."}, + []string{"OK"}, + []string{"Please log in again"}, + )) + + Expect(deps.PwdRepo.UpdateNewPassword).To(Equal("new-password")) + Expect(deps.PwdRepo.UpdateOldPassword).To(Equal("old-password")) + + Expect(deps.Config.AccessToken()).To(Equal("")) + Expect(deps.Config.OrganizationFields()).To(Equal(models.OrganizationFields{})) + Expect(deps.Config.SpaceFields()).To(Equal(models.SpaceFields{})) + }) + + It("fails when the password verification does not match", func() { + ui, _ := callPassword([]string{"old-password", "new-password", "new-password-with-error"}, deps) + + Expect(ui.PasswordPrompts).To(ContainSubstrings( + []string{"Current Password"}, + []string{"New Password"}, + []string{"Verify Password"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Password verification does not match"}, + )) + + Expect(deps.PwdRepo.UpdateNewPassword).To(Equal("")) + }) + + It("fails when the current password does not match", func() { + deps.PwdRepo.UpdateUnauthorized = true + ui, _ := callPassword([]string{"old-password", "new-password", "new-password"}, deps) + + Expect(ui.PasswordPrompts).To(ContainSubstrings( + []string{"Current Password"}, + []string{"New Password"}, + []string{"Verify Password"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Changing password..."}, + []string{"FAILED"}, + []string{"Current password did not match"}, + )) + + Expect(deps.PwdRepo.UpdateNewPassword).To(Equal("new-password")) + Expect(deps.PwdRepo.UpdateOldPassword).To(Equal("old-password")) + }) + }) +}) + +type passwordDeps struct { + ReqFactory *testreq.FakeReqFactory + PwdRepo *testapi.FakePasswordRepo + Config core_config.ReadWriter +} + +func getPasswordDeps() passwordDeps { + return passwordDeps{ + ReqFactory: &testreq.FakeReqFactory{LoginSuccess: true}, + PwdRepo: &testapi.FakePasswordRepo{UpdateUnauthorized: true}, + Config: testconfig.NewRepository(), + } +} + +func callPassword(inputs []string, deps passwordDeps) (*testterm.FakeUI, bool) { + ui := &testterm.FakeUI{Inputs: inputs} + cmd := NewPassword(ui, deps.PwdRepo, deps.Config) + passed := testcmd.RunCommand(cmd, []string{}, deps.ReqFactory) + + return ui, passed +} diff --git a/cf/commands/plugin/install_plugin.go b/cf/commands/plugin/install_plugin.go new file mode 100644 index 00000000000..0de1945c8fd --- /dev/null +++ b/cf/commands/plugin/install_plugin.go @@ -0,0 +1,178 @@ +package plugin + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/fileutils" + "github.com/cloudfoundry/cli/plugin" + "github.com/cloudfoundry/cli/plugin/rpc" + "github.com/codegangsta/cli" +) + +type PluginInstall struct { + ui terminal.UI + config plugin_config.PluginConfiguration + coreCmds map[string]command.Command +} + +func NewPluginInstall(ui terminal.UI, config plugin_config.PluginConfiguration, coreCmds map[string]command.Command) *PluginInstall { + return &PluginInstall{ + ui: ui, + config: config, + coreCmds: coreCmds, + } +} + +func (cmd *PluginInstall) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "install-plugin", + Description: T("Install the plugin defined in command argument"), + Usage: T("CF_NAME install-plugin PATH/TO/PLUGIN"), + } +} + +func (cmd *PluginInstall) GetRequirements(_ requirements.Factory, c *cli.Context) (req []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + return +} + +func (cmd *PluginInstall) Run(c *cli.Context) { + pluginSourceFilepath := c.Args()[0] + + if filepath.Dir(pluginSourceFilepath) == "." { + pluginSourceFilepath = "./" + filepath.Clean(pluginSourceFilepath) + } + + cmd.ui.Say(fmt.Sprintf(T("Installing plugin {{.PluginPath}}...", map[string]interface{}{"PluginPath": pluginSourceFilepath}))) + + cmd.ensureCandidatePluginBinaryExistsAtGivenPath(pluginSourceFilepath) + + _, pluginExecutableName := filepath.Split(pluginSourceFilepath) + + pluginDestinationFilepath := filepath.Join(cmd.config.GetPluginPath(), pluginExecutableName) + + cmd.ensurePluginBinaryWithSameFileNameDoesNotAlreadyExist(pluginDestinationFilepath, pluginExecutableName) + + pluginMetadata := cmd.runBinaryAndObtainPluginMetadata(pluginSourceFilepath) + + cmd.ensurePluginIsSafeForInstallation(pluginMetadata, pluginDestinationFilepath, pluginSourceFilepath) + + cmd.installPlugin(pluginMetadata, pluginDestinationFilepath, pluginSourceFilepath) + + cmd.ui.Ok() + cmd.ui.Say(fmt.Sprintf(T("Plugin {{.PluginName}} successfully installed.", map[string]interface{}{"PluginName": pluginMetadata.Name}))) +} + +func (cmd *PluginInstall) ensurePluginBinaryWithSameFileNameDoesNotAlreadyExist(pluginDestinationFilepath, pluginExecutableName string) { + _, err := os.Stat(pluginDestinationFilepath) + if err == nil || os.IsExist(err) { + cmd.ui.Failed(fmt.Sprintf(T("The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + map[string]interface{}{ + "PluginExecutableName": pluginExecutableName, + }))) + } else if !os.IsNotExist(err) { + cmd.ui.Failed(fmt.Sprintf(T("Unexpected error has occurred:\n{{.Error}}", map[string]interface{}{"Error": err.Error()}))) + } +} + +func (cmd *PluginInstall) ensurePluginIsSafeForInstallation(pluginMetadata *plugin.PluginMetadata, pluginDestinationFilepath string, pluginSourceFilepath string) { + plugins := cmd.config.Plugins() + if pluginMetadata.Name == "" { + cmd.ui.Failed(fmt.Sprintf(T("Unable to obtain plugin name for executable {{.Executable}}", map[string]interface{}{"Executable": pluginSourceFilepath}))) + } + + if _, ok := plugins[pluginMetadata.Name]; ok { + cmd.ui.Failed(fmt.Sprintf(T("Plugin name {{.PluginName}} is already taken", map[string]interface{}{"PluginName": pluginMetadata.Name}))) + } + + if pluginMetadata.Commands == nil { + cmd.ui.Failed(fmt.Sprintf(T("Error getting command list from plugin {{.FilePath}}", map[string]interface{}{"FilePath": pluginSourceFilepath}))) + } + + shortNames := cmd.getShortNames() + + for _, pluginCmd := range pluginMetadata.Commands { + if _, exists := cmd.coreCmds[pluginCmd.Name]; exists || shortNames[pluginCmd.Name] || pluginCmd.Name == "help" { + cmd.ui.Failed(fmt.Sprintf(T("Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + map[string]interface{}{"Command": pluginCmd.Name}))) + } + + for installedPluginName, installedPlugin := range plugins { + for _, installedPluginCmd := range installedPlugin.Commands { + if installedPluginCmd.Name == pluginCmd.Name { + cmd.ui.Failed(fmt.Sprintf(T("`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + map[string]interface{}{"Command": pluginCmd.Name, "PluginName": installedPluginName}))) + } + } + } + } +} + +func (cmd *PluginInstall) installPlugin(pluginMetadata *plugin.PluginMetadata, pluginDestinationFilepath, pluginSourceFilepath string) { + err := fileutils.CopyFile(pluginDestinationFilepath, pluginSourceFilepath) + if err != nil { + cmd.ui.Failed(fmt.Sprintf(T("Could not copy plugin binary: \n{{.Error}}", map[string]interface{}{"Error": err.Error()}))) + } + + configMetadata := plugin_config.PluginMetadata{ + Location: pluginDestinationFilepath, + Commands: pluginMetadata.Commands, + } + + cmd.config.SetPlugin(pluginMetadata.Name, configMetadata) +} + +func (cmd *PluginInstall) runBinaryAndObtainPluginMetadata(pluginSourceFilepath string) *plugin.PluginMetadata { + rpcService, err := rpc.NewRpcService(nil, nil, nil) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + err = rpcService.Start() + if err != nil { + cmd.ui.Failed(err.Error()) + } + defer rpcService.Stop() + + cmd.runPluginBinary(pluginSourceFilepath, rpcService.Port()) + return rpcService.RpcCmd.PluginMetadata +} + +func (cmd *PluginInstall) ensureCandidatePluginBinaryExistsAtGivenPath(pluginSourceFilepath string) { + _, err := os.Stat(pluginSourceFilepath) + if err != nil && os.IsNotExist(err) { + cmd.ui.Failed(fmt.Sprintf(T("Binary file '{{.BinaryFile}}' not found", map[string]interface{}{"BinaryFile": pluginSourceFilepath}))) + } +} + +func (cmd *PluginInstall) getShortNames() map[string]bool { + shortNames := make(map[string]bool) + for _, singleCmd := range cmd.coreCmds { + metaData := singleCmd.Metadata() + if metaData.ShortName != "" { + shortNames[metaData.ShortName] = true + } + } + return shortNames +} + +func (cmd *PluginInstall) runPluginBinary(location string, servicePort string) { + pluginInvocation := exec.Command(location, servicePort, "SendMetadata") + + err := pluginInvocation.Run() + if err != nil { + cmd.ui.Failed(err.Error()) + } +} diff --git a/cf/commands/plugin/install_plugin_test.go b/cf/commands/plugin/install_plugin_test.go new file mode 100644 index 00000000000..8cb26257b0b --- /dev/null +++ b/cf/commands/plugin/install_plugin_test.go @@ -0,0 +1,260 @@ +package plugin_test + +import ( + "io/ioutil" + "net/rpc" + "os" + "path/filepath" + "runtime" + + "github.com/cloudfoundry/cli/cf/command" + testCommand "github.com/cloudfoundry/cli/cf/command/fakes" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + testconfig "github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes" + "github.com/cloudfoundry/cli/plugin" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/plugin" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Install", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + config *testconfig.FakePluginConfiguration + + coreCmds map[string]command.Command + pluginFile *os.File + homeDir string + pluginDir string + curDir string + + test_1 string + test_2 string + test_curDir string + test_with_help string + test_with_push string + test_with_push_short_name string + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + config = &testconfig.FakePluginConfiguration{} + coreCmds = make(map[string]command.Command) + + dir, err := os.Getwd() + if err != nil { + panic(err) + } + test_1 = filepath.Join(dir, "..", "..", "..", "fixtures", "plugins", "test_1.exe") + test_2 = filepath.Join(dir, "..", "..", "..", "fixtures", "plugins", "test_2.exe") + test_curDir = filepath.Join("test_1.exe") + test_with_help = filepath.Join(dir, "..", "..", "..", "fixtures", "plugins", "test_with_help.exe") + test_with_push = filepath.Join(dir, "..", "..", "..", "fixtures", "plugins", "test_with_push.exe") + test_with_push_short_name = filepath.Join(dir, "..", "..", "..", "fixtures", "plugins", "test_with_push_short_name.exe") + + rpc.DefaultServer = rpc.NewServer() + + homeDir, err = ioutil.TempDir(os.TempDir(), "plugins") + Expect(err).ToNot(HaveOccurred()) + + pluginDir = filepath.Join(homeDir, ".cf", "plugins") + config.GetPluginPathReturns(pluginDir) + + curDir, err = os.Getwd() + Expect(err).ToNot(HaveOccurred()) + pluginFile, err = ioutil.TempFile("./", "test_plugin") + Expect(err).ToNot(HaveOccurred()) + + if runtime.GOOS != "windows" { + err = os.Chmod(test_1, 0700) + Expect(err).ToNot(HaveOccurred()) + } + }) + + AfterEach(func() { + os.Remove(filepath.Join(curDir, pluginFile.Name())) + os.Remove(homeDir) + }) + + runCommand := func(args ...string) bool { + cmd := NewPluginInstall(ui, config, coreCmds) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided a path to the plugin executable", func() { + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Describe("failures", func() { + Context("when the plugin contains a 'help' command", func() { + It("fails", func() { + runCommand(test_with_help) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Command `help` in the plugin being installed is a native CF command. Rename the `help` command in the plugin being installed in order to enable its installation and use."}, + []string{"FAILED"}, + )) + }) + }) + + Context("when the plugin contains a core command", func() { + It("fails if is shares a command name", func() { + coreCmds["push"] = &testCommand.FakeCommand{} + runCommand(test_with_push) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Command `push` in the plugin being installed is a native CF command. Rename the `push` command in the plugin being installed in order to enable its installation and use."}, + []string{"FAILED"}, + )) + }) + + It("fails if it shares a command short name", func() { + push := &testCommand.FakeCommand{} + push.MetadataReturns(command_metadata.CommandMetadata{ + ShortName: "p", + }) + + coreCmds["push"] = push + runCommand(test_with_push_short_name) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Command `p` in the plugin being installed is a native CF command. Rename the `p` command in the plugin being installed in order to enable its installation and use."}, + []string{"FAILED"}, + )) + }) + }) + + Context("when the plugin contains a command that another installed plugin contains", func() { + BeforeEach(func() { + pluginsMap := make(map[string]plugin_config.PluginMetadata) + pluginsMap["Test1Collision"] = plugin_config.PluginMetadata{ + Location: "location/to/config.exe", + Commands: []plugin.Command{ + { + Name: "test_1_cmd1", + HelpText: "Hi!", + }, + }, + } + config.PluginsReturns(pluginsMap) + }) + + It("fails", func() { + runCommand(test_1) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"`test_1_cmd1` is a command in plugin 'Test1Collision'. You could try uninstalling plugin 'Test1Collision' and then install this plugin in order to invoke the `test_1_cmd1` command. However, you should first fully understand the impact of uninstalling the existing 'Test1Collision' plugin."}, + []string{"FAILED"}, + )) + }) + }) + + It("plugin binary argument is a bad file path", func() { + runCommand("path/to/not/a/thing.exe") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Binary file 'path/to/not/a/thing.exe' not found"}, + []string{"FAILED"}, + )) + }) + + It("if plugin name is already taken", func() { + config.PluginsReturns(map[string]plugin_config.PluginMetadata{"Test1": plugin_config.PluginMetadata{}}) + runCommand(test_1) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Plugin name", "Test1", "is already taken"}, + []string{"FAILED"}, + )) + }) + + Context("io", func() { + BeforeEach(func() { + err := os.MkdirAll(pluginDir, 0700) + Expect(err).NotTo(HaveOccurred()) + }) + + It("if a file with the plugin name already exists under ~/.cf/plugin/", func() { + config.PluginsReturns(map[string]plugin_config.PluginMetadata{"useless": plugin_config.PluginMetadata{}}) + config.GetPluginPathReturns(curDir) + + runCommand(filepath.Join(curDir, pluginFile.Name())) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Installing plugin"}, + []string{"The file", pluginFile.Name(), "already exists"}, + []string{"FAILED"}, + )) + }) + }) + }) + + Describe("success", func() { + BeforeEach(func() { + err := os.MkdirAll(pluginDir, 0700) + Expect(err).ToNot(HaveOccurred()) + config.GetPluginPathReturns(pluginDir) + }) + + It("finds plugin in the current directory without having to specify `./`", func() { + curDir, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + + err = os.Chdir("../../../fixtures/plugins") + Expect(err).ToNot(HaveOccurred()) + + runCommand(test_curDir) + _, err = os.Stat(filepath.Join(pluginDir, "test_1.exe")) + Expect(err).ToNot(HaveOccurred()) + + err = os.Chdir(curDir) + Expect(err).ToNot(HaveOccurred()) + }) + + It("copies the plugin into directory /.cf/plugins/PLUGIN_FILE_NAME", func() { + runCommand(test_1) + + _, err := os.Stat(test_1) + Expect(err).ToNot(HaveOccurred()) + _, err = os.Stat(filepath.Join(pluginDir, "test_1.exe")) + Expect(err).ToNot(HaveOccurred()) + }) + + if runtime.GOOS != "windows" { + It("Chmods the plugin so it is executable", func() { + runCommand(test_1) + + fileInfo, err := os.Stat(filepath.Join(pluginDir, "test_1.exe")) + Expect(err).ToNot(HaveOccurred()) + Expect(int(fileInfo.Mode())).To(Equal(0700)) + }) + } + + It("populate the configuration with plugin metadata", func() { + runCommand(test_1) + + pluginName, pluginMetadata := config.SetPluginArgsForCall(0) + + Expect(pluginName).To(Equal("Test1")) + Expect(pluginMetadata.Location).To(Equal(filepath.Join(pluginDir, "test_1.exe"))) + Expect(pluginMetadata.Commands[0].Name).To(Equal("test_1_cmd1")) + Expect(pluginMetadata.Commands[0].HelpText).To(Equal("help text for test_1_cmd1")) + Expect(pluginMetadata.Commands[1].Name).To(Equal("test_1_cmd2")) + Expect(pluginMetadata.Commands[1].HelpText).To(Equal("help text for test_1_cmd2")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Installing plugin", test_1}, + []string{"OK"}, + []string{"Plugin", "Test1", "successfully installed"}, + )) + }) + }) +}) diff --git a/cf/commands/plugin/plugin_suite_test.go b/cf/commands/plugin/plugin_suite_test.go new file mode 100644 index 00000000000..aaa6f36cdd9 --- /dev/null +++ b/cf/commands/plugin/plugin_suite_test.go @@ -0,0 +1,30 @@ +package plugin_test + +import ( + "path/filepath" + + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlugin(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "test_with_help") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "test_with_push") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "test_with_push_short_name") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "test_1") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "test_2") + plugin_builder.BuildTestBinary(filepath.Join("..", "..", "..", "fixtures", "plugins"), "empty_plugin") + + RunSpecs(t, "Plugin Suite") +} diff --git a/cf/commands/plugin/plugins.go b/cf/commands/plugin/plugins.go new file mode 100644 index 00000000000..b2ba3b21aea --- /dev/null +++ b/cf/commands/plugin/plugins.go @@ -0,0 +1,56 @@ +package plugin + +import ( + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Plugins struct { + ui terminal.UI + config plugin_config.PluginConfiguration +} + +func NewPlugins(ui terminal.UI, config plugin_config.PluginConfiguration) *Plugins { + return &Plugins{ + ui: ui, + config: config, + } +} + +func (cmd *Plugins) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "plugins", + Description: T("list all available plugin commands"), + Usage: T("CF_NAME plugins"), + } +} + +func (cmd *Plugins) GetRequirements(_ requirements.Factory, c *cli.Context) (req []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + return +} + +func (cmd *Plugins) Run(c *cli.Context) { + cmd.ui.Say(T("Listing Installed Plugins...")) + + plugins := cmd.config.Plugins() + + table := terminal.NewTable(cmd.ui, []string{T("Plugin Name"), T("Command Name"), T("Command Help")}) + + for pluginName, metadata := range plugins { + for _, command := range metadata.Commands { + table.Add(pluginName, command.Name, command.HelpText) + } + } + + cmd.ui.Ok() + cmd.ui.Say("") + + table.Print() +} diff --git a/cf/commands/plugin/plugins_test.go b/cf/commands/plugin/plugins_test.go new file mode 100644 index 00000000000..c200f2b0bbc --- /dev/null +++ b/cf/commands/plugin/plugins_test.go @@ -0,0 +1,89 @@ +package plugin_test + +import ( + "net/rpc" + + . "github.com/cloudfoundry/cli/cf/commands/plugin" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + testconfig "github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes" + "github.com/cloudfoundry/cli/plugin" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Plugins", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + config *testconfig.FakePluginConfiguration + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + config = &testconfig.FakePluginConfiguration{} + + rpc.DefaultServer = rpc.NewServer() + }) + + runCommand := func(args ...string) bool { + cmd := NewPlugins(ui, config) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("returns a list of available methods of a plugin", func() { + config.PluginsReturns(map[string]plugin_config.PluginMetadata{ + "Test1": plugin_config.PluginMetadata{ + Location: "path/to/plugin", + Commands: []plugin.Command{ + {Name: "test_1_cmd1", HelpText: "help text for test_1_cmd1"}, + {Name: "test_1_cmd2", HelpText: "help text for test_1_cmd2"}, + }, + }, + }) + + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Listing Installed Plugins..."}, + []string{"OK"}, + []string{"Plugin Name", "Command Name", "Command Help"}, + []string{"Test1", "test_1_cmd1", "help text for test_1_cmd1"}, + []string{"Test1", "test_1_cmd2", "help text for test_1_cmd2"}, + )) + }) + + It("does not list the plugin when it provides no available commands", func() { + config.PluginsReturns(map[string]plugin_config.PluginMetadata{ + "EmptyPlugin": plugin_config.PluginMetadata{Location: "../../../fixtures/plugins/empty_plugin.exe"}, + }) + + runCommand() + Expect(ui.Outputs).NotTo(ContainSubstrings( + []string{"EmptyPlugin"}, + )) + }) + + It("list multiple plugins and their associated commands", func() { + config.PluginsReturns(map[string]plugin_config.PluginMetadata{ + "Test1": plugin_config.PluginMetadata{Location: "path/to/plugin1", Commands: []plugin.Command{{Name: "test_1_cmd1", HelpText: "help text for test_1_cmd1"}}}, + "Test2": plugin_config.PluginMetadata{Location: "path/to/plugin2", Commands: []plugin.Command{{Name: "test_2_cmd1", HelpText: "help text for test_2_cmd1"}}}, + }) + + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Test1", "test_1_cmd1", "help text for test_1_cmd1"}, + []string{"Test2", "test_2_cmd1", "help text for test_2_cmd1"}, + )) + }) +}) diff --git a/cf/commands/plugin/uninstall_plugin.go b/cf/commands/plugin/uninstall_plugin.go new file mode 100644 index 00000000000..bf5c1ee9a1b --- /dev/null +++ b/cf/commands/plugin/uninstall_plugin.go @@ -0,0 +1,63 @@ +package plugin + +import ( + "fmt" + "os" + + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type PluginUninstall struct { + ui terminal.UI + config plugin_config.PluginConfiguration +} + +func NewPluginUninstall(ui terminal.UI, config plugin_config.PluginConfiguration) *PluginUninstall { + return &PluginUninstall{ + ui: ui, + config: config, + } +} + +func (cmd *PluginUninstall) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "uninstall-plugin", + Description: T("Uninstall the plugin defined in command argument"), + Usage: T("CF_NAME uninstall-plugin PLUGIN-NAME"), + } +} + +func (cmd *PluginUninstall) GetRequirements(_ requirements.Factory, c *cli.Context) (req []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + return +} + +func (cmd *PluginUninstall) Run(c *cli.Context) { + + pluginName := c.Args()[0] + pluginNameMap := map[string]interface{}{"PluginName": pluginName} + + cmd.ui.Say(fmt.Sprintf(T("Uninstalling plugin {{.PluginName}}...", pluginNameMap))) + + plugins := cmd.config.Plugins() + + if _, ok := plugins[pluginName]; !ok { + cmd.ui.Failed(fmt.Sprintf(T("Plugin name {{.PluginName}} does not exist", pluginNameMap))) + } + + pluginMetadata := plugins[pluginName] + os.Remove(pluginMetadata.Location) + + cmd.config.RemovePlugin(pluginName) + + cmd.ui.Ok() + cmd.ui.Say(fmt.Sprintf(T("Plugin {{.PluginName}} successfully uninstalled.", pluginNameMap))) +} diff --git a/cf/commands/plugin/uninstall_plugin_test.go b/cf/commands/plugin/uninstall_plugin_test.go new file mode 100644 index 00000000000..65e56319c85 --- /dev/null +++ b/cf/commands/plugin/uninstall_plugin_test.go @@ -0,0 +1,118 @@ +package plugin_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/plugin" + "github.com/cloudfoundry/cli/fileutils" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Uninstall", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + fakePluginRepoDir string + pluginDir string + pluginConfig *plugin_config.PluginConfig + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + + var err error + fakePluginRepoDir, err = ioutil.TempDir(os.TempDir(), "plugins") + Expect(err).ToNot(HaveOccurred()) + + fixtureDir := filepath.Join("..", "..", "..", "fixtures", "plugins") + + pluginDir = filepath.Join(fakePluginRepoDir, ".cf", "plugins") + err = os.MkdirAll(pluginDir, 0700) + Expect(err).NotTo(HaveOccurred()) + + fileutils.CopyFile(filepath.Join(pluginDir, "test_1.exe"), filepath.Join(fixtureDir, "test_1.exe")) + fileutils.CopyFile(filepath.Join(pluginDir, "test_2.exe"), filepath.Join(fixtureDir, "test_2.exe")) + + config_helpers.PluginRepoDir = func() string { + return fakePluginRepoDir + } + + pluginConfig = plugin_config.NewPluginConfig(func(err error) { Expect(err).ToNot(HaveOccurred()) }) + pluginConfig.SetPlugin("test_1.exe", plugin_config.PluginMetadata{Location: filepath.Join(pluginDir, "test_1.exe")}) + pluginConfig.SetPlugin("test_2.exe", plugin_config.PluginMetadata{Location: filepath.Join(pluginDir, "test_2.exe")}) + }) + + AfterEach(func() { + os.Remove(fakePluginRepoDir) + }) + + runCommand := func(args ...string) bool { + cmd := NewPluginUninstall(ui, pluginConfig) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided a path to the plugin executable", func() { + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Describe("failures", func() { + It("if plugin name does not exist", func() { + runCommand("garbage") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Uninstalling plugin garbage..."}, + []string{"FAILED"}, + []string{"Plugin name", "garbage", "does not exist"}, + )) + }) + }) + + Describe("success", func() { + + It("removes the binary from the /.cf/plugins dir", func() { + _, err := os.Stat(filepath.Join(pluginDir, "test_1.exe")) + Expect(err).ToNot(HaveOccurred()) + + runCommand("test_1.exe") + + _, err = os.Stat(filepath.Join(pluginDir, "test_1.exe")) + Expect(err).To(HaveOccurred()) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + + It("removes the entry from the config.json", func() { + plugins := pluginConfig.Plugins() + Expect(plugins).To(HaveKey("test_1.exe")) + + runCommand("test_1.exe") + + plugins = pluginConfig.Plugins() + Expect(plugins).NotTo(HaveKey("test_1.exe")) + }) + + It("prints success text", func() { + runCommand("test_1.exe") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Uninstalling plugin test_1.exe..."}, + []string{"OK"}, + []string{"Plugin", "test_1.exe", "successfully uninstalled."}, + )) + }) + + }) + +}) diff --git a/cf/commands/quota/create_quota.go b/cf/commands/quota/create_quota.go new file mode 100644 index 00000000000..4125b4f20ef --- /dev/null +++ b/cf/commands/quota/create_quota.go @@ -0,0 +1,115 @@ +package quota + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository +} + +func NewCreateQuota(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) CreateQuota { + return CreateQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + } +} + +func (cmd CreateQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-quota", + Description: T("Define a new resource quota"), + Usage: T("CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("i", T("Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.")), + flag_helpers.NewStringFlag("m", T("Total amount of memory (e.g. 1024M, 1G, 10G)")), + flag_helpers.NewIntFlag("r", T("Total number of routes")), + flag_helpers.NewIntFlag("s", T("Total number of service instances")), + cli.BoolFlag{Name: "allow-paid-service-plans", Usage: T("Can provision instances of paid service plans")}, + }, + } +} + +func (cmd CreateQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd CreateQuota) Run(context *cli.Context) { + name := context.Args()[0] + + cmd.ui.Say(T("Creating quota {{.QuotaName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(name), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + quota := models.QuotaFields{ + Name: name, + } + + memoryLimit := context.String("m") + if memoryLimit != "" { + parsedMemory, err := formatters.ToMegabytes(memoryLimit) + if err != nil { + cmd.ui.Failed(T("Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", map[string]interface{}{"MemoryLimit": memoryLimit, "Err": err})) + } + + quota.MemoryLimit = parsedMemory + } + + instanceMemoryLimit := context.String("i") + if instanceMemoryLimit == "-1" || instanceMemoryLimit == "" { + quota.InstanceMemoryLimit = -1 + } else { + parsedMemory, errr := formatters.ToMegabytes(instanceMemoryLimit) + if errr != nil { + cmd.ui.Failed(T("Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", map[string]interface{}{"MemoryLimit": instanceMemoryLimit, "Err": errr})) + } + quota.InstanceMemoryLimit = parsedMemory + } + + if context.IsSet("r") { + quota.RoutesLimit = context.Int("r") + } + + if context.IsSet("s") { + quota.ServicesLimit = context.Int("s") + } + + if context.IsSet("allow-paid-service-plans") { + quota.NonBasicServicesAllowed = true + } + + err := cmd.quotaRepo.Create(quota) + + httpErr, ok := err.(errors.HttpError) + if ok && httpErr.ErrorCode() == errors.QUOTA_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Quota Definition {{.QuotaName}} already exists", map[string]interface{}{"QuotaName": quota.Name})) + return + } + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/quota/create_quota_test.go b/cf/commands/quota/create_quota_test.go new file mode 100644 index 00000000000..7d445def81f --- /dev/null +++ b/cf/commands/quota/create_quota_test.go @@ -0,0 +1,150 @@ +package quota_test + +import ( + . "github.com/cloudfoundry/cli/cf/commands/quota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" +) + +var _ = Describe("create-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewCreateQuota(ui, configuration.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when the user is not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("my-quota", "-m", "50G")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails requirements when called without a quota name", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("creates a quota with a given name", func() { + runCommand("my-quota") + Expect(quotaRepo.CreateArgsForCall(0).Name).To(Equal("my-quota")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating quota", "my-quota", "my-user", "..."}, + []string{"OK"}, + )) + }) + + Context("when the -i flag is not provided", func() { + It("defaults the memory limit to unlimited", func() { + runCommand("my-quota") + + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + }) + + Context("when the -m flag is provided", func() { + It("sets the memory limit", func() { + runCommand("-m", "50G", "erryday makin fitty jeez") + Expect(quotaRepo.CreateArgsForCall(0).MemoryLimit).To(Equal(int64(51200))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("whoops", "12") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when the -i flag is provided", func() { + It("sets the memory limit", func() { + runCommand("-i", "50G", "erryday makin fitty jeez") + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(51200))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("-i", "whoops", "wit mah hussle", "12") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + + Context("and the provided value is -1", func() { + It("sets the memory limit", func() { + runCommand("-i", "-1", "yo") + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + }) + }) + It("sets the route limit", func() { + runCommand("-r", "12", "ecstatic") + + Expect(quotaRepo.CreateArgsForCall(0).RoutesLimit).To(Equal(12)) + }) + + It("sets the service instance limit", func() { + runCommand("-s", "42", "black star") + Expect(quotaRepo.CreateArgsForCall(0).ServicesLimit).To(Equal(42)) + }) + + It("defaults to not allowing paid service plans", func() { + runCommand("my-pro-bono-quota") + Expect(quotaRepo.CreateArgsForCall(0).NonBasicServicesAllowed).To(BeFalse()) + }) + + Context("when requesting to allow paid service plans", func() { + It("creates the quota with paid service plans allowed", func() { + runCommand("--allow-paid-service-plans", "my-for-profit-quota") + Expect(quotaRepo.CreateArgsForCall(0).NonBasicServicesAllowed).To(BeTrue()) + }) + }) + + Context("when creating a quota returns an error", func() { + It("alerts the user when creating the quota fails", func() { + quotaRepo.CreateReturns(errors.New("WHOOP THERE IT IS")) + runCommand("my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating quota", "my-quota"}, + []string{"FAILED"}, + )) + }) + + It("warns the user when quota already exists", func() { + quotaRepo.CreateReturns(errors.NewHttpError(400, "240002", "Quota Definition is taken: quota-sct")) + runCommand("Banana") + + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"FAILED"}, + )) + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"already exists"})) + }) + + }) + }) +}) diff --git a/cf/commands/quota/delete_quota.go b/cf/commands/quota/delete_quota.go new file mode 100644 index 00000000000..9461b52e354 --- /dev/null +++ b/cf/commands/quota/delete_quota.go @@ -0,0 +1,86 @@ +package quota + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository + orgReq requirements.OrganizationRequirement +} + +func NewDeleteQuota(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) (cmd *DeleteQuota) { + return &DeleteQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + } +} + +func (cmd *DeleteQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-quota", + Description: T("Delete a quota"), + Usage: T("CF_NAME delete-quota QUOTA [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteQuota) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + err = errors.New(T("Incorrect Usage")) + cmd.ui.FailWithUsage(c) + return + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *DeleteQuota) Run(c *cli.Context) { + quotaName := c.Args()[0] + + if !c.Bool("f") { + response := cmd.ui.ConfirmDelete("quota", quotaName) + if !response { + return + } + } + + cmd.ui.Say(T("Deleting quota {{.QuotaName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(quotaName), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + quota, apiErr := cmd.quotaRepo.FindByName(quotaName) + + switch (apiErr).(type) { + case nil: // no error + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Quota {{.QuotaName}} does not exist", map[string]interface{}{"QuotaName": quotaName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + } + + apiErr = cmd.quotaRepo.Delete(quota.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/quota/delete_quota_test.go b/cf/commands/quota/delete_quota_test.go new file mode 100644 index 00000000000..2240aee3d20 --- /dev/null +++ b/cf/commands/quota/delete_quota_test.go @@ -0,0 +1,141 @@ +package quota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/quota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteQuota(ui, configuration.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when the user is not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("my-quota")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails requirements when called without a quota name", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("When the quota provided exists", func() { + BeforeEach(func() { + quota := models.QuotaFields{} + quota.Name = "my-quota" + quota.Guid = "my-quota-guid" + + quotaRepo.FindByNameReturns(quota, nil) + }) + + It("deletes a quota with a given name when the user confirms", func() { + ui.Inputs = []string{"y"} + + runCommand("my-quota") + Expect(quotaRepo.DeleteArgsForCall(0)).To(Equal("my-quota-guid")) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete the quota", "my-quota"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting quota", "my-quota", "my-user"}, + []string{"OK"}, + )) + }) + + It("does not prompt when the -f flag is provided", func() { + runCommand("-f", "my-quota") + + Expect(quotaRepo.DeleteArgsForCall(0)).To(Equal("my-quota-guid")) + + Expect(ui.Prompts).To(BeEmpty()) + }) + + It("shows an error when deletion fails", func() { + quotaRepo.DeleteReturns(errors.New("some error")) + + runCommand("-f", "my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "my-quota"}, + []string{"FAILED"}, + )) + }) + }) + + Context("when finding the quota fails", func() { + Context("when the quota provided does not exist", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{}, errors.NewModelNotFoundError("Quota", "non-existent-quota")) + }) + + It("warns the user when that the quota does not exist", func() { + runCommand("-f", "non-existent-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "non-existent-quota"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"non-existent-quota", "does not exist"}, + )) + }) + }) + + Context("when other types of error occur", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{}, errors.New("some error")) + }) + + It("shows an error", func() { + runCommand("-f", "my-quota") + + Expect(ui.WarnOutputs).ToNot(ContainSubstrings( + []string{"my-quota", "does not exist"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + + }) + }) + }) + }) +}) diff --git a/cf/commands/quota/quota.go b/cf/commands/quota/quota.go new file mode 100644 index 00000000000..66909ef399a --- /dev/null +++ b/cf/commands/quota/quota.go @@ -0,0 +1,78 @@ +package quota + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + "strconv" + + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type showQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository +} + +func NewShowQuota(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) *showQuota { + return &showQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + } +} + +func (cmd *showQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "quota", + Usage: T("CF_NAME quota QUOTA"), + Description: T("Show quota info"), + } +} + +func (cmd *showQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *showQuota) Run(context *cli.Context) { + quotaName := context.Args()[0] + cmd.ui.Say(T("Getting quota {{.QuotaName}} info as {{.Username}}...", map[string]interface{}{"QuotaName": quotaName, "Username": cmd.config.Username()})) + + quota, err := cmd.quotaRepo.FindByName(quotaName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + + var megabytes string + if quota.InstanceMemoryLimit == -1 { + megabytes = T("unlimited") + } else { + megabytes = formatters.ByteSize(quota.InstanceMemoryLimit * formatters.MEGABYTE) + } + + servicesLimit := strconv.Itoa(quota.ServicesLimit) + if servicesLimit == "-1" { + servicesLimit = T("unlimited") + } + table := terminal.NewTable(cmd.ui, []string{"", ""}) + table.Add(T("Total Memory"), formatters.ByteSize(quota.MemoryLimit*formatters.MEGABYTE)) + table.Add(T("Instance Memory"), megabytes) + table.Add(T("Routes"), fmt.Sprintf("%d", quota.RoutesLimit)) + table.Add(T("Services"), servicesLimit) + table.Add(T("Paid service plans"), formatters.Allowed(quota.NonBasicServicesAllowed)) + table.Print() +} diff --git a/cf/commands/quota/quota_suite_test.go b/cf/commands/quota/quota_suite_test.go new file mode 100644 index 00000000000..bd2e5666f7b --- /dev/null +++ b/cf/commands/quota/quota_suite_test.go @@ -0,0 +1,19 @@ +package quota_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestQuota(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Quota Suite") +} diff --git a/cf/commands/quota/quota_test.go b/cf/commands/quota/quota_test.go new file mode 100644 index 00000000000..db2143bbcdb --- /dev/null +++ b/cf/commands/quota/quota_test.go @@ -0,0 +1,156 @@ +package quota_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + . "github.com/cloudfoundry/cli/cf/commands/quota" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("quota", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + quotaRepo *fakes.FakeQuotaRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + quotaRepo = &fakes.FakeQuotaRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewShowQuota(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("When not logged in", func() { + It("fails requirements", func() { + Expect(runCommand("quota-name")).To(BeFalse()) + }) + }) + + Context("When logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("When not providing a quota name", func() { + It("fails with usage", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("When providing a quota name", func() { + Context("that exists", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{ + Guid: "my-quota-guid", + Name: "muh-muh-muh-my-qua-quota", + MemoryLimit: 512, + InstanceMemoryLimit: 5, + RoutesLimit: 2000, + ServicesLimit: 47, + NonBasicServicesAllowed: true, + }, nil) + }) + + It("shows you that quota", func() { + runCommand("muh-muh-muh-my-qua-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting quota", "muh-muh-muh-my-qua-quota", "my-user"}, + []string{"OK"}, + []string{"Total Memory", "512M"}, + []string{"Instance Memory", "5M"}, + []string{"Routes", "2000"}, + []string{"Services", "47"}, + []string{"Paid service plans", "allowed"}, + )) + }) + }) + + Context("when instance memory limit is -1", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{ + Guid: "my-quota-guid", + Name: "muh-muh-muh-my-qua-quota", + MemoryLimit: 512, + InstanceMemoryLimit: -1, + RoutesLimit: 2000, + ServicesLimit: 47, + NonBasicServicesAllowed: true, + }, nil) + }) + + It("shows you that quota", func() { + runCommand("muh-muh-muh-my-qua-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting quota", "muh-muh-muh-my-qua-quota", "my-user"}, + []string{"OK"}, + []string{"Total Memory", "512M"}, + []string{"Instance Memory", "unlimited"}, + []string{"Routes", "2000"}, + []string{"Services", "47"}, + []string{"Paid service plans", "allowed"}, + )) + }) + }) + + Context("when the services limit is -1", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{ + Guid: "my-quota-guid", + Name: "muh-muh-muh-my-qua-quota", + MemoryLimit: 512, + InstanceMemoryLimit: 14, + RoutesLimit: 2000, + ServicesLimit: -1, + NonBasicServicesAllowed: true, + }, nil) + }) + + It("shows you that quota", func() { + runCommand("muh-muh-muh-my-qua-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting quota", "muh-muh-muh-my-qua-quota", "my-user"}, + []string{"OK"}, + []string{"Total Memory", "512M"}, + []string{"Instance Memory", "14M"}, + []string{"Routes", "2000"}, + []string{"Services", "unlimited"}, + []string{"Paid service plans", "allowed"}, + )) + }) + }) + + Context("that doesn't exist", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.QuotaFields{}, errors.New("oops i accidentally a quota")) + }) + + It("gives an error", func() { + runCommand("an-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"oops"}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/quota/quotas.go b/cf/commands/quota/quotas.go new file mode 100644 index 00000000000..43fa175ef21 --- /dev/null +++ b/cf/commands/quota/quotas.go @@ -0,0 +1,87 @@ +package quota + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + "strconv" + + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListQuotas struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository +} + +func NewListQuotas(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) (cmd *ListQuotas) { + return &ListQuotas{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + } +} + +func (cmd *ListQuotas) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "quotas", + Description: T("List available usage quotas"), + Usage: T("CF_NAME quotas"), + } +} + +func (cmd *ListQuotas) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *ListQuotas) Run(c *cli.Context) { + cmd.ui.Say(T("Getting quotas as {{.Username}}...", map[string]interface{}{"Username": terminal.EntityNameColor(cmd.config.Username())})) + + quotas, apiErr := cmd.quotaRepo.FindAll() + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("name"), T("total memory limit"), T("instance memory limit"), T("routes"), T("service instances"), T("paid service plans")}) + + var megabytes string + for _, quota := range quotas { + if quota.InstanceMemoryLimit == -1 { + megabytes = T("unlimited") + } else { + megabytes = formatters.ByteSize(quota.InstanceMemoryLimit * formatters.MEGABYTE) + } + + servicesLimit := strconv.Itoa(quota.ServicesLimit) + if quota.ServicesLimit == -1 { + servicesLimit = T("unlimited") + } + + table.Add( + quota.Name, + formatters.ByteSize(quota.MemoryLimit*formatters.MEGABYTE), + megabytes, + fmt.Sprintf("%d", quota.RoutesLimit), + fmt.Sprintf(servicesLimit), + formatters.Allowed(quota.NonBasicServicesAllowed), + ) + } + + table.Print() +} diff --git a/cf/commands/quota/quotas_test.go b/cf/commands/quota/quotas_test.go new file mode 100644 index 00000000000..6cec5b3ca66 --- /dev/null +++ b/cf/commands/quota/quotas_test.go @@ -0,0 +1,113 @@ +package quota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/quota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("quotas command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewListQuotas(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when quotas exist", func() { + BeforeEach(func() { + quotaRepo.FindAllReturns([]models.QuotaFields{ + models.QuotaFields{ + Name: "quota-name", + MemoryLimit: 1024, + InstanceMemoryLimit: 512, + RoutesLimit: 111, + ServicesLimit: 222, + NonBasicServicesAllowed: true, + }, + models.QuotaFields{ + Name: "quota-non-basic-not-allowed", + MemoryLimit: 434, + InstanceMemoryLimit: -1, + RoutesLimit: 1, + ServicesLimit: 2, + NonBasicServicesAllowed: false, + }, + }, nil) + }) + + It("lists quotas", func() { + Expect(Expect(runCommand()).To(HavePassedRequirements())).To(HavePassedRequirements()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting quotas as", "my-user"}, + []string{"OK"}, + []string{"name", "total memory limit", "instance memory limit", "routes", "service instances", "paid service plans"}, + []string{"quota-name", "1G", "512M", "111", "222", "allowed"}, + []string{"quota-non-basic-not-allowed", "434M", "unlimited", "1", "2", "disallowed"}, + )) + }) + + It("displays unlimited services properly", func() { + quotaRepo.FindAllReturns([]models.QuotaFields{ + models.QuotaFields{ + Name: "quota-with-no-limit-to-services", + MemoryLimit: 434, + InstanceMemoryLimit: 1, + RoutesLimit: 2, + ServicesLimit: -1, + NonBasicServicesAllowed: false, + }, + }, nil) + Expect(Expect(runCommand()).To(HavePassedRequirements())).To(HavePassedRequirements()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"quota-with-no-limit-to-services", "434M", "1", "2", "unlimited", "disallowed"}, + )) + }) + }) + + Context("when an error occurs fetching quotas", func() { + BeforeEach(func() { + quotaRepo.FindAllReturns([]models.QuotaFields{}, errors.New("I haz a borken!")) + }) + + It("prints an error", func() { + Expect(runCommand()).To(HavePassedRequirements()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting quotas as", "my-user"}, + []string{"FAILED"}, + )) + }) + }) + +}) diff --git a/cf/commands/quota/update_quota.go b/cf/commands/quota/update_quota.go new file mode 100644 index 00000000000..cd76c808ff8 --- /dev/null +++ b/cf/commands/quota/update_quota.go @@ -0,0 +1,128 @@ +package quota + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type updateQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo quotas.QuotaRepository +} + +func NewUpdateQuota(ui terminal.UI, config core_config.Reader, quotaRepo quotas.QuotaRepository) *updateQuota { + return &updateQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + } +} + +func (cmd *updateQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-quota", + Description: T("Update an existing resource quota"), + Usage: T("CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("i", T("Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)")), + flag_helpers.NewStringFlag("m", T("Total amount of memory (e.g. 1024M, 1G, 10G)")), + flag_helpers.NewStringFlag("n", T("New name")), + flag_helpers.NewIntFlag("r", T("Total number of routes")), + flag_helpers.NewIntFlag("s", T("Total number of service instances")), + cli.BoolFlag{Name: "allow-paid-service-plans", Usage: T("Can provision instances of paid service plans")}, + cli.BoolFlag{Name: "disallow-paid-service-plans", Usage: T("Cannot provision instances of paid service plans")}, + }, + } +} + +func (cmd *updateQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *updateQuota) Run(c *cli.Context) { + oldQuotaName := c.Args()[0] + quota, err := cmd.quotaRepo.FindByName(oldQuotaName) + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + allowPaidServices := c.Bool("allow-paid-service-plans") + disallowPaidServices := c.Bool("disallow-paid-service-plans") + if allowPaidServices && disallowPaidServices { + cmd.ui.Failed(T("Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.")) + } + + if allowPaidServices { + quota.NonBasicServicesAllowed = true + } + + if disallowPaidServices { + quota.NonBasicServicesAllowed = false + } + + if c.String("i") != "" { + var memory int64 + + if c.String("i") == "-1" { + memory = -1 + } else { + var formatError error + + memory, formatError = formatters.ToMegabytes(c.String("i")) + + if formatError != nil { + cmd.ui.FailWithUsage(c) + } + } + + quota.InstanceMemoryLimit = memory + } + + if c.String("m") != "" { + memory, formatError := formatters.ToMegabytes(c.String("m")) + + if formatError != nil { + cmd.ui.FailWithUsage(c) + } + + quota.MemoryLimit = memory + } + + if c.String("n") != "" { + quota.Name = c.String("n") + } + + if c.IsSet("s") { + quota.ServicesLimit = c.Int("s") + } + + if c.IsSet("r") { + quota.RoutesLimit = c.Int("r") + } + + cmd.ui.Say(T("Updating quota {{.QuotaName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(oldQuotaName), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + err = cmd.quotaRepo.Update(quota) + if err != nil { + cmd.ui.Failed(err.Error()) + } + cmd.ui.Ok() +} diff --git a/cf/commands/quota/update_quota_test.go b/cf/commands/quota/update_quota_test.go new file mode 100644 index 00000000000..91579419e54 --- /dev/null +++ b/cf/commands/quota/update_quota_test.go @@ -0,0 +1,177 @@ +package quota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/quotas/fakes" + . "github.com/cloudfoundry/cli/cf/commands/quota" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("app Command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + quotaRepo *fakes.FakeQuotaRepository + quota models.QuotaFields + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + quotaRepo = &fakes.FakeQuotaRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUpdateQuota(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails if not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("cf-plays-dwarf-fortress")).To(BeFalse()) + }) + + It("fails with usage when no arguments are given", func() { + passed := runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + Expect(passed).To(BeFalse()) + }) + }) + + Describe("updating quota fields", func() { + BeforeEach(func() { + quota = models.QuotaFields{ + Guid: "quota-guid", + Name: "quota-name", + MemoryLimit: 1024, + RoutesLimit: 111, + ServicesLimit: 222, + } + }) + + JustBeforeEach(func() { + quotaRepo.FindByNameReturns(quota, nil) + }) + + Context("when the -i flag is provided", func() { + It("updates the instance memory limit", func() { + runCommand("-i", "15G", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("quota-name")) + Expect(quotaRepo.UpdateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(15360))) + }) + + It("totally accepts -1 as a value because it means unlimited", func() { + runCommand("-i", "-1", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("quota-name")) + Expect(quotaRepo.UpdateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + + It("fails with usage when the value cannot be parsed", func() { + runCommand("-m", "blasé", "le-tired") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the -m flag is provided", func() { + It("updates the memory limit", func() { + runCommand("-m", "15G", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("quota-name")) + Expect(quotaRepo.UpdateArgsForCall(0).MemoryLimit).To(Equal(int64(15360))) + }) + + It("fails with usage when the value cannot be parsed", func() { + runCommand("-m", "blasé", "le-tired") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the -n flag is provided", func() { + It("updates the quota name", func() { + runCommand("-n", "quota-new-name", "quota-name") + + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("quota-new-name")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating quota", "quota-name", "as", "my-user"}, + []string{"OK"}, + )) + }) + }) + + It("updates the total allowed services", func() { + runCommand("-s", "9000", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).ServicesLimit).To(Equal(9000)) + }) + + It("updates the total allowed routes", func() { + runCommand("-r", "9001", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).RoutesLimit).To(Equal(9001)) + }) + + Context("update paid service plans", func() { + BeforeEach(func() { + quota.NonBasicServicesAllowed = false + }) + + It("changes to paid service plan when --allow flag is provided", func() { + runCommand("--allow-paid-service-plans", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).NonBasicServicesAllowed).To(BeTrue()) + }) + + It("shows an error when both --allow and --disallow flags are provided", func() { + runCommand("--allow-paid-service-plans", "--disallow-paid-service-plans", "quota-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Both flags are not permitted"}, + )) + }) + + Context("when paid services are allowed", func() { + BeforeEach(func() { + quota.NonBasicServicesAllowed = true + }) + It("changes to non-paid service plan when --disallow flag is provided", func() { + quotaRepo.FindByNameReturns(quota, nil) // updating an existing quota + + runCommand("--disallow-paid-service-plans", "quota-name") + Expect(quotaRepo.UpdateArgsForCall(0).NonBasicServicesAllowed).To(BeFalse()) + }) + }) + }) + }) + + It("shows an error when updating fails", func() { + quotaRepo.UpdateReturns(errors.New("I accidentally a quota")) + runCommand("-m", "1M", "dead-serious") + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + + It("shows the user an error when finding the quota fails", func() { + quotaRepo.FindByNameReturns(models.QuotaFields{}, errors.New("i can't believe it's not quotas!")) + + runCommand("-m", "50Somethings", "what-could-possibly-go-wrong?") + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + + It("shows a message explaining the update", func() { + quota.Name = "i-love-ui" + quotaRepo.FindByNameReturns(quota, nil) + + runCommand("-m", "50G", "i-love-ui") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating quota", "i-love-ui", "as", "my-user"}, + []string{"OK"}, + )) + }) +}) diff --git a/cf/commands/route/check_route.go b/cf/commands/route/check_route.go new file mode 100644 index 00000000000..d90a5d8b4ef --- /dev/null +++ b/cf/commands/route/check_route.go @@ -0,0 +1,86 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CheckRoute struct { + ui terminal.UI + config core_config.Reader + routeRepo api.RouteRepository + domainRepo api.DomainRepository +} + +func NewCheckRoute(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository, domainRepo api.DomainRepository) (cmd *CheckRoute) { + cmd = new(CheckRoute) + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + cmd.domainRepo = domainRepo + return +} + +func (cmd *CheckRoute) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "check-route", + Description: T("Perform a simple check to determine whether a route currently exists or not."), + Usage: T("CF_NAME check-route HOST DOMAIN"), + } +} + +func (cmd *CheckRoute) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + return + } + reqs = []requirements.Requirement{ + requirementsFactory.NewTargetedOrgRequirement(), + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *CheckRoute) Run(c *cli.Context) { + hostName := c.Args()[0] + domainName := c.Args()[1] + + cmd.ui.Say(T("Checking for route...")) + + exists, err := cmd.CheckRoute(hostName, domainName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + + if exists { + cmd.ui.Say(T("Route {{.HostName}}.{{.DomainName}} does exist", + map[string]interface{}{"HostName": hostName, "DomainName": domainName}, + )) + } else { + cmd.ui.Say(T("Route {{.HostName}}.{{.DomainName}} does not exist", + map[string]interface{}{"HostName": hostName, "DomainName": domainName}, + )) + } +} + +func (cmd *CheckRoute) CheckRoute(hostName, domainName string) (bool, error) { + orgGuid := cmd.config.OrganizationFields().Guid + domain, err := cmd.domainRepo.FindByNameInOrg(domainName, orgGuid) + if err != nil { + return false, err + } + + found, err := cmd.routeRepo.CheckIfExists(hostName, domain) + if err != nil { + return false, err + } + + return found, nil +} diff --git a/cf/commands/route/check_route_test.go b/cf/commands/route/check_route_test.go new file mode 100644 index 00000000000..88c34298299 --- /dev/null +++ b/cf/commands/route/check_route_test.go @@ -0,0 +1,144 @@ +package route_test + +import ( + "errors" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/route" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("check-route command", func() { + var ( + ui *testterm.FakeUI + routeRepo *testapi.FakeRouteRepository + domainRepo *testapi.FakeDomainRepository + requirementsFactory *testreq.FakeReqFactory + config core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + routeRepo = &testapi.FakeRouteRepository{} + domainRepo = &testapi.FakeDomainRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + config = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewCheckRoute(ui, config, routeRepo, domainRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand("foobar.example.com", "bar.example.com")).To(BeFalse()) + }) + + It("fails when no org is targeted", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("foobar.example.com", "bar.example.com")).To(BeFalse()) + }) + + It("fails when the number of arguments is greater than two", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + passed := runCommand("foobar.example.com", "hello", "world") + + Expect(passed).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when the number of arguments is less than two", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + passed := runCommand("foobar.example.com") + + Expect(passed).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the route already exists", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + routeRepo.CheckIfExistsFound = true + }) + + It("prints out route does exist", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + + runCommand("some-existing-route", "example.com") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Checking for route..."}, + []string{"OK"}, + []string{"Route some-existing-route.example.com does exist"}, + )) + }) + }) + + Context("when the route does not exist", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + routeRepo.CheckIfExistsFound = false + }) + + It("prints out route does not exist", func() { + + runCommand("non-existent-route", "example.com") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Checking for route..."}, + []string{"OK"}, + []string{"Route non-existent-route.example.com does not exist"}, + )) + }) + }) + + Context("when finding the domain returns an error", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + domainRepo.FindByNameInOrgApiResponse = errors.New("Domain not found") + }) + + It("prints out route does not exist", func() { + + runCommand("some-silly-route", "some-non-real-domain") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Checking for route..."}, + []string{"FAILED"}, + []string{"Domain not found"}, + )) + }) + }) + Context("when checking if the route exists returns an error", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + routeRepo.CheckIfExistsError = errors.New("Some stupid error") + }) + + It("prints out route does not exist", func() { + + runCommand("some-silly-route", "some-non-real-domain") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Checking for route..."}, + []string{"FAILED"}, + []string{"Some stupid error"}, + )) + }) + }) + +}) diff --git a/cf/commands/route/create_route.go b/cf/commands/route/create_route.go new file mode 100644 index 00000000000..5ba65ee8182 --- /dev/null +++ b/cf/commands/route/create_route.go @@ -0,0 +1,107 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RouteCreator interface { + CreateRoute(hostName string, domain models.DomainFields, space models.SpaceFields) (route models.Route, apiErr error) +} + +type CreateRoute struct { + ui terminal.UI + config core_config.Reader + routeRepo api.RouteRepository + spaceReq requirements.SpaceRequirement + domainReq requirements.DomainRequirement +} + +func NewCreateRoute(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository) (cmd *CreateRoute) { + cmd = new(CreateRoute) + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + return +} + +func (cmd *CreateRoute) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-route", + Description: T("Create a url route in a space for later use"), + Usage: T("CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("n", T("Hostname")), + }, + } +} + +func (cmd *CreateRoute) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + spaceName := c.Args()[0] + domainName := c.Args()[1] + + cmd.spaceReq = requirementsFactory.NewSpaceRequirement(spaceName) + cmd.domainReq = requirementsFactory.NewDomainRequirement(domainName) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + cmd.spaceReq, + cmd.domainReq, + } + return +} + +func (cmd *CreateRoute) Run(c *cli.Context) { + hostName := c.String("n") + space := cmd.spaceReq.GetSpace() + domain := cmd.domainReq.GetDomain() + + _, apiErr := cmd.CreateRoute(hostName, domain, space.SpaceFields) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } +} + +func (cmd *CreateRoute) CreateRoute(hostName string, domain models.DomainFields, space models.SpaceFields) (route models.Route, apiErr error) { + cmd.ui.Say(T("Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "Hostname": terminal.EntityNameColor(domain.UrlForHost(hostName)), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(space.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + route, apiErr = cmd.routeRepo.CreateInSpace(hostName, domain.Guid, space.Guid) + if apiErr != nil { + var findApiResponse error + route, findApiResponse = cmd.routeRepo.FindByHostAndDomain(hostName, domain) + + if findApiResponse != nil || + route.Space.Guid != space.Guid || + route.Domain.Guid != domain.Guid { + return + } + + apiErr = nil + cmd.ui.Ok() + cmd.ui.Warn(T("Route {{.URL}} already exists", + map[string]interface{}{"URL": route.URL()})) + return + } + + cmd.ui.Ok() + return +} diff --git a/cf/commands/route/create_route_test.go b/cf/commands/route/create_route_test.go new file mode 100644 index 00000000000..c2b95177106 --- /dev/null +++ b/cf/commands/route/create_route_test.go @@ -0,0 +1,133 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/route" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-route command", func() { + var ( + ui *testterm.FakeUI + routeRepo *testapi.FakeRouteRepository + requirementsFactory *testreq.FakeReqFactory + config core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + routeRepo = &testapi.FakeRouteRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + config = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewCreateRoute(ui, config, routeRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand("my-space", "example.com", "-n", "foo")).To(BeFalse()) + }) + + It("fails when an org is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("my-space", "example.com", "-n", "foo")).To(BeFalse()) + }) + + It("fails with usage when not provided two args", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + + runCommand("my-space") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in, targeted a space and given a domain that exists", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + requirementsFactory.Domain = models.DomainFields{ + Guid: "domain-guid", + Name: "example.com", + } + requirementsFactory.Space = models.Space{SpaceFields: models.SpaceFields{ + Guid: "my-space-guid", + Name: "my-space", + }} + }) + + It("creates routes, obviously", func() { + runCommand("-n", "host", "my-space", "example.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "host.example.com", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(routeRepo.CreateInSpaceHost).To(Equal("host")) + Expect(routeRepo.CreateInSpaceDomainGuid).To(Equal("domain-guid")) + Expect(routeRepo.CreateInSpaceSpaceGuid).To(Equal("my-space-guid")) + }) + + It("is idempotent", func() { + routeRepo.CreateInSpaceErr = true + routeRepo.FindByHostAndDomainReturns.Route = models.Route{ + Space: requirementsFactory.Space.SpaceFields, + Guid: "my-route-guid", + Host: "host", + Domain: requirementsFactory.Domain, + } + + runCommand("-n", "host", "my-space", "example.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route"}, + []string{"OK"}, + []string{"host.example.com", "already exists"}, + )) + + Expect(routeRepo.CreateInSpaceHost).To(Equal("host")) + Expect(routeRepo.CreateInSpaceDomainGuid).To(Equal("domain-guid")) + Expect(routeRepo.CreateInSpaceSpaceGuid).To(Equal("my-space-guid")) + }) + + Describe("RouteCreator interface", func() { + It("creates a route, given a domain and space", func() { + createdRoute := models.Route{} + createdRoute.Host = "my-host" + createdRoute.Guid = "my-route-guid" + routeRepo := &testapi.FakeRouteRepository{ + CreateInSpaceCreatedRoute: createdRoute, + } + + cmd := NewCreateRoute(ui, config, routeRepo) + route, apiErr := cmd.CreateRoute("my-host", requirementsFactory.Domain, requirementsFactory.Space.SpaceFields) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(route.Guid).To(Equal(createdRoute.Guid)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating route", "my-host.example.com", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(routeRepo.CreateInSpaceHost).To(Equal("my-host")) + Expect(routeRepo.CreateInSpaceDomainGuid).To(Equal("domain-guid")) + Expect(routeRepo.CreateInSpaceSpaceGuid).To(Equal("my-space-guid")) + }) + }) + }) +}) diff --git a/cf/commands/route/delete_orphaned_routes.go b/cf/commands/route/delete_orphaned_routes.go new file mode 100644 index 00000000000..cb86c16cca5 --- /dev/null +++ b/cf/commands/route/delete_orphaned_routes.go @@ -0,0 +1,80 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteOrphanedRoutes struct { + ui terminal.UI + routeRepo api.RouteRepository + config core_config.Reader +} + +func NewDeleteOrphanedRoutes(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository) (cmd DeleteOrphanedRoutes) { + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + return +} + +func (cmd DeleteOrphanedRoutes) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-orphaned-routes", + Description: T("Delete all orphaned routes (e.g.: those that are not mapped to an app)"), + Usage: T("CF_NAME delete-orphaned-routes [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd DeleteOrphanedRoutes) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = append(reqs, reqFactory.NewLoginRequirement()) + return +} + +func (cmd DeleteOrphanedRoutes) Run(c *cli.Context) { + + force := c.Bool("f") + if !force { + response := cmd.ui.Confirm(T("Really delete orphaned routes?{{.Prompt}}", + map[string]interface{}{"Prompt": terminal.PromptColor(">")})) + + if !response { + return + } + } + + cmd.ui.Say(T("Getting routes as {{.Username}} ...\n", + map[string]interface{}{"Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.routeRepo.ListRoutes(func(route models.Route) bool { + + if len(route.Apps) == 0 { + cmd.ui.Say(T("Deleting route {{.Route}}...", + map[string]interface{}{"Route": terminal.EntityNameColor(route.Host + "." + route.Domain.Name)})) + apiErr := cmd.routeRepo.Delete(route.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return false + } + } + return true + }) + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching routes.\n{{.Err}}", map[string]interface{}{"Err": apiErr.Error()})) + return + } + cmd.ui.Ok() +} diff --git a/cf/commands/route/delete_orphaned_routes_test.go b/cf/commands/route/delete_orphaned_routes_test.go new file mode 100644 index 00000000000..9c8fa1a99a0 --- /dev/null +++ b/cf/commands/route/delete_orphaned_routes_test.go @@ -0,0 +1,119 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/route" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-orphaned-routes command", func() { + var routeRepo *testapi.FakeRouteRepository + var reqFactory *testreq.FakeReqFactory + + BeforeEach(func() { + routeRepo = &testapi.FakeRouteRepository{} + reqFactory = &testreq.FakeReqFactory{} + }) + + It("fails requirements when not logged in", func() { + _, passed := callDeleteOrphanedRoutes("y", []string{}, reqFactory, routeRepo) + Expect(passed).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + reqFactory.LoginSuccess = true + ui, passed := callDeleteOrphanedRoutes("y", []string{"blahblah"}, reqFactory, routeRepo) + Expect(passed).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("when logged in successfully", func() { + + BeforeEach(func() { + reqFactory.LoginSuccess = true + }) + + It("passes requirements when logged in", func() { + _, passed := callDeleteOrphanedRoutes("y", []string{}, reqFactory, routeRepo) + Expect(passed).To(BeTrue()) + }) + + It("passes when confirmation is provided", func() { + var ui *testterm.FakeUI + domain := models.DomainFields{Name: "example.com"} + domain2 := models.DomainFields{Name: "cookieclicker.co"} + + app1 := models.ApplicationFields{Name: "dora"} + + route := models.Route{} + route.Host = "hostname-1" + route.Domain = domain + route.Apps = []models.ApplicationFields{app1} + + route2 := models.Route{} + route2.Guid = "route2-guid" + route2.Host = "hostname-2" + route2.Domain = domain2 + + routeRepo.Routes = []models.Route{route, route2} + + ui, _ = callDeleteOrphanedRoutes("y", []string{}, reqFactory, routeRepo) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete orphaned routes"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting route", "hostname-2.cookieclicker.co"}, + []string{"OK"}, + )) + Expect(routeRepo.DeletedRouteGuids).To(ContainElement("route2-guid")) + }) + + It("passes when the force flag is used", func() { + var ui *testterm.FakeUI + domain := models.DomainFields{Name: "example.com"} + domain2 := models.DomainFields{Name: "cookieclicker.co"} + + app1 := models.ApplicationFields{Name: "dora"} + + route := models.Route{} + route.Host = "hostname-1" + route.Domain = domain + route.Apps = []models.ApplicationFields{app1} + + route2 := models.Route{} + route2.Guid = "route2-guid" + route2.Host = "hostname-2" + route2.Domain = domain2 + + routeRepo.Routes = []models.Route{route, route2} + + ui, _ = callDeleteOrphanedRoutes("", []string{"-f"}, reqFactory, routeRepo) + + Expect(len(ui.Prompts)).To(Equal(0)) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting route", "hostname-2.cookieclicker.co"}, + []string{"OK"}, + )) + Expect(routeRepo.DeletedRouteGuids).To(ContainElement("route2-guid")) + }) + }) +}) + +func callDeleteOrphanedRoutes(confirmation string, args []string, reqFactory *testreq.FakeReqFactory, routeRepo *testapi.FakeRouteRepository) (*testterm.FakeUI, bool) { + ui := &testterm.FakeUI{Inputs: []string{confirmation}} + configRepo := testconfig.NewRepositoryWithDefaults() + cmd := NewDeleteOrphanedRoutes(ui, configRepo, routeRepo) + passed := testcmd.RunCommand(cmd, args, reqFactory) + + return ui, passed +} diff --git a/cf/commands/route/delete_route.go b/cf/commands/route/delete_route.go new file mode 100644 index 00000000000..8aacb14b38c --- /dev/null +++ b/cf/commands/route/delete_route.go @@ -0,0 +1,93 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteRoute struct { + ui terminal.UI + config core_config.Reader + routeRepo api.RouteRepository + domainReq requirements.DomainRequirement +} + +func NewDeleteRoute(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository) (cmd *DeleteRoute) { + cmd = new(DeleteRoute) + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + return +} + +func (cmd *DeleteRoute) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-route", + Description: T("Delete a route"), + Usage: T("CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + flag_helpers.NewStringFlag("n", T("Hostname")), + }, + } +} + +func (cmd *DeleteRoute) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.domainReq = requirementsFactory.NewDomainRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.domainReq, + } + return +} + +func (cmd *DeleteRoute) Run(c *cli.Context) { + host := c.String("n") + domainName := c.Args()[0] + + url := domainName + if host != "" { + url = host + "." + domainName + } + if !c.Bool("f") { + if !cmd.ui.ConfirmDelete("route", url) { + return + } + } + + cmd.ui.Say(T("Deleting route {{.URL}}...", map[string]interface{}{"URL": terminal.EntityNameColor(url)})) + + domain := cmd.domainReq.GetDomain() + route, apiErr := cmd.routeRepo.FindByHostAndDomain(host, domain) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Warn(T("Unable to delete, route '{{.URL}}' does not exist.", + map[string]interface{}{"URL": url})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + apiErr = cmd.routeRepo.Delete(route.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/route/delete_route_test.go b/cf/commands/route/delete_route_test.go new file mode 100644 index 00000000000..48290792de7 --- /dev/null +++ b/cf/commands/route/delete_route_test.go @@ -0,0 +1,111 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/route" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-route command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + routeRepo *testapi.FakeRouteRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"yes"}} + + routeRepo = &testapi.FakeRouteRepository{} + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + } + }) + + runCommand := func(args ...string) bool { + configRepo := testconfig.NewRepositoryWithDefaults() + cmd := NewDeleteRoute(ui, configRepo, routeRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("does not pass requirements", func() { + Expect(runCommand("-n", "my-host", "example.com")).To(BeFalse()) + }) + }) + + Context("when logged in successfully", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + route := models.Route{Guid: "route-guid"} + route.Domain = models.DomainFields{ + Guid: "domain-guid", + Name: "example.com", + } + routeRepo.FindByHostAndDomainReturns.Route = route + }) + + It("fails with usage when given zero args", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("does not fail with usage when provided with a domain", func() { + runCommand("example.com") + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + + It("does not fail with usage when provided a hostname", func() { + runCommand("-n", "my-host", "example.com") + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + + It("deletes routes when the user confirms", func() { + runCommand("-n", "my-host", "example.com") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the route my-host"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting route", "my-host.example.com"}, + []string{"OK"}, + )) + Expect(routeRepo.DeletedRouteGuids).To(Equal([]string{"route-guid"})) + }) + + It("does not prompt the user to confirm when they pass the '-f' flag", func() { + ui.Inputs = []string{} + runCommand("-f", "-n", "my-host", "example.com") + + Expect(ui.Prompts).To(BeEmpty()) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "my-host.example.com"}, + []string{"OK"}, + )) + Expect(routeRepo.DeletedRouteGuids).To(Equal([]string{"route-guid"})) + }) + + It("succeeds with a warning when the route does not exist", func() { + routeRepo.FindByHostAndDomainReturns.Error = errors.NewModelNotFoundError("Org", "not found") + + runCommand("-n", "my-host", "example.com") + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"my-host", "does not exist"})) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"OK"})) + }) + }) +}) diff --git a/cf/commands/route/map_route.go b/cf/commands/route/map_route.go new file mode 100644 index 00000000000..a60dce9c588 --- /dev/null +++ b/cf/commands/route/map_route.go @@ -0,0 +1,86 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type MapRoute struct { + ui terminal.UI + config core_config.Reader + routeRepo api.RouteRepository + appReq requirements.ApplicationRequirement + domainReq requirements.DomainRequirement + routeCreator RouteCreator +} + +func NewMapRoute(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository, routeCreator RouteCreator) (cmd *MapRoute) { + cmd = new(MapRoute) + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + cmd.routeCreator = routeCreator + return +} + +func (cmd *MapRoute) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "map-route", + Description: T("Add a url route to an app"), + Usage: T("CF_NAME map-route APP DOMAIN [-n HOSTNAME]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("n", T("Hostname")), + }, + } +} + +func (cmd *MapRoute) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + appName := c.Args()[0] + domainName := c.Args()[1] + + cmd.appReq = requirementsFactory.NewApplicationRequirement(appName) + cmd.domainReq = requirementsFactory.NewDomainRequirement(domainName) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.appReq, + cmd.domainReq, + } + return +} + +func (cmd *MapRoute) Run(c *cli.Context) { + hostName := c.String("n") + domain := cmd.domainReq.GetDomain() + app := cmd.appReq.GetApplication() + + route, apiErr := cmd.routeCreator.CreateRoute(hostName, domain, cmd.config.SpaceFields()) + if apiErr != nil { + cmd.ui.Failed(T("Error resolving route:\n{{.Err}}", map[string]interface{}{"Err": apiErr.Error()})) + } + cmd.ui.Say(T("Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "URL": terminal.EntityNameColor(route.URL()), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr = cmd.routeRepo.Bind(route.Guid, app.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/route/map_route_test.go b/cf/commands/route/map_route_test.go new file mode 100644 index 00000000000..1dc358181d4 --- /dev/null +++ b/cf/commands/route/map_route_test.go @@ -0,0 +1,80 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/route" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("map-route command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + routeRepo *testapi.FakeRouteRepository + requirementsFactory *testreq.FakeReqFactory + routeCreator *testcmd.FakeRouteCreator + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + configRepo = testconfig.NewRepositoryWithDefaults() + routeRepo = new(testapi.FakeRouteRepository) + routeCreator = &testcmd.FakeRouteCreator{} + requirementsFactory = new(testreq.FakeReqFactory) + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewMapRoute(ui, configRepo, routeRepo, routeCreator), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not invoked with exactly two args", func() { + runCommand("whoops-all-crunchberries") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("whatever", "shuttup")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + domain := models.DomainFields{Guid: "my-domain-guid", Name: "example.com"} + route := models.Route{Guid: "my-route-guid", Host: "foo", Domain: domain} + + app := models.Application{} + app.Guid = "my-app-guid" + app.Name = "my-app" + + requirementsFactory.LoginSuccess = true + requirementsFactory.Application = app + requirementsFactory.Domain = domain + routeCreator.ReservedRoute = route + }) + + It("maps a route, obviously", func() { + passed := runCommand("-n", "my-host", "my-app", "my-domain.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Adding route", "foo.example.com", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(routeRepo.BoundRouteGuid).To(Equal("my-route-guid")) + Expect(routeRepo.BoundAppGuid).To(Equal("my-app-guid")) + Expect(passed).To(BeTrue()) + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(requirementsFactory.DomainName).To(Equal("my-domain.com")) + }) + }) +}) diff --git a/cf/commands/route/route_suite_test.go b/cf/commands/route/route_suite_test.go new file mode 100644 index 00000000000..4cb5f2af3bc --- /dev/null +++ b/cf/commands/route/route_suite_test.go @@ -0,0 +1,19 @@ +package route_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRoute(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Route Suite") +} diff --git a/cf/commands/route/routes.go b/cf/commands/route/routes.go new file mode 100644 index 00000000000..82be13f355e --- /dev/null +++ b/cf/commands/route/routes.go @@ -0,0 +1,75 @@ +package route + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "strings" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListRoutes struct { + ui terminal.UI + routeRepo api.RouteRepository + config core_config.Reader +} + +func NewListRoutes(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository) (cmd ListRoutes) { + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + return +} + +func (cmd ListRoutes) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "routes", + ShortName: "r", + Description: T("List all routes in the current space"), + Usage: "CF_NAME routes", + } +} + +func (cmd ListRoutes) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) ([]requirements.Requirement, error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + }, nil +} + +func (cmd ListRoutes) Run(c *cli.Context) { + cmd.ui.Say(T("Getting routes as {{.Username}} ...\n", + map[string]interface{}{"Username": terminal.EntityNameColor(cmd.config.Username())})) + + table := cmd.ui.Table([]string{T("host"), T("domain"), T("apps")}) + + noRoutes := true + apiErr := cmd.routeRepo.ListRoutes(func(route models.Route) bool { + noRoutes = false + appNames := []string{} + for _, app := range route.Apps { + appNames = append(appNames, app.Name) + } + + table.Add(route.Host, route.Domain.Name, strings.Join(appNames, ",")) + return true + }) + table.Print() + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching routes.\n{{.Err}}", map[string]interface{}{"Err": apiErr.Error()})) + return + } + + if noRoutes { + cmd.ui.Say(T("No routes found")) + } +} diff --git a/cf/commands/route/routes_test.go b/cf/commands/route/routes_test.go new file mode 100644 index 00000000000..7abf31fd919 --- /dev/null +++ b/cf/commands/route/routes_test.go @@ -0,0 +1,117 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/route" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("routes command", func() { + var ( + ui *testterm.FakeUI + routeRepo *testapi.FakeRouteRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedSpaceSuccess: true, + } + routeRepo = &testapi.FakeRouteRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewListRoutes(ui, configRepo, routeRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("login requirements", func() { + It("fails if the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).To(BeFalse()) + }) + + It("fails when an org and space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + + Expect(runCommand()).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when there are routes", func() { + BeforeEach(func() { + domain := models.DomainFields{Name: "example.com"} + domain2 := models.DomainFields{Name: "cookieclicker.co"} + + app1 := models.ApplicationFields{Name: "dora"} + app2 := models.ApplicationFields{Name: "bora"} + + route := models.Route{} + route.Host = "hostname-1" + route.Domain = domain + route.Apps = []models.ApplicationFields{app1} + + route2 := models.Route{} + route2.Host = "hostname-2" + route2.Domain = domain2 + route2.Apps = []models.ApplicationFields{app1, app2} + routeRepo.Routes = []models.Route{route, route2} + }) + + It("lists routes", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting routes", "my-user"}, + []string{"host", "domain", "apps"}, + []string{"hostname-1", "example.com", "dora"}, + []string{"hostname-2", "cookieclicker.co", "dora", "bora"}, + )) + }) + }) + + Context("when there are not routes", func() { + It("tells the user when no routes were found", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting routes"}, + []string{"No routes found"}, + )) + }) + }) + + Context("when there is an error listing routes", func() { + BeforeEach(func() { + routeRepo.ListErr = true + }) + + It("returns an error to the user", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting routes"}, + []string{"FAILED"}, + )) + }) + }) +}) diff --git a/cf/commands/route/unmap_route.go b/cf/commands/route/unmap_route.go new file mode 100644 index 00000000000..cb47002b2c5 --- /dev/null +++ b/cf/commands/route/unmap_route.go @@ -0,0 +1,94 @@ +package route + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnmapRoute struct { + ui terminal.UI + config core_config.Reader + routeRepo api.RouteRepository + appReq requirements.ApplicationRequirement + domainReq requirements.DomainRequirement +} + +func NewUnmapRoute(ui terminal.UI, config core_config.Reader, routeRepo api.RouteRepository) (cmd *UnmapRoute) { + cmd = new(UnmapRoute) + cmd.ui = ui + cmd.config = config + cmd.routeRepo = routeRepo + return +} + +func (cmd *UnmapRoute) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unmap-route", + Description: T("Remove a url route from an app"), + Usage: T("CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("n", T("Hostname")), + }, + } +} + +func (cmd *UnmapRoute) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + appName := c.Args()[0] + domainName := c.Args()[1] + + cmd.appReq = requirementsFactory.NewApplicationRequirement(appName) + cmd.domainReq = requirementsFactory.NewDomainRequirement(domainName) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.appReq, + cmd.domainReq, + } + return +} + +func (cmd *UnmapRoute) Run(c *cli.Context) { + hostName := c.String("n") + domain := cmd.domainReq.GetDomain() + app := cmd.appReq.GetApplication() + + route, apiErr := cmd.routeRepo.FindByHostAndDomain(hostName, domain) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + cmd.ui.Say(T("Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "URL": terminal.EntityNameColor(route.URL()), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + var routeFound bool + for _, appRoute := range app.Routes { + if appRoute.Guid == route.Guid { + routeFound = true + apiErr = cmd.routeRepo.Unbind(route.Guid, app.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + } + } + cmd.ui.Ok() + + if !routeFound { + cmd.ui.Warn(T("\nRoute to be unmapped is not currently mapped to the application.")) + } + +} diff --git a/cf/commands/route/unmap_route_test.go b/cf/commands/route/unmap_route_test.go new file mode 100644 index 00000000000..cf03fe27b3e --- /dev/null +++ b/cf/commands/route/unmap_route_test.go @@ -0,0 +1,135 @@ +package route_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/route" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("unmap-route command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + routeRepo *testapi.FakeRouteRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + configRepo = testconfig.NewRepositoryWithDefaults() + routeRepo = new(testapi.FakeRouteRepository) + requirementsFactory = new(testreq.FakeReqFactory) + }) + + runCommand := func(args ...string) bool { + cmd := NewUnmapRoute(ui, configRepo, routeRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when the user is not logged in", func() { + It("fails requirements", func() { + Expect(runCommand("my-app", "some-domain.com")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when the user does not provide two args", func() { + It("fails with usage", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user provides an app and a domain", func() { + BeforeEach(func() { + requirementsFactory.Application = models.Application{ + ApplicationFields: models.ApplicationFields{ + Guid: "my-app-guid", + Name: "my-app", + }, + Routes: []models.RouteSummary{ + models.RouteSummary{ + Guid: "my-route-guid", + }, + }, + } + + requirementsFactory.Domain = models.DomainFields{ + Guid: "my-domain-guid", + Name: "example.com", + } + routeRepo.FindByHostAndDomainReturns.Route = models.Route{ + Domain: requirementsFactory.Domain, + Guid: "my-route-guid", + Host: "foo", + } + }) + + It("passes requirements", func() { + Expect(runCommand("-n", "my-host", "my-app", "my-domain.com")).To(BeTrue()) + }) + + It("reads the app and domain from its requirements", func() { + runCommand("-n", "my-host", "my-app", "my-domain.com") + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(requirementsFactory.DomainName).To(Equal("my-domain.com")) + }) + + It("unmaps the route", func() { + runCommand("-n", "my-host", "my-app", "my-domain.com") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing route", "foo.example.com", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).ToNot(ContainSubstrings( + []string{"Route to be unmapped is not currently mapped to the application."}, + )) + + Expect(routeRepo.UnboundRouteGuid).To(Equal("my-route-guid")) + Expect(routeRepo.UnboundAppGuid).To(Equal("my-app-guid")) + }) + + Context("when the route does not exist for the app", func() { + BeforeEach(func() { + requirementsFactory.Application = models.Application{ + ApplicationFields: models.ApplicationFields{ + Guid: "my-app-guid", + Name: "my-app", + }, + Routes: []models.RouteSummary{ + models.RouteSummary{ + Guid: "not-my-route-guid", + }, + }, + } + }) + + It("informs the user the route did not exist on the applicaiton", func() { + runCommand("-n", "my-host", "my-app", "my-domain.com") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing route", "foo.example.com", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"Route to be unmapped is not currently mapped to the application."}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/bind_running_security_group.go b/cf/commands/securitygroup/bind_running_security_group.go new file mode 100644 index 00000000000..439a67fbe43 --- /dev/null +++ b/cf/commands/securitygroup/bind_running_security_group.go @@ -0,0 +1,75 @@ +package securitygroup + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type bindToRunningGroup struct { + ui terminal.UI + configRepo core_config.Reader + securityGroupRepo security_groups.SecurityGroupRepo + runningGroupRepo running.RunningSecurityGroupsRepo +} + +func NewBindToRunningGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo, runningGroupRepo running.RunningSecurityGroupsRepo) command.Command { + return &bindToRunningGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + runningGroupRepo: runningGroupRepo, + } +} + +func (cmd *bindToRunningGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME bind-running-security-group SECURITY_GROUP") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "bind-running-security-group", + Description: T("Bind a security group to the list of security groups to be used for running applications"), + Usage: strings.Join([]string{primaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd *bindToRunningGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *bindToRunningGroup) Run(context *cli.Context) { + name := context.Args()[0] + + securityGroup, err := cmd.securityGroupRepo.Read(name) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Say(T("Binding security group {{.security_group}} to defaults for running as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(securityGroup.Name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + err = cmd.runningGroupRepo.BindToRunningSet(securityGroup.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} diff --git a/cf/commands/securitygroup/bind_running_security_group_test.go b/cf/commands/securitygroup/bind_running_security_group_test.go new file mode 100644 index 00000000000..373a78d7bf9 --- /dev/null +++ b/cf/commands/securitygroup/bind_running_security_group_test.go @@ -0,0 +1,106 @@ +package securitygroup_test + +import ( + "errors" + + fakeRunning "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running/fakes" + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("bind-running-security-group command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + fakeSecurityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + fakeRunningSecurityGroupRepo *fakeRunning.FakeRunningSecurityGroupsRepo + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + fakeSecurityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + fakeRunningSecurityGroupRepo = &fakeRunning.FakeRunningSecurityGroupsRepo{} + }) + + runCommand := func(args ...string) bool { + cmd := NewBindToRunningGroup(ui, configRepo, fakeSecurityGroupRepo, fakeRunningSecurityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("name")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and provides the name of a group", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + group := models.SecurityGroup{} + group.Guid = "being-a-guid" + group.Name = "security-group-name" + fakeSecurityGroupRepo.ReadReturns(group, nil) + }) + + JustBeforeEach(func() { + runCommand("security-group-name") + }) + + It("Describes what it is doing to the user", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Binding", "security-group-name", "as", "my-user"}, + []string{"OK"}, + []string{"TIP: Changes will not apply to existing running applications until they are restarted."}, + )) + }) + + It("binds the group to the running group set", func() { + Expect(fakeSecurityGroupRepo.ReadArgsForCall(0)).To(Equal("security-group-name")) + Expect(fakeRunningSecurityGroupRepo.BindToRunningSetArgsForCall(0)).To(Equal("being-a-guid")) + }) + + Context("when binding the security group to the running set fails", func() { + BeforeEach(func() { + fakeRunningSecurityGroupRepo.BindToRunningSetReturns(errors.New("WOAH. I know kung fu")) + }) + + It("fails and describes the failure to the user", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"WOAH. I know kung fu"}, + )) + }) + }) + + Context("when the security group with the given name cannot be found", func() { + BeforeEach(func() { + fakeSecurityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.New("Crème insufficiently brûlée'd")) + }) + + It("fails and tells the user that the security group does not exist", func() { + Expect(fakeRunningSecurityGroupRepo.BindToRunningSetCallCount()).To(Equal(0)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/bind_security_group.go b/cf/commands/securitygroup/bind_security_group.go new file mode 100644 index 00000000000..5b9dbc57046 --- /dev/null +++ b/cf/commands/securitygroup/bind_security_group.go @@ -0,0 +1,103 @@ +package securitygroup + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/security_groups" + sgbinder "github.com/cloudfoundry/cli/cf/api/security_groups/spaces" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type BindSecurityGroup struct { + ui terminal.UI + configRepo core_config.Reader + orgRepo organizations.OrganizationRepository + spaceRepo spaces.SpaceRepository + securityGroupRepo security_groups.SecurityGroupRepo + spaceBinder sgbinder.SecurityGroupSpaceBinder +} + +func NewBindSecurityGroup( + ui terminal.UI, + configRepo core_config.Reader, + securityGroupRepo security_groups.SecurityGroupRepo, + spaceRepo spaces.SpaceRepository, + orgRepo organizations.OrganizationRepository, + spaceBinder sgbinder.SecurityGroupSpaceBinder, +) BindSecurityGroup { + return BindSecurityGroup{ + ui: ui, + configRepo: configRepo, + spaceRepo: spaceRepo, + orgRepo: orgRepo, + securityGroupRepo: securityGroupRepo, + spaceBinder: spaceBinder, + } +} + +func (cmd BindSecurityGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME bind-security-group SECURITY_GROUP ORG SPACE") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "bind-security-group", + Description: T("Bind a security group to a space"), + Usage: strings.Join([]string{primaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd BindSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) (reqs []requirements.Requirement, err error) { + if len(context.Args()) != 3 { + cmd.ui.FailWithUsage(context) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd BindSecurityGroup) Run(context *cli.Context) { + securityGroupName := context.Args()[0] + orgName := context.Args()[1] + spaceName := context.Args()[2] + + cmd.ui.Say(T("Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + map[string]interface{}{ + "security_group": securityGroupName, + "space": spaceName, + "organization": orgName, + "username": cmd.configRepo.Username(), + })) + + securityGroup, err := cmd.securityGroupRepo.Read(securityGroupName) + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + org, err := cmd.orgRepo.FindByName(orgName) + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + space, err := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + err = cmd.spaceBinder.BindSpace(securityGroup.Guid, space.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} diff --git a/cf/commands/securitygroup/bind_security_group_test.go b/cf/commands/securitygroup/bind_security_group_test.go new file mode 100644 index 00000000000..d6ed31032af --- /dev/null +++ b/cf/commands/securitygroup/bind_security_group_test.go @@ -0,0 +1,164 @@ +package securitygroup_test + +import ( + "github.com/cloudfoundry/cli/cf/api/fakes" + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + zoidberg "github.com/cloudfoundry/cli/cf/api/security_groups/spaces/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("bind-security-group command", func() { + var ( + ui *testterm.FakeUI + cmd BindSecurityGroup + configRepo core_config.ReadWriter + fakeSecurityGroupRepo *testapi.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + fakeSpaceRepo *fakes.FakeSpaceRepository + fakeOrgRepo *test_org.FakeOrganizationRepository + fakeSpaceBinder *zoidberg.FakeSecurityGroupSpaceBinder + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + fakeOrgRepo = &test_org.FakeOrganizationRepository{} + fakeSpaceRepo = &fakes.FakeSpaceRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + fakeSecurityGroupRepo = &testapi.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + fakeSpaceBinder = &zoidberg.FakeSecurityGroupSpaceBinder{} + cmd = NewBindSecurityGroup(ui, configRepo, fakeSecurityGroupRepo, fakeSpaceRepo, fakeOrgRepo, fakeSpaceBinder) + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("my-craaaaaazy-security-group", "my-org", "my-space")).To(BeFalse()) + }) + + It("succeeds when the user is logged in", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("my-craaaaaazy-security-group", "my-org", "my-space")).To(BeTrue()) + }) + + It("fails with usage when not provided the name of a security group, org, and space", func() { + requirementsFactory.LoginSuccess = true + runCommand("one fish", "two fish", "three fish", "purple fish") + + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and provides the name of a security group", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when a security group with that name does not exist", func() { + BeforeEach(func() { + fakeSecurityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.NewModelNotFoundError("security group", "my-nonexistent-security-group")) + }) + + It("fails and tells the user", func() { + runCommand("my-nonexistent-security-group", "my-org", "my-space") + + Expect(fakeSecurityGroupRepo.ReadArgsForCall(0)).To(Equal("my-nonexistent-security-group")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"security group", "my-nonexistent-security-group", "not found"}, + )) + }) + }) + + Context("when the org does not exist", func() { + BeforeEach(func() { + fakeOrgRepo.FindByNameReturns(models.Organization{}, errors.New("Org org not found")) + }) + + It("fails and tells the user", func() { + runCommand("sec group", "org", "space") + + Expect(fakeOrgRepo.FindByNameArgsForCall(0)).To(Equal("org")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Org", "org", "not found"}, + )) + }) + }) + + Context("when the space does not exist", func() { + BeforeEach(func() { + org := models.Organization{} + org.Name = "org-name" + org.Guid = "org-guid" + fakeOrgRepo.ListOrgsReturns([]models.Organization{org}, nil) + fakeOrgRepo.FindByNameReturns(org, nil) + fakeSpaceRepo.FindByNameInOrgError = errors.NewModelNotFoundError("Space", "space-name") + }) + + It("fails and tells the user", func() { + runCommand("sec group", "org-name", "space-name") + + Expect(fakeSpaceRepo.FindByNameInOrgName).To(Equal("space-name")) + Expect(fakeSpaceRepo.FindByNameInOrgOrgGuid).To(Equal("org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Space", "space-name", "not found"}, + )) + }) + }) + + Context("everything is hunky dory", func() { + BeforeEach(func() { + org := models.Organization{} + org.Name = "org-name" + org.Guid = "org-guid" + fakeOrgRepo.ListOrgsReturns([]models.Organization{org}, nil) + + space := models.Space{} + space.Name = "space-name" + space.Guid = "space-guid" + fakeSpaceRepo.FindByNameInOrgSpace = space + + securityGroup := models.SecurityGroup{} + securityGroup.Name = "security-group" + securityGroup.Guid = "security-group-guid" + fakeSecurityGroupRepo.ReadReturns(securityGroup, nil) + }) + + JustBeforeEach(func() { + runCommand("security-group", "org-name", "space-name") + }) + + It("assigns the security group to the space", func() { + secGroupGuid, spaceGuid := fakeSpaceBinder.BindSpaceArgsForCall(0) + Expect(secGroupGuid).To(Equal("security-group-guid")) + Expect(spaceGuid).To(Equal("space-guid")) + }) + + It("describes what it is doing for the user's benefit", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning security group security-group to space space-name in org org-name as my-user"}, + []string{"OK"}, + []string{"TIP: Changes will not apply to existing running applications until they are restarted."}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/bind_staging_security_group.go b/cf/commands/securitygroup/bind_staging_security_group.go new file mode 100644 index 00000000000..d02ad3a1646 --- /dev/null +++ b/cf/commands/securitygroup/bind_staging_security_group.go @@ -0,0 +1,69 @@ +package securitygroup + +import ( + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type bindToStagingGroup struct { + ui terminal.UI + configRepo core_config.Reader + securityGroupRepo security_groups.SecurityGroupRepo + stagingGroupRepo staging.StagingSecurityGroupsRepo +} + +func NewBindToStagingGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo, stagingGroupRepo staging.StagingSecurityGroupsRepo) command.Command { + return &bindToStagingGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + stagingGroupRepo: stagingGroupRepo, + } +} + +func (cmd *bindToStagingGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "bind-staging-security-group", + Description: T("Bind a security group to the list of security groups to be used for staging applications"), + Usage: T("CF_NAME bind-staging-security-group SECURITY_GROUP"), + } +} + +func (cmd *bindToStagingGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *bindToStagingGroup) Run(context *cli.Context) { + name := context.Args()[0] + + securityGroup, err := cmd.securityGroupRepo.Read(name) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Say(T("Binding security group {{.security_group}} to staging as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(securityGroup.Name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + err = cmd.stagingGroupRepo.BindToStagingSet(securityGroup.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/securitygroup/bind_staging_security_group_test.go b/cf/commands/securitygroup/bind_staging_security_group_test.go new file mode 100644 index 00000000000..ef25de494b6 --- /dev/null +++ b/cf/commands/securitygroup/bind_staging_security_group_test.go @@ -0,0 +1,105 @@ +package securitygroup_test + +import ( + "errors" + + fakeStaging "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging/fakes" + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("bind-staging-security-group command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + fakeSecurityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + fakeStagingSecurityGroupRepo *fakeStaging.FakeStagingSecurityGroupsRepo + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + fakeSecurityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + fakeStagingSecurityGroupRepo = &fakeStaging.FakeStagingSecurityGroupsRepo{} + }) + + runCommand := func(args ...string) bool { + cmd := NewBindToStagingGroup(ui, configRepo, fakeSecurityGroupRepo, fakeStagingSecurityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("name")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and provides the name of a group", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + group := models.SecurityGroup{} + group.Guid = "just-pretend-this-is-a-guid" + group.Name = "a-security-group-name" + fakeSecurityGroupRepo.ReadReturns(group, nil) + }) + + JustBeforeEach(func() { + runCommand("a-security-group-name") + }) + + It("binds the group to the default staging group set", func() { + Expect(fakeSecurityGroupRepo.ReadArgsForCall(0)).To(Equal("a-security-group-name")) + Expect(fakeStagingSecurityGroupRepo.BindToStagingSetArgsForCall(0)).To(Equal("just-pretend-this-is-a-guid")) + }) + + It("describes what it's doing to the user", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Binding", "a-security-group-name", "as", "my-user"}, + []string{"OK"}, + )) + }) + + Context("when binding the security group to the default set fails", func() { + BeforeEach(func() { + fakeStagingSecurityGroupRepo.BindToStagingSetReturns(errors.New("WOAH. I know kung fu")) + }) + + It("fails and describes the failure to the user", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"WOAH. I know kung fu"}, + )) + }) + }) + + Context("when the security group with the given name cannot be found", func() { + BeforeEach(func() { + fakeSecurityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.New("Crème insufficiently brûlée'd")) + }) + + It("fails and tells the user that the security group does not exist", func() { + Expect(fakeStagingSecurityGroupRepo.BindToStagingSetCallCount()).To(Equal(0)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/create_security_group.go b/cf/commands/securitygroup/create_security_group.go new file mode 100644 index 00000000000..9beffc8a9b2 --- /dev/null +++ b/cf/commands/securitygroup/create_security_group.go @@ -0,0 +1,104 @@ +package securitygroup + +import ( + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/json" + "github.com/codegangsta/cli" +) + +type CreateSecurityGroup struct { + ui terminal.UI + securityGroupRepo security_groups.SecurityGroupRepo + configRepo core_config.Reader +} + +func NewCreateSecurityGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo) CreateSecurityGroup { + return CreateSecurityGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + } +} + +func (cmd CreateSecurityGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE") + secondaryUsage := T(` The provided path can be an absolute or relative path to a file. The file should have + a single array with JSON objects inside describing the rules. The JSON Base Object is + omitted and only the square brackets and associated child object are required in the file. + + Valid json file example: + [ + { + "protocol": "tcp", + "destination": "10.244.1.18", + "ports": "3306" + } + ]`) + + return command_metadata.CommandMetadata{ + Name: "create-security-group", + Description: T("Create a security group"), + Usage: strings.Join([]string{primaryUsage, secondaryUsage}, "\n\n"), + } +} + +func (cmd CreateSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 2 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd CreateSecurityGroup) Run(context *cli.Context) { + name := context.Args()[0] + pathToJSONFile := context.Args()[1] + rules, err := json.ParseJSON(pathToJSONFile) + if err != nil { + cmd.ui.Failed(T(`Incorrect json format: file: {{.JSONFile}} + +Valid json file example: +[ + { + "protocol": "tcp", + "destination": "10.244.1.18", + "ports": "3306" + } +]`, map[string]interface{}{"JSONFile": pathToJSONFile})) + } + + cmd.ui.Say(T("Creating security group {{.security_group}} as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + err = cmd.securityGroupRepo.Create(name, rules) + + httpErr, ok := err.(errors.HttpError) + if ok && httpErr.ErrorCode() == errors.SECURITY_GROUP_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Security group {{.security_group}} {{.error_message}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "error_message": terminal.WarningColor(T("already exists")), + })) + return + } + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/securitygroup/create_security_group_test.go b/cf/commands/securitygroup/create_security_group_test.go new file mode 100644 index 00000000000..690008bcc71 --- /dev/null +++ b/cf/commands/securitygroup/create_security_group_test.go @@ -0,0 +1,136 @@ +package securitygroup_test + +import ( + "io/ioutil" + "os" + + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-security-group command", func() { + var ( + ui *testterm.FakeUI + securityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + securityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewCreateSecurityGroup(ui, configRepo, securityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("the-security-group")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails with usage when a rules file is not provided", func() { + requirementsFactory.LoginSuccess = true + runCommand("AWESOME_SECURITY_GROUP_NAME") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in", func() { + var tempFile *os.File + + BeforeEach(func() { + tempFile, _ = ioutil.TempFile("", "") + requirementsFactory.LoginSuccess = true + }) + + AfterEach(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + + JustBeforeEach(func() { + runCommand("my-group", tempFile.Name()) + }) + + Context("when the file specified has valid json", func() { + BeforeEach(func() { + tempFile.Write([]byte(`[{"protocol":"udp","ports":"8080-9090","destination":"198.41.191.47/1"}]`)) + }) + + It("displays a message describing what its going to do", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating security group", "my-group", "my-user"}, + []string{"OK"}, + )) + }) + + It("creates the security group with those rules", func() { + _, rules := securityGroupRepo.CreateArgsForCall(0) + Expect(rules).To(Equal([]map[string]interface{}{ + {"protocol": "udp", "ports": "8080-9090", "destination": "198.41.191.47/1"}, + })) + }) + + Context("when the API returns an error", func() { + Context("some sort of awful terrible error that we were not prescient enough to anticipate", func() { + BeforeEach(func() { + securityGroupRepo.CreateReturns(errors.New("Wops I failed")) + }) + + It("fails loudly", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating security group", "my-group"}, + []string{"FAILED"}, + )) + }) + }) + + Context("when the group already exists", func() { + BeforeEach(func() { + securityGroupRepo.CreateReturns(errors.NewHttpError(400, "300005", "The security group is taken: my-group")) + }) + + It("warns the user when group already exists", func() { + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"already exists"})) + }) + }) + }) + }) + + Context("when the file specified has invalid json", func() { + BeforeEach(func() { + tempFile.Write([]byte(`[{noquote: thiswontwork}]`)) + }) + + It("freaks out", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Incorrect json format: file:", tempFile.Name()}, + []string{"Valid json file exampl"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/delete_security_group.go b/cf/commands/securitygroup/delete_security_group.go new file mode 100644 index 00000000000..dc147600be7 --- /dev/null +++ b/cf/commands/securitygroup/delete_security_group.go @@ -0,0 +1,80 @@ +package securitygroup + +import ( + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteSecurityGroup struct { + ui terminal.UI + securityGroupRepo security_groups.SecurityGroupRepo + configRepo core_config.Reader +} + +func NewDeleteSecurityGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo) DeleteSecurityGroup { + return DeleteSecurityGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + } +} + +func (cmd DeleteSecurityGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-security-group", + Description: T("Deletes a security group"), + Usage: T("CF_NAME delete-security-group SECURITY_GROUP [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd DeleteSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd DeleteSecurityGroup) Run(context *cli.Context) { + name := context.Args()[0] + cmd.ui.Say(T("Deleting security group {{.security_group}} as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + if !context.Bool("f") { + response := cmd.ui.ConfirmDelete(T("security group"), name) + if !response { + return + } + } + + group, err := cmd.securityGroupRepo.Read(name) + switch err.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Security group {{.security_group}} does not exist", map[string]interface{}{"security_group": name})) + return + default: + cmd.ui.Failed(err.Error()) + } + + err = cmd.securityGroupRepo.Delete(group.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/securitygroup/delete_security_group_test.go b/cf/commands/securitygroup/delete_security_group_test.go new file mode 100644 index 00000000000..ac9b4084dc0 --- /dev/null +++ b/cf/commands/securitygroup/delete_security_group_test.go @@ -0,0 +1,140 @@ +package securitygroup_test + +import ( + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-security-group command", func() { + var ( + ui *testterm.FakeUI + securityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + securityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteSecurityGroup(ui, configRepo, securityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("should fail if not logged in", func() { + Expect(runCommand("my-group")).To(BeFalse()) + }) + + It("should fail with usage when not provided a single argument", func() { + requirementsFactory.LoginSuccess = true + runCommand("whoops", "I can't believe", "I accidentally", "the whole thing") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when the group with the given name exists", func() { + BeforeEach(func() { + securityGroupRepo.ReadReturns(models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group", + Guid: "group-guid", + }, + }, nil) + }) + + Context("delete a security group", func() { + It("when passed the -f flag", func() { + runCommand("-f", "my-group") + Expect(securityGroupRepo.ReadArgsForCall(0)).To(Equal("my-group")) + Expect(securityGroupRepo.DeleteArgsForCall(0)).To(Equal("group-guid")) + + Expect(ui.Prompts).To(BeEmpty()) + }) + + It("should prompt user when -f flag is not present", func() { + ui.Inputs = []string{"y"} + + runCommand("my-group") + Expect(securityGroupRepo.ReadArgsForCall(0)).To(Equal("my-group")) + Expect(securityGroupRepo.DeleteArgsForCall(0)).To(Equal("group-guid")) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete the security group", "my-group"}, + )) + }) + + It("should not delete when user passes 'n' to prompt", func() { + ui.Inputs = []string{"n"} + + runCommand("my-group") + Expect(securityGroupRepo.ReadCallCount()).To(Equal(0)) + Expect(securityGroupRepo.DeleteCallCount()).To(Equal(0)) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete the security group", "my-group"}, + )) + }) + }) + + It("tells the user what it's about to do", func() { + runCommand("-f", "my-group") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "security group", "my-group", "my-user"}, + []string{"OK"}, + )) + }) + }) + + Context("when finding the group returns an error", func() { + BeforeEach(func() { + securityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.New("pbbbbbbbbbbt")) + }) + + It("fails and tells the user", func() { + runCommand("-f", "whoops") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when a group with that name does not exist", func() { + BeforeEach(func() { + securityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.NewModelNotFoundError("Security group", "uh uh uh -- you didn't sahy the magick word")) + }) + + It("fails and tells the user", func() { + runCommand("-f", "whoop") + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"whoop", "does not exist"})) + }) + }) + + It("fails and warns the user if deleting fails", func() { + securityGroupRepo.DeleteReturns(errors.New("raspberry")) + runCommand("-f", "whoops") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) +}) diff --git a/cf/commands/securitygroup/running_security_groups.go b/cf/commands/securitygroup/running_security_groups.go new file mode 100644 index 00000000000..ac273fa7696 --- /dev/null +++ b/cf/commands/securitygroup/running_security_groups.go @@ -0,0 +1,64 @@ +package securitygroup + +import ( + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type listRunningSecurityGroups struct { + ui terminal.UI + runningSecurityGroupRepo running.RunningSecurityGroupsRepo + configRepo core_config.Reader +} + +func NewListRunningSecurityGroups(ui terminal.UI, configRepo core_config.Reader, runningSecurityGroupRepo running.RunningSecurityGroupsRepo) listRunningSecurityGroups { + return listRunningSecurityGroups{ + ui: ui, + configRepo: configRepo, + runningSecurityGroupRepo: runningSecurityGroupRepo, + } +} + +func (cmd listRunningSecurityGroups) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "running-security-groups", + Description: T("List security groups in the set of security groups for running applications"), + Usage: "CF_NAME running-security-groups", + } +} + +func (cmd listRunningSecurityGroups) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 0 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd listRunningSecurityGroups) Run(context *cli.Context) { + cmd.ui.Say(T("Acquiring running security groups as '{{.username}}'", map[string]interface{}{ + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + defaultSecurityGroupsFields, err := cmd.runningSecurityGroupRepo.List() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(defaultSecurityGroupsFields) > 0 { + for _, value := range defaultSecurityGroupsFields { + cmd.ui.Say(value.Name) + } + } else { + cmd.ui.Say(T("No running security groups set")) + } +} diff --git a/cf/commands/securitygroup/running_security_groups_test.go b/cf/commands/securitygroup/running_security_groups_test.go new file mode 100644 index 00000000000..8cea9f978ea --- /dev/null +++ b/cf/commands/securitygroup/running_security_groups_test.go @@ -0,0 +1,93 @@ +package securitygroup_test + +import ( + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + + testapi "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running/fakes" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Running-security-groups command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + fakeRunningSecurityGroupRepo *testapi.FakeRunningSecurityGroupsRepo + cmd command.Command + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + fakeRunningSecurityGroupRepo = &testapi.FakeRunningSecurityGroupsRepo{} + cmd = NewListRunningSecurityGroups(ui, configRepo, fakeRunningSecurityGroupRepo) + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) testcmd.RunCommandResult { + return testcmd.RunCommandMoreBetter(cmd, requirementsFactory, args...) + } + + Describe("requirements", func() { + It("should fail when not logged in", func() { + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when there are some security groups set in the Running group", func() { + BeforeEach(func() { + fakeRunningSecurityGroupRepo.ListReturns([]models.SecurityGroupFields{ + {Name: "hiphopopotamus"}, + {Name: "my lyrics are bottomless"}, + {Name: "steve"}, + }, nil) + }) + + It("shows the user the name of the security groups of the Running set", func() { + Expect(runCommand()).To(HaveSucceeded()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Acquiring", "security groups", "my-user"}, + []string{"hiphopopotamus"}, + []string{"my lyrics are bottomless"}, + []string{"steve"}, + )) + }) + }) + + Context("when the API returns an error", func() { + BeforeEach(func() { + fakeRunningSecurityGroupRepo.ListReturns(nil, errors.New("uh oh")) + }) + + It("fails loudly", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when there are no security groups set in the Running group", func() { + It("tells the user that there are none", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No", "security groups", "set"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/security_group.go b/cf/commands/securitygroup/security_group.go new file mode 100644 index 00000000000..fa5439a5ec7 --- /dev/null +++ b/cf/commands/securitygroup/security_group.go @@ -0,0 +1,85 @@ +package securitygroup + +import ( + "encoding/json" + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ShowSecurityGroup struct { + ui terminal.UI + securityGroupRepo security_groups.SecurityGroupRepo + configRepo core_config.Reader +} + +func NewShowSecurityGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo) ShowSecurityGroup { + return ShowSecurityGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + } +} + +func (cmd ShowSecurityGroup) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "security-group", + Description: T("Show a single security group"), + Usage: T("CF_NAME security-group SECURITY_GROUP"), + } +} + +func (cmd ShowSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd ShowSecurityGroup) Run(context *cli.Context) { + name := context.Args()[0] + + cmd.ui.Say(T("Getting info for security group {{.security_group}} as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + securityGroup, err := cmd.securityGroupRepo.Read(name) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + jsonEncodedBytes, encodingErr := json.MarshalIndent(securityGroup.Rules, "\t", "\t") + if encodingErr != nil { + cmd.ui.Failed(encodingErr.Error()) + } + + cmd.ui.Ok() + table := terminal.NewTable(cmd.ui, []string{"", ""}) + table.Add(T("Name"), securityGroup.Name) + table.Add(T("Rules"), "") + table.Print() + cmd.ui.Say("\t" + string(jsonEncodedBytes)) + + cmd.ui.Say("") + + if len(securityGroup.Spaces) > 0 { + table = terminal.NewTable(cmd.ui, []string{"", T("Organization"), T("Space")}) + + for index, space := range securityGroup.Spaces { + table.Add(fmt.Sprintf("#%d", index), space.Organization.Name, space.Name) + } + table.Print() + } else { + cmd.ui.Say(T("No spaces assigned")) + } +} diff --git a/cf/commands/securitygroup/security_group_test.go b/cf/commands/securitygroup/security_group_test.go new file mode 100644 index 00000000000..62f100d6827 --- /dev/null +++ b/cf/commands/securitygroup/security_group_test.go @@ -0,0 +1,129 @@ +package securitygroup_test + +import ( + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("security-group command", func() { + var ( + ui *testterm.FakeUI + securityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + securityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewShowSecurityGroup(ui, configRepo, securityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("should fail if not logged in", func() { + Expect(runCommand("my-group")).To(BeFalse()) + }) + + It("should fail with usage when not provided a single argument", func() { + requirementsFactory.LoginSuccess = true + runCommand("whoops", "I can't believe", "I accidentally", "the whole thing") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when the group with the given name exists", func() { + BeforeEach(func() { + rulesMap := []map[string]interface{}{{"just-pretend": "that-this-is-correct"}} + securityGroup := models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group", + Guid: "group-guid", + Rules: rulesMap, + }, + Spaces: []models.Space{ + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid-1", Name: "space-1"}, + Organization: models.OrganizationFields{Guid: "my-org-guid-1", Name: "org-1"}, + }, + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid", Name: "space-2"}, + Organization: models.OrganizationFields{Guid: "my-org-guid-1", Name: "org-2"}, + }, + }, + } + + securityGroupRepo.ReadReturns(securityGroup, nil) + }) + + It("should fetch the security group from its repo", func() { + runCommand("my-group") + Expect(securityGroupRepo.ReadArgsForCall(0)).To(Equal("my-group")) + }) + + It("tells the user what it's about to do and then shows the group", func() { + runCommand("my-group") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting", "security group", "my-group", "my-user"}, + []string{"OK"}, + []string{"Name", "my-group"}, + []string{"Rules"}, + []string{"["}, + []string{"{"}, + []string{"just-pretend", "that-this-is-correct"}, + []string{"}"}, + []string{"]"}, + []string{"#0", "org-1", "space-1"}, + []string{"#1", "org-2", "space-2"}, + )) + }) + + It("tells the user if no spaces are assigned", func() { + securityGroup := models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group", + Guid: "group-guid", + Rules: []map[string]interface{}{}, + }, + Spaces: []models.Space{}, + } + + securityGroupRepo.ReadReturns(securityGroup, nil) + + runCommand("my-group") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No spaces assigned"}, + )) + }) + }) + + It("fails and warns the user if a group with that name could not be found", func() { + securityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.New("half-past-tea-time")) + runCommand("im-late!") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) +}) diff --git a/cf/commands/securitygroup/security_groups.go b/cf/commands/securitygroup/security_groups.go new file mode 100644 index 00000000000..d2a34701e43 --- /dev/null +++ b/cf/commands/securitygroup/security_groups.go @@ -0,0 +1,89 @@ +package securitygroup + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SecurityGroups struct { + ui terminal.UI + securityGroupRepo security_groups.SecurityGroupRepo + configRepo core_config.Reader +} + +func NewSecurityGroups(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo) SecurityGroups { + return SecurityGroups{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + } +} + +func (cmd SecurityGroups) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "security-groups", + Description: T("List all security groups"), + Usage: "CF_NAME security-groups", + } +} + +func (cmd SecurityGroups) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 0 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd SecurityGroups) Run(context *cli.Context) { + cmd.ui.Say(T("Getting security groups as {{.username}}", + map[string]interface{}{ + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + securityGroups, err := cmd.securityGroupRepo.FindAll() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(securityGroups) == 0 { + cmd.ui.Say(T("No security groups")) + return + } + + table := terminal.NewTable(cmd.ui, []string{"", T("Name"), T("Organization"), T("Space")}) + + for index, securityGroup := range securityGroups { + if len(securityGroup.Spaces) > 0 { + cmd.printSpaces(table, securityGroup, index) + } else { + table.Add(fmt.Sprintf("#%d", index), securityGroup.Name, "", "") + } + } + table.Print() +} + +func (cmd SecurityGroups) printSpaces(table terminal.Table, securityGroup models.SecurityGroup, index int) { + outputted_index := false + + for _, space := range securityGroup.Spaces { + if !outputted_index { + table.Add(fmt.Sprintf("#%d", index), securityGroup.Name, space.Organization.Name, space.Name) + outputted_index = true + } else { + table.Add("", securityGroup.Name, space.Organization.Name, space.Name) + } + } +} diff --git a/cf/commands/securitygroup/security_groups_test.go b/cf/commands/securitygroup/security_groups_test.go new file mode 100644 index 00000000000..8c157679600 --- /dev/null +++ b/cf/commands/securitygroup/security_groups_test.go @@ -0,0 +1,142 @@ +package securitygroup_test + +import ( + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("list-security-groups command", func() { + var ( + ui *testterm.FakeUI + repo *fakeSecurityGroup.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + repo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewSecurityGroups(ui, configRepo, repo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("should fail if not logged in", func() { + Expect(runCommand()).To(BeFalse()) + }) + + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand("why am I typing here") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("tells the user what it's about to do", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting", "security groups", "my-user"}, + )) + }) + + It("handles api errors with an error message", func() { + repo.FindAllReturns([]models.SecurityGroup{}, errors.New("YO YO YO, ERROR YO")) + + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + }) + + Context("when there are no security groups", func() { + It("Should tell the user that there are no security groups", func() { + repo.FindAllReturns([]models.SecurityGroup{}, nil) + + runCommand() + Expect(ui.Outputs).To(ContainSubstrings([]string{"No security groups"})) + }) + }) + + Context("when there is at least one security group", func() { + BeforeEach(func() { + securityGroup := models.SecurityGroup{} + securityGroup.Name = "my-group" + securityGroup.Guid = "group-guid" + + repo.FindAllReturns([]models.SecurityGroup{securityGroup}, nil) + }) + + Describe("Where there are spaces assigned", func() { + BeforeEach(func() { + securityGroups := []models.SecurityGroup{ + { + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group", + Guid: "group-guid", + }, + Spaces: []models.Space{ + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid-1", Name: "space-1"}, + Organization: models.OrganizationFields{Guid: "my-org-guid-1", Name: "org-1"}, + }, + { + SpaceFields: models.SpaceFields{Guid: "my-space-guid", Name: "space-2"}, + Organization: models.OrganizationFields{Guid: "my-org-guid-2", Name: "org-2"}, + }, + }, + }, + } + + repo.FindAllReturns(securityGroups, nil) + }) + + It("lists out the security group's: name, organization and space", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting", "security group", "my-user"}, + []string{"OK"}, + []string{"#0", "my-group", "org-1", "space-1"}, + )) + + //If there is a panic in this test, it is likely due to the following + //Expectation to be false + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"#0", "my-group", "org-2", "space-2"}, + )) + }) + }) + + Describe("Where there are no spaces assigned", func() { + It("lists out the security group's: name", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting", "security group", "my-user"}, + []string{"OK"}, + []string{"#0", "my-group"}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/staging_security_groups.go b/cf/commands/securitygroup/staging_security_groups.go new file mode 100644 index 00000000000..9431201a08d --- /dev/null +++ b/cf/commands/securitygroup/staging_security_groups.go @@ -0,0 +1,65 @@ +package securitygroup + +import ( + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type listStagingSecurityGroups struct { + ui terminal.UI + stagingSecurityGroupRepo staging.StagingSecurityGroupsRepo + configRepo core_config.Reader +} + +func NewListStagingSecurityGroups(ui terminal.UI, configRepo core_config.Reader, stagingSecurityGroupRepo staging.StagingSecurityGroupsRepo) listStagingSecurityGroups { + return listStagingSecurityGroups{ + ui: ui, + configRepo: configRepo, + stagingSecurityGroupRepo: stagingSecurityGroupRepo, + } +} + +func (cmd listStagingSecurityGroups) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "staging-security-groups", + Description: T("List security groups in the staging set for applications"), + Usage: "CF_NAME staging-security-groups", + } +} + +func (cmd listStagingSecurityGroups) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 0 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd listStagingSecurityGroups) Run(context *cli.Context) { + cmd.ui.Say(T("Acquiring staging security group as {{.username}}", + map[string]interface{}{ + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + SecurityGroupsFields, err := cmd.stagingSecurityGroupRepo.List() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(SecurityGroupsFields) > 0 { + for _, value := range SecurityGroupsFields { + cmd.ui.Say(value.Name) + } + } else { + cmd.ui.Say(T("No staging security group set")) + } +} diff --git a/cf/commands/securitygroup/staging_security_groups_test.go b/cf/commands/securitygroup/staging_security_groups_test.go new file mode 100644 index 00000000000..af8277fbcca --- /dev/null +++ b/cf/commands/securitygroup/staging_security_groups_test.go @@ -0,0 +1,93 @@ +package securitygroup_test + +import ( + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + + testapi "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging/fakes" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("staging-security-groups command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + fakeStagingSecurityGroupRepo *testapi.FakeStagingSecurityGroupsRepo + cmd command.Command + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + fakeStagingSecurityGroupRepo = &testapi.FakeStagingSecurityGroupsRepo{} + cmd = NewListStagingSecurityGroups(ui, configRepo, fakeStagingSecurityGroupRepo) + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) testcmd.RunCommandResult { + return testcmd.RunCommandMoreBetter(cmd, requirementsFactory, args...) + } + + Describe("requirements", func() { + It("should fail when not logged in", func() { + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when there are some security groups set for staging", func() { + BeforeEach(func() { + fakeStagingSecurityGroupRepo.ListReturns([]models.SecurityGroupFields{ + {Name: "hiphopopotamus"}, + {Name: "my lyrics are bottomless"}, + {Name: "steve"}, + }, nil) + }) + + It("shows the user the name of the security groups for staging", func() { + Expect(runCommand()).To(HaveSucceeded()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Acquiring", "staging security group", "my-user"}, + []string{"hiphopopotamus"}, + []string{"my lyrics are bottomless"}, + []string{"steve"}, + )) + }) + }) + + Context("when the API returns an error", func() { + BeforeEach(func() { + fakeStagingSecurityGroupRepo.ListReturns(nil, errors.New("uh oh")) + }) + + It("fails loudly", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when there are no security groups set for staging", func() { + It("tells the user that there are none", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No", "staging security group", "set"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/suite_test.go b/cf/commands/securitygroup/suite_test.go new file mode 100644 index 00000000000..b47502da777 --- /dev/null +++ b/cf/commands/securitygroup/suite_test.go @@ -0,0 +1,19 @@ +package securitygroup_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSecurityGroup(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "SecurityGroup Suite") +} diff --git a/cf/commands/securitygroup/unbind_running_security_group.go b/cf/commands/securitygroup/unbind_running_security_group.go new file mode 100644 index 00000000000..8d085b04c11 --- /dev/null +++ b/cf/commands/securitygroup/unbind_running_security_group.go @@ -0,0 +1,84 @@ +package securitygroup + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running" + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type unbindFromRunningGroup struct { + ui terminal.UI + configRepo core_config.Reader + securityGroupRepo security_groups.SecurityGroupRepo + runningGroupRepo running.RunningSecurityGroupsRepo +} + +func NewUnbindFromRunningGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo, runningGroupRepo running.RunningSecurityGroupsRepo) command.Command { + return &unbindFromRunningGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + runningGroupRepo: runningGroupRepo, + } +} + +func (cmd *unbindFromRunningGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME unbind-running-security-group SECURITY_GROUP") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "unbind-running-security-group", + Description: T("Unbind a security group from the set of security groups for running applications"), + Usage: strings.Join([]string{primaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd *unbindFromRunningGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *unbindFromRunningGroup) Run(context *cli.Context) { + name := context.Args()[0] + + securityGroup, err := cmd.securityGroupRepo.Read(name) + switch (err).(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Security group {{.security_group}} {{.error_message}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "error_message": terminal.WarningColor(T("does not exist.")), + })) + return + default: + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Say(T("Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(securityGroup.Name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + err = cmd.runningGroupRepo.UnbindFromRunningSet(securityGroup.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} diff --git a/cf/commands/securitygroup/unbind_running_security_group_test.go b/cf/commands/securitygroup/unbind_running_security_group_test.go new file mode 100644 index 00000000000..5355224414c --- /dev/null +++ b/cf/commands/securitygroup/unbind_running_security_group_test.go @@ -0,0 +1,99 @@ +package securitygroup_test + +import ( + fakeRunningDefaults "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/running/fakes" + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("unbind-running-security-group command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + fakeSecurityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + fakeRunningSecurityGroupsRepo *fakeRunningDefaults.FakeRunningSecurityGroupsRepo + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + fakeSecurityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + fakeRunningSecurityGroupsRepo = &fakeRunningDefaults.FakeRunningSecurityGroupsRepo{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUnbindFromRunningGroup(ui, configRepo, fakeSecurityGroupRepo, fakeRunningSecurityGroupsRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("name")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and provides the name of a group", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("security group exists", func() { + BeforeEach(func() { + group := models.SecurityGroup{} + group.Guid = "just-pretend-this-is-a-guid" + group.Name = "a-security-group-name" + fakeSecurityGroupRepo.ReadReturns(group, nil) + }) + + JustBeforeEach(func() { + runCommand("a-security-group-name") + }) + + It("unbinds the group from the running group set", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding", "security group", "a-security-group-name", "my-user"}, + []string{"TIP: Changes will not apply to existing running applications until they are restarted."}, + []string{"OK"}, + )) + + Expect(fakeSecurityGroupRepo.ReadArgsForCall(0)).To(Equal("a-security-group-name")) + Expect(fakeRunningSecurityGroupsRepo.UnbindFromRunningSetArgsForCall(0)).To(Equal("just-pretend-this-is-a-guid")) + }) + }) + + Context("when the security group does not exist", func() { + BeforeEach(func() { + fakeSecurityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.NewModelNotFoundError("security group", "anana-qui-parle")) + }) + + It("warns the user", func() { + runCommand("anana-qui-parle") + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"Security group", "anana-qui-parle", "does not exist"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"OK"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/unbind_security_group.go b/cf/commands/securitygroup/unbind_security_group.go new file mode 100644 index 00000000000..e364a0bdb5a --- /dev/null +++ b/cf/commands/securitygroup/unbind_security_group.go @@ -0,0 +1,115 @@ +package securitygroup + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/security_groups" + sgbinder "github.com/cloudfoundry/cli/cf/api/security_groups/spaces" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnbindSecurityGroup struct { + ui terminal.UI + configRepo core_config.Reader + securityGroupRepo security_groups.SecurityGroupRepo + orgRepo organizations.OrganizationRepository + spaceRepo spaces.SpaceRepository + secBinder sgbinder.SecurityGroupSpaceBinder +} + +func NewUnbindSecurityGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo, orgRepo organizations.OrganizationRepository, spaceRepo spaces.SpaceRepository, secBinder sgbinder.SecurityGroupSpaceBinder) UnbindSecurityGroup { + return UnbindSecurityGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + orgRepo: orgRepo, + spaceRepo: spaceRepo, + secBinder: secBinder, + } +} + +func (cmd UnbindSecurityGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "unbind-security-group", + Description: T("Unbind a security group from a space"), + Usage: strings.Join([]string{primaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd UnbindSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + argLength := len(context.Args()) + if argLength == 0 || argLength == 2 || argLength >= 4 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd UnbindSecurityGroup) Run(context *cli.Context) { + var spaceGuid string + secName := context.Args()[0] + + if len(context.Args()) == 1 { + spaceGuid = cmd.configRepo.SpaceFields().Guid + spaceName := cmd.configRepo.SpaceFields().Name + orgName := cmd.configRepo.OrganizationFields().Name + + cmd.flavorText(secName, orgName, spaceName) + } else { + orgName := context.Args()[1] + spaceName := context.Args()[2] + + cmd.flavorText(secName, orgName, spaceName) + + spaceGuid = cmd.lookupSpaceGuid(orgName, spaceName) + } + + securityGroup, err := cmd.securityGroupRepo.Read(secName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + secGuid := securityGroup.Guid + + err = cmd.secBinder.UnbindSpace(secGuid, spaceGuid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} + +func (cmd UnbindSecurityGroup) flavorText(secName string, orgName string, spaceName string) { + cmd.ui.Say(T("Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(secName), + "organization": terminal.EntityNameColor(orgName), + "space": terminal.EntityNameColor(spaceName), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) +} + +func (cmd UnbindSecurityGroup) lookupSpaceGuid(orgName string, spaceName string) string { + organization, err := cmd.orgRepo.FindByName(orgName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + orgGuid := organization.Guid + + space, err := cmd.spaceRepo.FindByNameInOrg(spaceName, orgGuid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + return space.Guid +} diff --git a/cf/commands/securitygroup/unbind_security_group_test.go b/cf/commands/securitygroup/unbind_security_group_test.go new file mode 100644 index 00000000000..fc720178845 --- /dev/null +++ b/cf/commands/securitygroup/unbind_security_group_test.go @@ -0,0 +1,136 @@ +package securitygroup_test + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + + fake_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + fakeBinder "github.com/cloudfoundry/cli/cf/api/security_groups/spaces/fakes" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("unbind-security-group command", func() { + var ( + ui *testterm.FakeUI + securityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + orgRepo *fake_org.FakeOrganizationRepository + spaceRepo *fakes.FakeSpaceRepository + secBinder *fakeBinder.FakeSecurityGroupSpaceBinder + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + securityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + orgRepo = &fake_org.FakeOrganizationRepository{} + spaceRepo = &fakes.FakeSpaceRepository{} + secBinder = &fakeBinder.FakeSecurityGroupSpaceBinder{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewUnbindSecurityGroup(ui, configRepo, securityGroupRepo, orgRepo, spaceRepo, secBinder) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("should fail if not logged in", func() { + Expect(runCommand("my-group")).To(BeFalse()) + }) + + It("should fail with usage when not provided with any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("should fail with usage when provided with a number of arguments that is either 2 or 4 or a number larger than 4", func() { + requirementsFactory.LoginSuccess = true + runCommand("I", "like") + Expect(ui.FailedWithUsage).To(BeTrue()) + runCommand("Turn", "down", "for", "what") + Expect(ui.FailedWithUsage).To(BeTrue()) + runCommand("My", "Very", "Excellent", "Mother", "Just", "Sat", "Under", "Nine", "ThingsThatArentPlanets") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when everything exists", func() { + BeforeEach(func() { + securityGroup := models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group", + Guid: "my-group-guid", + Rules: []map[string]interface{}{}, + }, + } + + securityGroupRepo.ReadReturns(securityGroup, nil) + + orgRepo.ListOrgsReturns([]models.Organization{{ + OrganizationFields: models.OrganizationFields{ + Name: "my-org", + Guid: "my-org-guid", + }}, + }, nil) + + spaceRepo.FindByNameInOrgSpace = models.Space{SpaceFields: models.SpaceFields{Name: "my-space", Guid: "my-space-guid"}} + }) + + It("removes the security group when we only pass the security group name (using the targeted org and space)", func() { + runCommand("my-group") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding security group", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + securityGroupGuid, spaceGuid := secBinder.UnbindSpaceArgsForCall(0) + Expect(securityGroupGuid).To(Equal("my-group-guid")) + Expect(spaceGuid).To(Equal("my-space-guid")) + }) + + It("removes the security group when we pass the org and space", func() { + runCommand("my-group", "my-org", "my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding security group", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + securityGroupGuid, spaceGuid := secBinder.UnbindSpaceArgsForCall(0) + Expect(securityGroupGuid).To(Equal("my-group-guid")) + Expect(spaceGuid).To(Equal("my-space-guid")) + }) + }) + + Context("when one of the things does not exist", func() { + BeforeEach(func() { + securityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.New("I accidentally the")) + }) + + It("fails with an error", func() { + runCommand("my-group", "my-org", "my-space") + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/unbind_staging_security_group.go b/cf/commands/securitygroup/unbind_staging_security_group.go new file mode 100644 index 00000000000..9eb1ddc9817 --- /dev/null +++ b/cf/commands/securitygroup/unbind_staging_security_group.go @@ -0,0 +1,86 @@ +package securitygroup + +import ( + "strings" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging" + "github.com/cloudfoundry/cli/cf/command" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type unbindFromStagingGroup struct { + ui terminal.UI + configRepo core_config.Reader + securityGroupRepo security_groups.SecurityGroupRepo + stagingGroupRepo staging.StagingSecurityGroupsRepo +} + +func NewUnbindFromStagingGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo, stagingGroupRepo staging.StagingSecurityGroupsRepo) command.Command { + return &unbindFromStagingGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + stagingGroupRepo: stagingGroupRepo, + } +} + +func (cmd *unbindFromStagingGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME unbind-staging-security-group SECURITY_GROUP") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "unbind-staging-security-group", + Description: T("Unbind a security group from the set of security groups for staging applications"), + Usage: strings.Join([]string{primaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd *unbindFromStagingGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd *unbindFromStagingGroup) Run(context *cli.Context) { + name := context.Args()[0] + + cmd.ui.Say(T("Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + + securityGroup, err := cmd.securityGroupRepo.Read(name) + switch (err).(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Security group {{.security_group}} {{.error_message}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "error_message": terminal.WarningColor(T("does not exist.")), + })) + return + default: + cmd.ui.Failed(err.Error()) + } + + err = cmd.stagingGroupRepo.UnbindFromStagingSet(securityGroup.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} diff --git a/cf/commands/securitygroup/unbind_staging_security_group_test.go b/cf/commands/securitygroup/unbind_staging_security_group_test.go new file mode 100644 index 00000000000..04bc522b2a2 --- /dev/null +++ b/cf/commands/securitygroup/unbind_staging_security_group_test.go @@ -0,0 +1,98 @@ +package securitygroup_test + +import ( + fakeStagingDefaults "github.com/cloudfoundry/cli/cf/api/security_groups/defaults/staging/fakes" + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("unbind-staging-security-group command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + fakeSecurityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + fakeStagingSecurityGroupsRepo *fakeStagingDefaults.FakeStagingSecurityGroupsRepo + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + fakeSecurityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + fakeStagingSecurityGroupsRepo = &fakeStagingDefaults.FakeStagingSecurityGroupsRepo{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUnbindFromStagingGroup(ui, configRepo, fakeSecurityGroupRepo, fakeStagingSecurityGroupsRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("name")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in and provides the name of a group", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("security group exists", func() { + BeforeEach(func() { + group := models.SecurityGroup{} + group.Guid = "just-pretend-this-is-a-guid" + group.Name = "a-security-group-name" + fakeSecurityGroupRepo.ReadReturns(group, nil) + }) + + JustBeforeEach(func() { + runCommand("a-security-group-name") + }) + + It("unbinds the group from the default staging group set", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding", "security group", "a-security-group-name", "my-user"}, + []string{"OK"}, + )) + + Expect(fakeSecurityGroupRepo.ReadArgsForCall(0)).To(Equal("a-security-group-name")) + Expect(fakeStagingSecurityGroupsRepo.UnbindFromStagingSetArgsForCall(0)).To(Equal("just-pretend-this-is-a-guid")) + }) + }) + + Context("when the security group does not exist", func() { + BeforeEach(func() { + fakeSecurityGroupRepo.ReadReturns(models.SecurityGroup{}, errors.NewModelNotFoundError("security group", "anana-qui-parle")) + }) + + It("warns the user", func() { + runCommand("anana-qui-parle") + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"Security group", "anana-qui-parle", "does not exist"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"OK"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/securitygroup/update_security_group.go b/cf/commands/securitygroup/update_security_group.go new file mode 100644 index 00000000000..95cdf4c5c59 --- /dev/null +++ b/cf/commands/securitygroup/update_security_group.go @@ -0,0 +1,76 @@ +package securitygroup + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "strings" + + "github.com/cloudfoundry/cli/cf/api/security_groups" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/json" + "github.com/codegangsta/cli" +) + +type UpdateSecurityGroup struct { + ui terminal.UI + securityGroupRepo security_groups.SecurityGroupRepo + configRepo core_config.Reader +} + +func NewUpdateSecurityGroup(ui terminal.UI, configRepo core_config.Reader, securityGroupRepo security_groups.SecurityGroupRepo) UpdateSecurityGroup { + return UpdateSecurityGroup{ + ui: ui, + configRepo: configRepo, + securityGroupRepo: securityGroupRepo, + } +} + +func (cmd UpdateSecurityGroup) Metadata() command_metadata.CommandMetadata { + primaryUsage := T("CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE") + secondaryUsage := T(" The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.") + tipUsage := T("TIP: Changes will not apply to existing running applications until they are restarted.") + return command_metadata.CommandMetadata{ + Name: "update-security-group", + Description: T("Update a security group"), + Usage: strings.Join([]string{primaryUsage, secondaryUsage, tipUsage}, "\n\n"), + } +} + +func (cmd UpdateSecurityGroup) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 2 { + cmd.ui.FailWithUsage(context) + } + + requirements := []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return requirements, nil +} + +func (cmd UpdateSecurityGroup) Run(context *cli.Context) { + name := context.Args()[0] + securityGroup, err := cmd.securityGroupRepo.Read(name) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + pathToJSONFile := context.Args()[1] + rules, err := json.ParseJSON(pathToJSONFile) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Say(T("Updating security group {{.security_group}} as {{.username}}", + map[string]interface{}{ + "security_group": terminal.EntityNameColor(name), + "username": terminal.EntityNameColor(cmd.configRepo.Username()), + })) + err = cmd.securityGroupRepo.Update(securityGroup.Guid, rules) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() + cmd.ui.Say("\n\n") + cmd.ui.Say(T("TIP: Changes will not apply to existing running applications until they are restarted.")) +} diff --git a/cf/commands/securitygroup/update_security_group_test.go b/cf/commands/securitygroup/update_security_group_test.go new file mode 100644 index 00000000000..e66be50e304 --- /dev/null +++ b/cf/commands/securitygroup/update_security_group_test.go @@ -0,0 +1,134 @@ +package securitygroup_test + +import ( + "io/ioutil" + "os" + + fakeSecurityGroup "github.com/cloudfoundry/cli/cf/api/security_groups/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/securitygroup" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("update-security-group command", func() { + var ( + ui *testterm.FakeUI + securityGroupRepo *fakeSecurityGroup.FakeSecurityGroupRepo + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + securityGroupRepo = &fakeSecurityGroup.FakeSecurityGroupRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + cmd := NewUpdateSecurityGroup(ui, configRepo, securityGroupRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + Expect(runCommand("the-security-group")).To(BeFalse()) + }) + + It("fails with usage when a name is not provided", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails with usage when a file path is not provided", func() { + requirementsFactory.LoginSuccess = true + runCommand("my-group-name") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in", func() { + var tempFile *os.File + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + securityGroup := models.SecurityGroup{ + SecurityGroupFields: models.SecurityGroupFields{ + Name: "my-group-name", + Guid: "my-group-guid", + }, + } + securityGroupRepo.ReadReturns(securityGroup, nil) + tempFile, _ = ioutil.TempFile("", "") + }) + + AfterEach(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + + JustBeforeEach(func() { + runCommand("my-group-name", tempFile.Name()) + }) + + Context("when the file specified has valid json", func() { + BeforeEach(func() { + tempFile.Write([]byte(`[{"protocol":"udp","port":"8080-9090","destination":"198.41.191.47/1"}]`)) + }) + + It("displays a message describing what its going to do", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating security group", "my-group-name", "my-user"}, + []string{"OK"}, + []string{"TIP: Changes will not apply to existing running applications until they are restarted."}, + )) + }) + + It("updates the security group with those rules, obviously", func() { + jsonData := []map[string]interface{}{ + {"protocol": "udp", "port": "8080-9090", "destination": "198.41.191.47/1"}, + } + + _, jsonArg := securityGroupRepo.UpdateArgsForCall(0) + + Expect(jsonArg).To(Equal(jsonData)) + }) + + Context("when the API returns an error", func() { + Context("some sort of awful terrible error that we were not prescient enough to anticipate", func() { + BeforeEach(func() { + securityGroupRepo.UpdateReturns(errors.New("Wops I failed")) + }) + + It("fails loudly", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating security group", "my-group-name"}, + []string{"FAILED"}, + )) + }) + }) + }) + + Context("when the file specified has invalid json", func() { + BeforeEach(func() { + tempFile.Write([]byte(`[{noquote: thiswontwork}]`)) + }) + + It("freaks out", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/service/bind_service.go b/cf/commands/service/bind_service.go new file mode 100644 index 00000000000..76104d0449a --- /dev/null +++ b/cf/commands/service/bind_service.go @@ -0,0 +1,97 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type BindService struct { + ui terminal.UI + config core_config.Reader + serviceBindingRepo api.ServiceBindingRepository + appReq requirements.ApplicationRequirement + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +type ServiceBinder interface { + BindApplication(app models.Application, serviceInstance models.ServiceInstance) (apiErr error) +} + +func NewBindService(ui terminal.UI, config core_config.Reader, serviceBindingRepo api.ServiceBindingRepository) (cmd *BindService) { + cmd = new(BindService) + cmd.ui = ui + cmd.config = config + cmd.serviceBindingRepo = serviceBindingRepo + return +} + +func (cmd *BindService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "bind-service", + ShortName: "bs", + Description: T("Bind a service instance to an app"), + Usage: T("CF_NAME bind-service APP SERVICE_INSTANCE"), + } +} + +func (cmd *BindService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + appName := c.Args()[0] + serviceName := c.Args()[1] + + cmd.appReq = requirementsFactory.NewApplicationRequirement(appName) + cmd.serviceInstanceReq = requirementsFactory.NewServiceInstanceRequirement(serviceName) + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq, cmd.serviceInstanceReq} + return +} + +func (cmd *BindService) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() + + cmd.ui.Say(T("Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceInstanceName": terminal.EntityNameColor(serviceInstance.Name), + "AppName": terminal.EntityNameColor(app.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + err := cmd.BindApplication(app, serviceInstance) + if err != nil { + if err, ok := err.(errors.HttpError); ok && err.ErrorCode() == errors.APP_ALREADY_BOUND { + cmd.ui.Ok() + cmd.ui.Warn(T("App {{.AppName}} is already bound to {{.ServiceName}}.", + map[string]interface{}{ + "AppName": app.Name, + "ServiceName": serviceInstance.Name, + })) + return + } else { + cmd.ui.Failed(err.Error()) + } + } + + cmd.ui.Ok() + cmd.ui.Say(T("TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + map[string]interface{}{"CFCommand": terminal.CommandColor(cf.Name() + " restage")})) +} + +func (cmd *BindService) BindApplication(app models.Application, serviceInstance models.ServiceInstance) (apiErr error) { + apiErr = cmd.serviceBindingRepo.Create(serviceInstance.Guid, app.Guid) + return +} diff --git a/cf/commands/service/bind_service_test.go b/cf/commands/service/bind_service_test.go new file mode 100644 index 00000000000..d55d9064c70 --- /dev/null +++ b/cf/commands/service/bind_service_test.go @@ -0,0 +1,104 @@ +package service_test + +import ( + "github.com/cloudfoundry/cli/cf/api" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("bind-service command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{} + }) + + It("fails requirements when not logged in", func() { + cmd := NewBindService(&testterm.FakeUI{}, testconfig.NewRepository(), &testapi.FakeServiceBindingRepo{}) + + Expect(testcmd.RunCommand(cmd, []string{"service", "app"}, requirementsFactory)).To(BeFalse()) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("binds a service instance to an app", func() { + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "my-service" + serviceInstance.Guid = "my-service-guid" + requirementsFactory.Application = app + requirementsFactory.ServiceInstance = serviceInstance + serviceBindingRepo := &testapi.FakeServiceBindingRepo{} + ui := callBindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(requirementsFactory.ServiceInstanceName).To(Equal("my-service")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Binding service", "my-service", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"TIP"}, + )) + Expect(serviceBindingRepo.CreateServiceInstanceGuid).To(Equal("my-service-guid")) + Expect(serviceBindingRepo.CreateApplicationGuid).To(Equal("my-app-guid")) + }) + + It("warns the user when the service instance is already bound to the given app", func() { + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "my-service" + serviceInstance.Guid = "my-service-guid" + requirementsFactory.Application = app + requirementsFactory.ServiceInstance = serviceInstance + serviceBindingRepo := &testapi.FakeServiceBindingRepo{CreateErrorCode: "90003"} + ui := callBindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Binding service"}, + []string{"OK"}, + []string{"my-app", "is already bound", "my-service"}, + )) + }) + + It("fails with usage when called without a service instance and app", func() { + serviceBindingRepo := &testapi.FakeServiceBindingRepo{} + + ui := callBindService([]string{"my-service"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeTrue()) + + ui = callBindService([]string{"my-app"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeTrue()) + + ui = callBindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + }) +}) + +func callBindService(args []string, requirementsFactory *testreq.FakeReqFactory, serviceBindingRepo api.ServiceBindingRepository) (fakeUI *testterm.FakeUI) { + fakeUI = new(testterm.FakeUI) + + config := testconfig.NewRepositoryWithDefaults() + + cmd := NewBindService(fakeUI, config, serviceBindingRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} diff --git a/cf/commands/service/create_service.go b/cf/commands/service/create_service.go new file mode 100644 index 00000000000..35b6eda4fc7 --- /dev/null +++ b/cf/commands/service/create_service.go @@ -0,0 +1,123 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateService struct { + ui terminal.UI + config core_config.Reader + serviceRepo api.ServiceRepository + serviceBuilder service_builder.ServiceBuilder +} + +func NewCreateService(ui terminal.UI, config core_config.Reader, serviceRepo api.ServiceRepository, serviceBuilder service_builder.ServiceBuilder) (cmd CreateService) { + cmd.ui = ui + cmd.config = config + cmd.serviceRepo = serviceRepo + cmd.serviceBuilder = serviceBuilder + return +} + +func (cmd CreateService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-service", + ShortName: "cs", + Description: T("Create a service instance"), + Usage: T(`CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE + +EXAMPLE: + CF_NAME create-service cleardb spark clear-db-mine + +TIP: + Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps`), + } +} + +func (cmd CreateService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + } + + return +} + +func (cmd CreateService) Run(c *cli.Context) { + serviceName := c.Args()[0] + planName := c.Args()[1] + serviceInstanceName := c.Args()[2] + + cmd.ui.Say(T("Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceInstanceName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + plan, err := cmd.CreateService(serviceName, planName, serviceInstanceName) + + switch err.(type) { + case nil: + cmd.ui.Ok() + if !plan.Free { + cmd.ui.Say("") + cmd.ui.Say(T("Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + map[string]interface{}{ + "PlanName": terminal.EntityNameColor(plan.Name), + "ServiceName": terminal.EntityNameColor(serviceName), + "ServiceInstanceName": terminal.EntityNameColor(serviceInstanceName), + })) + cmd.ui.Say("") + } + case *errors.ModelAlreadyExistsError: + cmd.ui.Ok() + cmd.ui.Warn(err.Error()) + default: + cmd.ui.Failed(err.Error()) + } +} + +func (cmd CreateService) CreateService(serviceName string, planName string, serviceInstanceName string) (models.ServicePlanFields, error) { + offerings, apiErr := cmd.serviceBuilder.GetServicesByNameForSpaceWithPlans(cmd.config.SpaceFields().Guid, serviceName) + if apiErr != nil { + return models.ServicePlanFields{}, apiErr + } + + plan, apiErr := findPlanFromOfferings(offerings, planName) + if apiErr != nil { + return plan, apiErr + } + + apiErr = cmd.serviceRepo.CreateServiceInstance(serviceInstanceName, plan.Guid) + return plan, apiErr +} + +func findPlanFromOfferings(offerings models.ServiceOfferings, name string) (plan models.ServicePlanFields, err error) { + for _, offering := range offerings { + for _, plan := range offering.Plans { + if name == plan.Name { + return plan, nil + } + } + } + + err = errors.New(T("Could not find plan with name {{.ServicePlanName}}", + map[string]interface{}{"ServicePlanName": name}, + )) + return +} diff --git a/cf/commands/service/create_service_test.go b/cf/commands/service/create_service_test.go new file mode 100644 index 00000000000..22010aabf19 --- /dev/null +++ b/cf/commands/service/create_service_test.go @@ -0,0 +1,142 @@ +package service_test + +import ( + "github.com/cloudfoundry/cli/cf/actors/service_builder/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("create-service command", func() { + var ( + ui *testterm.FakeUI + config core_config.Repository + requirementsFactory *testreq.FakeReqFactory + cmd CreateService + serviceRepo *testapi.FakeServiceRepo + serviceBuilder *fakes.FakeServiceBuilder + + offering1 models.ServiceOffering + offering2 models.ServiceOffering + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + serviceRepo = &testapi.FakeServiceRepo{} + serviceBuilder = &fakes.FakeServiceBuilder{} + cmd = NewCreateService(ui, config, serviceRepo, serviceBuilder) + + offering1 = models.ServiceOffering{} + offering1.Label = "cleardb" + offering1.Plans = []models.ServicePlanFields{{ + Name: "spark", + Guid: "cleardb-spark-guid", + Free: true, + }, { + Name: "expensive", + Guid: "luxury-guid", + Free: false, + }} + + offering2 = models.ServiceOffering{} + offering2.Label = "postgres" + + serviceBuilder.GetServicesByNameForSpaceWithPlansReturns(models.ServiceOfferings{offering1, offering2}, nil) + }) + + var callCreateService = func(args []string) bool { + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("passes when logged in and a space is targeted", func() { + Expect(callCreateService([]string{"cleardb", "spark", "my-cleardb-service"})).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(callCreateService([]string{"cleardb", "spark", "my-cleardb-service"})).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + Expect(callCreateService([]string{"cleardb", "spark", "my-cleardb-service"})).To(BeFalse()) + }) + }) + + It("successfully creates a service", func() { + callCreateService([]string{"cleardb", "spark", "my-cleardb-service"}) + + spaceGuid, serviceName := serviceBuilder.GetServicesByNameForSpaceWithPlansArgsForCall(0) + Expect(spaceGuid).To(Equal(config.SpaceFields().Guid)) + Expect(serviceName).To(Equal("cleardb")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service", "my-cleardb-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + Expect(serviceRepo.CreateServiceInstanceArgs.Name).To(Equal("my-cleardb-service")) + Expect(serviceRepo.CreateServiceInstanceArgs.PlanGuid).To(Equal("cleardb-spark-guid")) + }) + + Describe("warning the user about paid services", func() { + It("does not warn the user when the service is free", func() { + callCreateService([]string{"cleardb", "spark", "my-free-cleardb-service"}) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service", "my-free-cleardb-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + Expect(ui.Outputs).NotTo(ContainSubstrings([]string{"will incurr a cost"})) + }) + + It("warns the user when the service is not free", func() { + callCreateService([]string{"cleardb", "expensive", "my-expensive-cleardb-service"}) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service", "my-expensive-cleardb-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Attention: The plan `expensive` of service `cleardb` is not free. The instance `my-expensive-cleardb-service` will incur a cost. Contact your administrator if you think this is in error."}, + )) + }) + }) + + It("warns the user when the service already exists with the same service plan", func() { + serviceRepo.CreateServiceInstanceReturns.Error = errors.NewModelAlreadyExistsError("Service", "my-cleardb-service") + + callCreateService([]string{"cleardb", "spark", "my-cleardb-service"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service", "my-cleardb-service"}, + []string{"OK"}, + []string{"my-cleardb-service", "already exists"}, + )) + Expect(serviceRepo.CreateServiceInstanceArgs.Name).To(Equal("my-cleardb-service")) + Expect(serviceRepo.CreateServiceInstanceArgs.PlanGuid).To(Equal("cleardb-spark-guid")) + }) + + Context("When there are multiple services with the same label", func() { + It("finds the plan even if it has to search multiple services", func() { + offering2.Label = "cleardb" + + serviceRepo.CreateServiceInstanceReturns.Error = errors.NewModelAlreadyExistsError("Service", "my-cleardb-service") + callCreateService([]string{"cleardb", "spark", "my-cleardb-service"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service", "my-cleardb-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + Expect(serviceRepo.CreateServiceInstanceArgs.Name).To(Equal("my-cleardb-service")) + Expect(serviceRepo.CreateServiceInstanceArgs.PlanGuid).To(Equal("cleardb-spark-guid")) + }) + }) +}) diff --git a/cf/commands/service/create_user_provided_service.go b/cf/commands/service/create_user_provided_service.go new file mode 100644 index 00000000000..8633f45b7c7 --- /dev/null +++ b/cf/commands/service/create_user_provided_service.go @@ -0,0 +1,100 @@ +package service + +import ( + "encoding/json" + . "github.com/cloudfoundry/cli/cf/i18n" + "strings" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateUserProvidedService struct { + ui terminal.UI + config core_config.Reader + userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository +} + +func NewCreateUserProvidedService(ui terminal.UI, config core_config.Reader, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (cmd CreateUserProvidedService) { + cmd.ui = ui + cmd.config = config + cmd.userProvidedServiceInstanceRepo = userProvidedServiceInstanceRepo + return +} + +func (cmd CreateUserProvidedService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-user-provided-service", + ShortName: "cups", + Description: T("Make a user-provided service instance available to cf apps"), + Usage: T(`CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL] + + Pass comma separated credential parameter names to enable interactive mode: + CF_NAME create-user-provided-service SERVICE_INSTANCE -p "comma, separated, parameter, names" + + Pass credential parameters as JSON to create a service non-interactively: + CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{"name":"value","name":"value"}' + +EXAMPLE: + CF_NAME create-user-provided-service oracle-db-mine -p "username, password" + CF_NAME create-user-provided-service oracle-db-mine -p '{"username":"admin","password":"pa55woRD"}' + CF_NAME create-user-provided-service my-drain-service -l syslog://example.com +`), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Credentials")), + flag_helpers.NewStringFlag("l", T("Syslog Drain Url")), + }, + } +} + +func (cmd CreateUserProvidedService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd CreateUserProvidedService) Run(c *cli.Context) { + name := c.Args()[0] + drainUrl := c.String("l") + + params := c.String("p") + params = strings.Trim(params, `"`) + paramsMap := make(map[string]interface{}) + + err := json.Unmarshal([]byte(params), ¶msMap) + if err != nil && params != "" { + paramsMap = cmd.mapValuesFromPrompt(params, paramsMap) + } + + cmd.ui.Say(T("Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr := cmd.userProvidedServiceInstanceRepo.Create(name, drainUrl, paramsMap) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} + +func (cmd CreateUserProvidedService) mapValuesFromPrompt(params string, paramsMap map[string]interface{}) map[string]interface{} { + for _, param := range strings.Split(params, ",") { + param = strings.Trim(param, " ") + paramsMap[param] = cmd.ui.Ask("%s", param) + } + return paramsMap +} diff --git a/cf/commands/service/create_user_provided_service_test.go b/cf/commands/service/create_user_provided_service_test.go new file mode 100644 index 00000000000..8c3130fa2e6 --- /dev/null +++ b/cf/commands/service/create_user_provided_service_test.go @@ -0,0 +1,100 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("create-user-provided-service command", func() { + var ( + ui *testterm.FakeUI + config core_config.ReadWriter + repo *testapi.FakeUserProvidedServiceInstanceRepo + requirementsFactory *testreq.FakeReqFactory + cmd CreateUserProvidedService + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + repo = &testapi.FakeUserProvidedServiceInstanceRepo{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + cmd = NewCreateUserProvidedService(ui, config, repo) + }) + + Describe("login requirements", func() { + It("fails if the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(testcmd.RunCommand(cmd, []string{"my-service"}, requirementsFactory)).To(BeFalse()) + }) + }) + + It("creates a new user provided service given just a name", func() { + testcmd.RunCommand(cmd, []string{"my-custom-service"}, requirementsFactory) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating user provided service"}, + []string{"OK"}, + )) + }) + + It("accepts service parameters interactively", func() { + ui.Inputs = []string{"foo value", "bar value", "baz value"} + testcmd.RunCommand(cmd, []string{"-p", `"foo, bar, baz"`, "my-custom-service"}, requirementsFactory) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"foo"}, + []string{"bar"}, + []string{"baz"}, + )) + + Expect(repo.CreateName).To(Equal("my-custom-service")) + Expect(repo.CreateParams).To(Equal(map[string]interface{}{ + "foo": "foo value", + "bar": "bar value", + "baz": "baz value", + })) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating user provided service", "my-custom-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + }) + + It("accepts service parameters as JSON without prompting", func() { + args := []string{"-p", `{"foo": "foo value", "bar": "bar value", "baz": 4}`, "my-custom-service"} + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Prompts).To(BeEmpty()) + Expect(repo.CreateName).To(Equal("my-custom-service")) + Expect(repo.CreateParams).To(Equal(map[string]interface{}{ + "foo": "foo value", + "bar": "bar value", + "baz": float64(4), + })) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating user provided service"}, + []string{"OK"}, + )) + }) + + It("creates a user provided service with a syslog drain url", func() { + args := []string{"-l", "syslog://example.com", "-p", `{"foo": "foo value", "bar": "bar value", "baz": "baz value"}`, "my-custom-service"} + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(repo.CreateDrainUrl).To(Equal("syslog://example.com")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating user provided service"}, + []string{"OK"}, + )) + }) +}) diff --git a/cf/commands/service/delete_service.go b/cf/commands/service/delete_service.go new file mode 100644 index 00000000000..a8b05cdc18a --- /dev/null +++ b/cf/commands/service/delete_service.go @@ -0,0 +1,86 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteService struct { + ui terminal.UI + config core_config.Reader + serviceRepo api.ServiceRepository + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +func NewDeleteService(ui terminal.UI, config core_config.Reader, serviceRepo api.ServiceRepository) (cmd *DeleteService) { + cmd = new(DeleteService) + cmd.ui = ui + cmd.config = config + cmd.serviceRepo = serviceRepo + return +} + +func (cmd *DeleteService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-service", + ShortName: "ds", + Description: T("Delete a service instance"), + Usage: T("CF_NAME delete-service SERVICE_INSTANCE [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func (cmd *DeleteService) Run(c *cli.Context) { + serviceName := c.Args()[0] + + if !c.Bool("f") { + if !cmd.ui.ConfirmDelete(T("service"), serviceName) { + return + } + } + + cmd.ui.Say(T("Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + instance, apiErr := cmd.serviceRepo.FindInstanceByName(serviceName) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Service {{.ServiceName}} does not exist.", map[string]interface{}{"ServiceName": serviceName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + apiErr = cmd.serviceRepo.DeleteService(instance) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/service/delete_service_test.go b/cf/commands/service/delete_service_test.go new file mode 100644 index 00000000000..9a311d50000 --- /dev/null +++ b/cf/commands/service/delete_service_test.go @@ -0,0 +1,113 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-service command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + serviceRepo *testapi.FakeServiceRepo + serviceInstance models.ServiceInstance + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{ + Inputs: []string{"yes"}, + } + + serviceRepo = &testapi.FakeServiceRepo{} + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + } + }) + + runCommand := func(args ...string) bool { + configRepo := testconfig.NewRepositoryWithDefaults() + cmd := NewDeleteService(ui, configRepo, serviceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("does not pass requirements", func() { + Expect(runCommand("vestigial-service")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails with usage when not provided exactly one arg", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("when the service exists", func() { + BeforeEach(func() { + serviceInstance = models.ServiceInstance{} + serviceInstance.Name = "my-service" + serviceInstance.Guid = "my-service-guid" + serviceRepo.FindInstanceByNameServiceInstance = serviceInstance + }) + + Context("when the command is confirmed", func() { + It("deletes the service", func() { + runCommand("my-service") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the service my-service"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service", "my-service", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(serviceRepo.DeleteServiceServiceInstance).To(Equal(serviceInstance)) + }) + }) + + It("skips confirmation when the -f flag is given", func() { + runCommand("-f", "foo.com") + + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service", "foo.com"}, + []string{"OK"}, + )) + }) + }) + + Context("when the service does not exist", func() { + BeforeEach(func() { + serviceRepo.FindInstanceByNameNotFound = true + }) + + It("warns the user the service does not exist", func() { + runCommand("-f", "my-service") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service", "my-service"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"my-service", "does not exist"})) + }) + }) + }) +}) diff --git a/cf/commands/service/marketplace.go b/cf/commands/service/marketplace.go new file mode 100644 index 00000000000..524464d5305 --- /dev/null +++ b/cf/commands/service/marketplace.go @@ -0,0 +1,174 @@ +package service + +import ( + "sort" + "strings" + + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/actors/service_builder" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type MarketplaceServices struct { + ui terminal.UI + config core_config.Reader + serviceBuilder service_builder.ServiceBuilder +} + +func NewMarketplaceServices(ui terminal.UI, config core_config.Reader, serviceBuilder service_builder.ServiceBuilder) MarketplaceServices { + return MarketplaceServices{ + ui: ui, + config: config, + serviceBuilder: serviceBuilder, + } +} + +func (cmd MarketplaceServices) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "marketplace", + ShortName: "m", + Description: T("List available offerings in the marketplace"), + Usage: "CF_NAME marketplace", + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("s", T("Show plan details for a particular service offering")), + }, + } +} + +func (cmd MarketplaceServices) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = append(reqs, requirementsFactory.NewApiEndpointRequirement()) + return +} + +func (cmd MarketplaceServices) Run(c *cli.Context) { + serviceName := c.String("s") + + if serviceName != "" { + cmd.marketplaceByService(serviceName) + } else { + cmd.marketplace() + } +} + +func (cmd MarketplaceServices) marketplaceByService(serviceName string) { + var ( + serviceOffering models.ServiceOffering + apiErr error + ) + + if cmd.config.HasSpace() { + cmd.ui.Say(T("Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceName), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + serviceOffering, apiErr = cmd.serviceBuilder.GetServiceByNameForSpaceWithPlans(serviceName, cmd.config.SpaceFields().Guid) + } else if !cmd.config.IsLoggedIn() { + cmd.ui.Say(T("Getting service plan information for service {{.ServiceName}}...", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName)})) + serviceOffering, apiErr = cmd.serviceBuilder.GetServiceByNameWithPlans(serviceName) + } else { + cmd.ui.Failed(T("Cannot list plan information for {{.ServiceName}} without a targeted space", + map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName)})) + } + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if serviceOffering.Guid == "" { + cmd.ui.Say(T("Service offering not found")) + return + } + + table := terminal.NewTable(cmd.ui, []string{T("service plan"), T("description"), T("free or paid")}) + for _, plan := range serviceOffering.Plans { + var freeOrPaid string + if plan.Free { + freeOrPaid = "free" + } else { + freeOrPaid = "paid" + } + table.Add(plan.Name, plan.Description, freeOrPaid) + } + + table.Print() +} + +func (cmd MarketplaceServices) marketplace() { + var ( + serviceOfferings models.ServiceOfferings + apiErr error + ) + + if cmd.config.HasSpace() { + cmd.ui.Say(T("Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + serviceOfferings, apiErr = cmd.serviceBuilder.GetServicesForSpaceWithPlans(cmd.config.SpaceFields().Guid) + } else if !cmd.config.IsLoggedIn() { + cmd.ui.Say(T("Getting all services from marketplace...")) + serviceOfferings, apiErr = cmd.serviceBuilder.GetAllServicesWithPlans() + } else { + cmd.ui.Failed(T("Cannot list marketplace services without a targeted space")) + } + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(serviceOfferings) == 0 { + cmd.ui.Say(T("No service offerings found")) + return + } + + table := terminal.NewTable(cmd.ui, []string{T("service"), T("plans"), T("description")}) + + sort.Sort(serviceOfferings) + var paidPlanExists bool + for _, offering := range serviceOfferings { + planNames := "" + + for _, plan := range offering.Plans { + if plan.Name == "" { + continue + } + if plan.Free { + planNames += ", " + plan.Name + } else { + paidPlanExists = true + planNames += ", " + plan.Name + "*" + } + } + + planNames = strings.TrimPrefix(planNames, ", ") + + table.Add(offering.Label, planNames, offering.Description) + } + + table.Print() + if paidPlanExists { + cmd.ui.Say(T("\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.")) + } + cmd.ui.Say(T("\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.")) +} diff --git a/cf/commands/service/marketplace_test.go b/cf/commands/service/marketplace_test.go new file mode 100644 index 00000000000..128f1688cd1 --- /dev/null +++ b/cf/commands/service/marketplace_test.go @@ -0,0 +1,236 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/actors/service_builder/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("marketplace command", func() { + var ui *testterm.FakeUI + var requirementsFactory *testreq.FakeReqFactory + var config core_config.ReadWriter + var serviceBuilder *testapi.FakeServiceBuilder + var fakeServiceOfferings []models.ServiceOffering + var serviceWithAPaidPlan models.ServiceOffering + var service2 models.ServiceOffering + + BeforeEach(func() { + serviceBuilder = &testapi.FakeServiceBuilder{} + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{ApiEndpointSuccess: true} + + serviceWithAPaidPlan = models.ServiceOffering{ + Plans: []models.ServicePlanFields{ + models.ServicePlanFields{Name: "service-plan-a", Description: "service-plan-a description", Free: true}, + models.ServicePlanFields{Name: "service-plan-b", Description: "service-plan-b description", Free: false}, + }, + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "zzz-my-service-offering", + Guid: "service-1-guid", + Description: "service offering 1 description", + }} + service2 = models.ServiceOffering{ + Plans: []models.ServicePlanFields{ + models.ServicePlanFields{Name: "service-plan-c", Free: true}, + models.ServicePlanFields{Name: "service-plan-d", Free: true}}, + ServiceOfferingFields: models.ServiceOfferingFields{ + Label: "aaa-my-service-offering", + Description: "service offering 2 description", + }, + } + fakeServiceOfferings = []models.ServiceOffering{serviceWithAPaidPlan, service2} + }) + + Describe("Requirements", func() { + Context("when the an API endpoint is not targeted", func() { + It("does not meet its requirements", func() { + config := testconfig.NewRepository() + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + requirementsFactory.ApiEndpointSuccess = false + + Expect(testcmd.RunCommand(cmd, []string{}, requirementsFactory)).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + config := testconfig.NewRepository() + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + requirementsFactory.ApiEndpointSuccess = true + Expect(testcmd.RunCommand(cmd, []string{"blahblah"}, requirementsFactory)).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + }) + + Context("when the user has a space targeted", func() { + BeforeEach(func() { + config.SetSpaceFields(models.SpaceFields{ + Guid: "the-space-guid", + Name: "the-space-name", + }) + serviceBuilder.GetServicesForSpaceWithPlansReturns(fakeServiceOfferings, nil) + }) + + It("lists all of the service offerings for the space", func() { + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + args := serviceBuilder.GetServicesForSpaceWithPlansArgsForCall(0) + Expect(args).To(Equal("the-space-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting services from marketplace in org", "my-org", "the-space-name", "my-user"}, + []string{"OK"}, + []string{"service", "plans", "description"}, + []string{"aaa-my-service-offering", "service offering 2 description", "service-plan-c,", "service-plan-d"}, + []string{"zzz-my-service-offering", "service offering 1 description", "service-plan-a,", "service-plan-b*"}, + []string{"* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred."}, + []string{"TIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service."}, + )) + }) + + Context("when there are no paid plans", func() { + BeforeEach(func() { + serviceBuilder.GetServicesForSpaceWithPlansReturns([]models.ServiceOffering{service2}, nil) + }) + + It("lists the service offerings without displaying the paid message", func() { + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting services from marketplace in org", "my-org", "the-space-name", "my-user"}, + []string{"OK"}, + []string{"service", "plans", "description"}, + []string{"aaa-my-service-offering", "service offering 2 description", "service-plan-c", "service-plan-d"}, + )) + Expect(ui.Outputs).NotTo(ContainSubstrings( + []string{"* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred."}, + )) + }) + + }) + + Context("when the user passes the -s flag", func() { + It("Displays the list of plans for each service with info", func() { + serviceBuilder.GetServiceByNameForSpaceWithPlansReturns(serviceWithAPaidPlan, nil) + + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{"-s", "aaa-my-service-offering"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service plan information for service aaa-my-service-offering as my-user..."}, + []string{"OK"}, + []string{"service plan", "description", "free or paid"}, + []string{"service-plan-a", "service-plan-a description", "free"}, + []string{"service-plan-b", "service-plan-b description", "paid"}, + )) + }) + + It("informs the user if the service cannot be found", func() { + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{"-s", "aaa-my-service-offering"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Service offering not found"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"service plan", "description", "free or paid"}, + )) + }) + }) + }) + + Context("when the user doesn't have a space targeted", func() { + BeforeEach(func() { + config.SetSpaceFields(models.SpaceFields{}) + }) + + It("tells the user to target a space", func() { + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"without", "space"}, + )) + }) + }) + }) + + Context("when user is not logged in", func() { + BeforeEach(func() { + config = testconfig.NewRepository() + }) + + It("lists all public service offerings if any are available", func() { + serviceBuilder := &testapi.FakeServiceBuilder{} + serviceBuilder.GetAllServicesWithPlansReturns(fakeServiceOfferings, nil) + + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting all services from marketplace"}, + []string{"OK"}, + []string{"service", "plans", "description"}, + []string{"aaa-my-service-offering", "service offering 2 description", "service-plan-c", "service-plan-d"}, + []string{"zzz-my-service-offering", "service offering 1 description", "service-plan-a", "service-plan-b"}, + )) + }) + + It("does not display a table if no service offerings exist", func() { + serviceBuilder := &testapi.FakeServiceBuilder{} + serviceBuilder.GetAllServicesWithPlansReturns([]models.ServiceOffering{}, nil) + + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No service offerings found"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"service", "plans", "description"}, + )) + }) + + Context("when the user passes the -s flag", func() { + It("Displays the list of plans for each service with info", func() { + serviceBuilder.GetServiceByNameWithPlansReturns(serviceWithAPaidPlan, nil) + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{"-s", "aaa-my-service-offering"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service plan information for service aaa-my-service-offering"}, + []string{"OK"}, + []string{"service plan", "description", "free or paid"}, + []string{"service-plan-a", "service-plan-a description", "free"}, + []string{"service-plan-b", "service-plan-b description", "paid"}, + )) + }) + + It("informs the user if the service cannot be found", func() { + cmd := NewMarketplaceServices(ui, config, serviceBuilder) + testcmd.RunCommand(cmd, []string{"-s", "aaa-my-service-offering"}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Service offering not found"}, + )) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"service plan", "description", "free or paid"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/service/migrate_service_instances.go b/cf/commands/service/migrate_service_instances.go new file mode 100644 index 00000000000..25901e4d284 --- /dev/null +++ b/cf/commands/service/migrate_service_instances.go @@ -0,0 +1,146 @@ +package service + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/resources" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type MigrateServiceInstances struct { + ui terminal.UI + configRepo core_config.Reader + serviceRepo api.ServiceRepository +} + +func NewMigrateServiceInstances(ui terminal.UI, configRepo core_config.Reader, serviceRepo api.ServiceRepository) (cmd *MigrateServiceInstances) { + cmd = new(MigrateServiceInstances) + cmd.ui = ui + cmd.configRepo = configRepo + cmd.serviceRepo = serviceRepo + return +} + +func migrateServiceInstanceWarning() string { + return T("WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.") +} + +func (cmd *MigrateServiceInstances) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "migrate-service-instances", + Description: T("Migrate service instances from one service plan to another"), + Usage: T("CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n") + migrateServiceInstanceWarning(), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force migration without confirmation")}, + }, + } +} + +func (cmd *MigrateServiceInstances) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 5 { + cmd.ui.FailWithUsage(c) + return + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *MigrateServiceInstances) Run(c *cli.Context) { + v1 := resources.ServicePlanDescription{ + ServiceLabel: c.Args()[0], + ServiceProvider: c.Args()[1], + ServicePlanName: c.Args()[2], + } + v2 := resources.ServicePlanDescription{ + ServiceLabel: c.Args()[3], + ServicePlanName: c.Args()[4], + } + force := c.Bool("f") + + v1Guid, apiErr := cmd.serviceRepo.FindServicePlanByDescription(v1) + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Failed(T("Plan {{.ServicePlanName}} cannot be found", + map[string]interface{}{ + "ServicePlanName": terminal.EntityNameColor(v1.String()), + })) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + v2Guid, apiErr := cmd.serviceRepo.FindServicePlanByDescription(v2) + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Failed(T("Plan {{.ServicePlanName}} cannot be found", + map[string]interface{}{ + "ServicePlanName": terminal.EntityNameColor(v2.String()), + })) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + count, apiErr := cmd.serviceRepo.GetServiceInstanceCountForServicePlan(v1Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } else if count == 0 { + cmd.ui.Failed(T("Plan {{.ServicePlanName}} has no service instances to migrate", map[string]interface{}{"ServicePlanName": terminal.EntityNameColor(v1.String())})) + return + } + + cmd.ui.Warn(migrateServiceInstanceWarning()) + + serviceInstancesPhrase := pluralizeServiceInstances(count) + + if !force { + response := cmd.ui.Confirm( + T("Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?>", + map[string]interface{}{ + "ServiceInstanceDescription": serviceInstancesPhrase, + "OldServicePlanName": terminal.EntityNameColor(v1.String()), + "NewServicePlanName": terminal.EntityNameColor(v2.String()), + })) + if !response { + return + } + } + + cmd.ui.Say(T("Attempting to migrate {{.ServiceInstanceDescription}}...", map[string]interface{}{"ServiceInstanceDescription": serviceInstancesPhrase})) + + changedCount, apiErr := cmd.serviceRepo.MigrateServicePlanFromV1ToV2(v1Guid, v2Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Say(T("{{.CountOfServices}} migrated.", map[string]interface{}{"CountOfServices": pluralizeServiceInstances(changedCount)})) + cmd.ui.Ok() + + return +} + +func pluralizeServiceInstances(count int) string { + var phrase string + if count == 1 { + phrase = T("service instance") + } else { + phrase = T("service instances") + } + + return fmt.Sprintf("%d %s", count, phrase) +} diff --git a/cf/commands/service/migrate_service_instances_test.go b/cf/commands/service/migrate_service_instances_test.go new file mode 100644 index 00000000000..a93e46473ec --- /dev/null +++ b/cf/commands/service/migrate_service_instances_test.go @@ -0,0 +1,292 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/resources" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("migrating service instances from v1 to v2", func() { + var ( + ui *testterm.FakeUI + serviceRepo *testapi.FakeServiceRepo + cmd *MigrateServiceInstances + requirementsFactory *testreq.FakeReqFactory + args []string + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config := testconfig.NewRepository() + serviceRepo = &testapi.FakeServiceRepo{} + cmd = NewMigrateServiceInstances(ui, config, serviceRepo) + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: false} + args = []string{} + }) + + Describe("requirements", func() { + It("requires you to be logged in", func() { + Expect(testcmd.RunCommand(cmd, args, requirementsFactory)).To(BeFalse()) + }) + + It("requires five arguments to run", func() { + requirementsFactory.LoginSuccess = true + args = []string{"one", "two", "three"} + + Expect(testcmd.RunCommand(cmd, args, requirementsFactory)).To(BeFalse()) + }) + + It("passes requirements if user is logged in and provided five args to run", func() { + requirementsFactory.LoginSuccess = true + args = []string{"one", "two", "three", "four", "five"} + ui.Inputs = append(ui.Inputs, "no") + + Expect(testcmd.RunCommand(cmd, args, requirementsFactory)).To(BeTrue()) + }) + }) + + Describe("migrating service instances", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + args = []string{"v1-service-label", "v1-provider-name", "v1-plan-name", "v2-service-label", "v2-plan-name"} + serviceRepo.ServiceInstanceCountForServicePlan = 1 + }) + + It("displays the warning and the prompt including info about the instances and plan to migrate", func() { + ui.Inputs = []string{""} + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really migrate", "1 service instance", + "from plan", "v1-service-label", "v1-provider-name", "v1-plan-name", + "to", "v2-service-label", "v2-plan-name"}, + )) + }) + + Context("when the user confirms", func() { + BeforeEach(func() { + ui.Inputs = []string{"yes"} + }) + + Context("when the v1 and v2 service instances exists", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResultGuids = []string{"v1-guid", "v2-guid"} + serviceRepo.MigrateServicePlanFromV1ToV2ReturnedCount = 1 + }) + + It("makes a request to migrate the v1 service instance", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(serviceRepo.V1GuidToMigrate).To(Equal("v1-guid")) + Expect(serviceRepo.V2GuidToMigrate).To(Equal("v2-guid")) + }) + + It("finds the v1 service plan by its name, provider and service label", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + expectedV1 := resources.ServicePlanDescription{ + ServicePlanName: "v1-plan-name", + ServiceProvider: "v1-provider-name", + ServiceLabel: "v1-service-label", + } + Expect(serviceRepo.FindServicePlanByDescriptionArguments[0]).To(Equal(expectedV1)) + }) + + It("finds the v2 service plan by its name and service label", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + expectedV2 := resources.ServicePlanDescription{ + ServicePlanName: "v2-plan-name", + ServiceLabel: "v2-service-label", + } + Expect(serviceRepo.FindServicePlanByDescriptionArguments[1]).To(Equal(expectedV2)) + }) + + It("notifies the user that the migration was successful", func() { + serviceRepo.ServiceInstanceCountForServicePlan = 2 + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Attempting to migrate", "2", "service instances"}, + []string{"1", "service instance", "migrated"}, + []string{"OK"}, + )) + }) + }) + + Context("when finding the v1 plan fails", func() { + Context("because the plan does not exist", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResponses = []error{errors.NewModelNotFoundError("Service Plan", "")} + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Plan", "v1-service-label", "v1-provider-name", "v1-plan-name", "cannot be found"}, + )) + }) + + It("does not display the warning", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + }) + }) + + Context("because there was an http error", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResponses = []error{errors.New("uh oh")} + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"uh oh"}, + )) + }) + + It("does not display the warning", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + }) + }) + }) + + Context("when finding the v2 plan fails", func() { + Context("because the plan does not exist", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResponses = []error{nil, errors.NewModelNotFoundError("Service Plan", "")} + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Plan", "v2-service-label", "v2-plan-name", "cannot be found"}, + )) + }) + + It("does not display the warning", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + }) + }) + + Context("because there was an http error", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResponses = []error{nil, errors.New("uh oh")} + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"uh oh"}, + )) + }) + + It("does not display the warning", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + }) + }) + }) + + Context("when migrating the plans fails", func() { + BeforeEach(func() { + serviceRepo.MigrateServicePlanFromV1ToV2Response = errors.New("ruh roh") + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"ruh roh"}, + )) + }) + }) + + Context("when there are no instances to migrate", func() { + BeforeEach(func() { + serviceRepo.FindServicePlanByDescriptionResultGuids = []string{"v1-guid", "v2-guid"} + serviceRepo.ServiceInstanceCountForServicePlan = 0 + }) + + It("returns a meaningful error", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"no service instances to migrate"}, + )) + }) + + It("does not show the user the warning", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING:", "this operation is to replace a service broker"})) + }) + }) + + Context("when it cannot fetch the number of instances", func() { + BeforeEach(func() { + serviceRepo.ServiceInstanceCountApiResponse = errors.New("service instance fetch is very bad") + }) + + It("notifies the user of the failure", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"service instance fetch is very bad"}, + )) + }) + }) + }) + + Context("when the user does not confirm", func() { + BeforeEach(func() { + ui.Inputs = append(ui.Inputs, "no") + }) + + It("does not continue the migration", func() { + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Migrating"})) + Expect(serviceRepo.MigrateServicePlanFromV1ToV2Called).To(BeFalse()) + }) + }) + + Context("when the user ignores confirmation using the force flag", func() { + It("does not prompt the user for confirmation", func() { + args = []string{"-f", "v1-service-label", "v1-provider-name", "v1-plan-name", "v2-service-label", "v2-plan-name"} + + testcmd.RunCommand(cmd, args, requirementsFactory) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Really migrate"})) + Expect(serviceRepo.MigrateServicePlanFromV1ToV2Called).To(BeTrue()) + }) + }) + }) +}) diff --git a/cf/commands/service/purge_service_offering.go b/cf/commands/service/purge_service_offering.go new file mode 100644 index 00000000000..63d139ecc9b --- /dev/null +++ b/cf/commands/service/purge_service_offering.go @@ -0,0 +1,83 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type PurgeServiceOffering struct { + ui terminal.UI + serviceRepo api.ServiceRepository +} + +func (cmd PurgeServiceOffering) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + return +} + +func scaryWarningMessage() string { + return T(`WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.`) +} + +func (cmd PurgeServiceOffering) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "purge-service-offering", + Description: T("Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker"), + Usage: T("CF_NAME purge-service-offering SERVICE [-p PROVIDER]") + "\n\n" + scaryWarningMessage(), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Provider")), + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd PurgeServiceOffering) Run(c *cli.Context) { + serviceName := c.Args()[0] + + offering, apiErr := cmd.serviceRepo.FindServiceOfferingByLabelAndProvider(serviceName, c.String("p")) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Warn(T("Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.")) + return + default: + cmd.ui.Failed(apiErr.Error()) + } + + confirmed := c.Bool("f") + if !confirmed { + cmd.ui.Warn(scaryWarningMessage()) + confirmed = cmd.ui.Confirm(T("Really purge service offering {{.ServiceName}} from Cloud Foundry?", + map[string]interface{}{"ServiceName": serviceName}, + )) + } + + if !confirmed { + return + } + cmd.ui.Say(T("Purging service {{.ServiceName}}...", map[string]interface{}{"ServiceName": serviceName})) + err := cmd.serviceRepo.PurgeServiceOffering(offering) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} + +func NewPurgeServiceOffering(ui terminal.UI, config core_config.Reader, serviceRepo api.ServiceRepository) (cmd PurgeServiceOffering) { + cmd.ui = ui + cmd.serviceRepo = serviceRepo + return +} diff --git a/cf/commands/service/purge_service_offering_test.go b/cf/commands/service/purge_service_offering_test.go new file mode 100644 index 00000000000..14cdc538e09 --- /dev/null +++ b/cf/commands/service/purge_service_offering_test.go @@ -0,0 +1,196 @@ +package service_test + +import ( + "errors" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + cferrors "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("purge-service command", func() { + Describe("requirements", func() { + It("fails when not logged in", func() { + deps := setupDependencies() + deps.requirementsFactory.LoginSuccess = false + + cmd := NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo) + passed := testcmd.RunCommand( + cmd, + []string{"-f", "whatever"}, + deps.requirementsFactory, + ) + + Expect(passed).To(BeFalse()) + }) + + It("fails when called without exactly one arg", func() { + deps := setupDependencies() + deps.requirementsFactory.LoginSuccess = true + + passed := testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{}, + deps.requirementsFactory, + ) + + Expect(passed).To(BeFalse()) + Expect(deps.ui.FailedWithUsage).To(BeTrue()) + }) + }) + + It("works when given -p and a provider name", func() { + deps := setupDependencies() + + offering := maker.NewServiceOffering("the-service-name") + deps.serviceRepo.FindServiceOfferingByLabelAndProviderServiceOffering = offering + + deps.ui.Inputs = []string{"yes"} + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"-p", "the-provider", "the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.serviceRepo.FindServiceOfferingByLabelAndProviderName).To(Equal("the-service-name")) + Expect(deps.serviceRepo.FindServiceOfferingByLabelAndProviderProvider).To(Equal("the-provider")) + Expect(deps.serviceRepo.PurgedServiceOffering).To(Equal(offering)) + }) + + It("works when not given a provider", func() { + deps := setupDependencies() + + offering := maker.NewServiceOffering("the-service-name") + deps.serviceRepo.FindServiceOfferingByLabelAndProviderServiceOffering = offering + + deps.ui.Inputs = []string{"yes"} + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.ui.Outputs).To(ContainSubstrings([]string{"WARNING"})) + Expect(deps.ui.Prompts).To(ContainSubstrings([]string{"Really purge service", "the-service-name"})) + Expect(deps.ui.Outputs).To(ContainSubstrings([]string{"Purging service the-service-name..."})) + + Expect(deps.serviceRepo.FindServiceOfferingByLabelAndProviderName).To(Equal("the-service-name")) + Expect(deps.serviceRepo.FindServiceOfferingByLabelAndProviderProvider).To(Equal("")) + Expect(deps.serviceRepo.PurgedServiceOffering).To(Equal(offering)) + + Expect(deps.ui.Outputs).To(ContainSubstrings([]string{"OK"})) + }) + + It("exits when the user does not acknowledge the confirmation", func() { + deps := setupDependencies() + + deps.ui.Inputs = []string{"no"} + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.serviceRepo.FindServiceOfferingByLabelAndProviderCalled).To(Equal(true)) + Expect(deps.serviceRepo.PurgeServiceOfferingCalled).To(Equal(false)) + }) + + It("does not prompt with confirmation when -f is passed", func() { + deps := setupDependencies() + + offering := maker.NewServiceOffering("the-service-name") + deps.serviceRepo.FindServiceOfferingByLabelAndProviderServiceOffering = offering + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"-f", "the-service-name"}, + deps.requirementsFactory, + ) + + Expect(len(deps.ui.Prompts)).To(Equal(0)) + Expect(deps.serviceRepo.PurgeServiceOfferingCalled).To(Equal(true)) + }) + + It("fails with an error message when the request fails", func() { + deps := setupDependencies() + + deps.serviceRepo.FindServiceOfferingByLabelAndProviderApiResponse = cferrors.NewWithError("oh no!", errors.New("!")) + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"-f", "-p", "the-provider", "the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"oh no!"}, + )) + + Expect(deps.serviceRepo.PurgeServiceOfferingCalled).To(Equal(false)) + }) + + It("fails with an error message when the purging request fails", func() { + deps := setupDependencies() + deps.serviceRepo.PurgeServiceOfferingApiResponse = cferrors.New("crumpets insufficiently buttered") + + testcmd.RunCommand(NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"-f", "-p", "the-provider", "the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"crumpets insufficiently buttered"}, + )) + }) + + It("indicates when a service doesn't exist", func() { + deps := setupDependencies() + + deps.serviceRepo.FindServiceOfferingByLabelAndProviderApiResponse = cferrors.NewModelNotFoundError("Service Offering", "") + + deps.ui.Inputs = []string{"yes"} + + testcmd.RunCommand( + NewPurgeServiceOffering(deps.ui, deps.config, deps.serviceRepo), + []string{"-p", "the-provider", "the-service-name"}, + deps.requirementsFactory, + ) + + Expect(deps.ui.Outputs).To(ContainSubstrings([]string{"Service offering", "does not exist"})) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"WARNING"})) + Expect(deps.ui.Outputs).ToNot(ContainSubstrings([]string{"Ok"})) + + Expect(deps.serviceRepo.PurgeServiceOfferingCalled).To(Equal(false)) + }) +}) + +type commandDependencies struct { + ui *testterm.FakeUI + config core_config.ReadWriter + serviceRepo *testapi.FakeServiceRepo + requirementsFactory *testreq.FakeReqFactory +} + +func setupDependencies() (obj commandDependencies) { + obj.ui = &testterm.FakeUI{} + + obj.config = testconfig.NewRepository() + obj.requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + obj.serviceRepo = new(testapi.FakeServiceRepo) + return +} diff --git a/cf/commands/service/rename_service.go b/cf/commands/service/rename_service.go new file mode 100644 index 00000000000..cb98c2add85 --- /dev/null +++ b/cf/commands/service/rename_service.go @@ -0,0 +1,82 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameService struct { + ui terminal.UI + config core_config.Reader + serviceRepo api.ServiceRepository + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +func NewRenameService(ui terminal.UI, config core_config.Reader, serviceRepo api.ServiceRepository) (cmd *RenameService) { + cmd = new(RenameService) + cmd.ui = ui + cmd.config = config + cmd.serviceRepo = serviceRepo + return +} + +func (cmd *RenameService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename-service", + Description: T("Rename a service instance"), + Usage: T("CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE"), + } +} + +func (cmd *RenameService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + return + } + + cmd.serviceInstanceReq = requirementsFactory.NewServiceInstanceRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.serviceInstanceReq, + } + + return +} + +func (cmd *RenameService) Run(c *cli.Context) { + newName := c.Args()[1] + serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() + + cmd.ui.Say(T("Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceInstance.Name), + "NewServiceName": terminal.EntityNameColor(newName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + err := cmd.serviceRepo.RenameService(serviceInstance, newName) + + if err != nil { + if httpError, ok := err.(errors.HttpError); ok && httpError.ErrorCode() == errors.SERVICE_INSTANCE_NAME_TAKEN { + cmd.ui.Failed(T("{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + map[string]interface{}{ + "ErrorDescription": httpError.Error(), + "CFServicesCommand": cf.Name() + " " + "services", + })) + } else { + cmd.ui.Failed(err.Error()) + } + } + + cmd.ui.Ok() +} diff --git a/cf/commands/service/rename_service_test.go b/cf/commands/service/rename_service_test.go new file mode 100644 index 00000000000..092e3ac9396 --- /dev/null +++ b/cf/commands/service/rename_service_test.go @@ -0,0 +1,81 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/service" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("rename-service command", func() { + var ( + ui *testterm.FakeUI + config core_config.ReadWriter + serviceRepo *testapi.FakeServiceRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + serviceRepo = &testapi.FakeServiceRepo{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewRenameService(ui, config, serviceRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("Fails with usage when exactly two parameters not passed", func() { + runCommand("whatever") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.TargetedSpaceSuccess = true + + Expect(runCommand("banana", "fppants")).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("banana", "faaaaasdf")).To(BeFalse()) + }) + }) + + Context("when logged in and a space is targeted", func() { + var serviceInstance models.ServiceInstance + + BeforeEach(func() { + serviceInstance = models.ServiceInstance{} + serviceInstance.Name = "different-name" + serviceInstance.Guid = "different-name-guid" + + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + requirementsFactory.ServiceInstance = serviceInstance + }) + + It("renames the service, obviously", func() { + runCommand("my-service", "new-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming service", "different-name", "new-name", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(serviceRepo.RenameServiceServiceInstance).To(Equal(serviceInstance)) + Expect(serviceRepo.RenameServiceNewName).To(Equal("new-name")) + }) + }) +}) diff --git a/cf/commands/service/service.go b/cf/commands/service/service.go new file mode 100644 index 00000000000..011fa875587 --- /dev/null +++ b/cf/commands/service/service.go @@ -0,0 +1,71 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf/command_metadata" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ShowService struct { + ui terminal.UI + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +func NewShowService(ui terminal.UI) (cmd *ShowService) { + cmd = new(ShowService) + cmd.ui = ui + return +} + +func (cmd *ShowService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "service", + Description: T("Show service instance info"), + Usage: T("CF_NAME service SERVICE_INSTANCE"), + } +} + +func (cmd *ShowService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.serviceInstanceReq = requirementsFactory.NewServiceInstanceRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + cmd.serviceInstanceReq, + } + return +} + +func (cmd *ShowService) Run(c *cli.Context) { + serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() + + cmd.ui.Say("") + cmd.ui.Say(T("Service instance: {{.ServiceName}}", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceInstance.Name)})) + + if serviceInstance.IsUserProvided() { + cmd.ui.Say(T("Service: {{.ServiceDescription}}", + map[string]interface{}{ + "ServiceDescription": terminal.EntityNameColor(T("user-provided")), + })) + } else { + cmd.ui.Say(T("Service: {{.ServiceDescription}}", + map[string]interface{}{ + "ServiceDescription": terminal.EntityNameColor(serviceInstance.ServiceOffering.Label), + })) + cmd.ui.Say(T("Plan: {{.ServicePlanName}}", + map[string]interface{}{ + "ServicePlanName": terminal.EntityNameColor(serviceInstance.ServicePlan.Name), + })) + cmd.ui.Say(T("Description: {{.ServiceDescription}}", map[string]interface{}{"ServiceDescription": terminal.EntityNameColor(serviceInstance.ServiceOffering.Description)})) + cmd.ui.Say(T("Documentation url: {{.URL}}", + map[string]interface{}{ + "URL": terminal.EntityNameColor(serviceInstance.ServiceOffering.DocumentationUrl), + })) + } +} diff --git a/cf/commands/service/service_suite_test.go b/cf/commands/service/service_suite_test.go new file mode 100644 index 00000000000..b6214ec5eb2 --- /dev/null +++ b/cf/commands/service/service_suite_test.go @@ -0,0 +1,19 @@ +package service_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestService(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Service Suite") +} diff --git a/cf/commands/service/service_test.go b/cf/commands/service/service_test.go new file mode 100644 index 00000000000..37197767596 --- /dev/null +++ b/cf/commands/service/service_test.go @@ -0,0 +1,103 @@ +package service_test + +import ( + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/service" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("service command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewShowService(ui), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not provided the name of the service to show", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + runCommand() + + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.TargetedSpaceSuccess = true + + Expect(runCommand("come-ON")).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("okay-this-time-please??")).To(BeFalse()) + }) + }) + + Context("when logged in, a space is targeted, and provided the name of a service that exists", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + }) + + Context("when the service is externally provided", func() { + BeforeEach(func() { + offering := models.ServiceOfferingFields{Label: "mysql", DocumentationUrl: "http://documentation.url", Description: "the-description"} + plan := models.ServicePlanFields{Guid: "plan-guid", Name: "plan-name"} + + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "service1" + serviceInstance.Guid = "service1-guid" + serviceInstance.ServicePlan = plan + serviceInstance.ServiceOffering = offering + requirementsFactory.ServiceInstance = serviceInstance + }) + + It("shows the service", func() { + runCommand("service1") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Service instance:", "service1"}, + []string{"Service: ", "mysql"}, + []string{"Plan: ", "plan-name"}, + []string{"Description: ", "the-description"}, + []string{"Documentation url: ", "http://documentation.url"}, + )) + Expect(requirementsFactory.ServiceInstanceName).To(Equal("service1")) + }) + }) + + Context("when th e service is user provided", func() { + BeforeEach(func() { + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "service1" + serviceInstance.Guid = "service1-guid" + requirementsFactory.ServiceInstance = serviceInstance + }) + + It("shows user provided services", func() { + runCommand("service1") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Service instance: ", "service1"}, + []string{"Service: ", "user-provided"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/service/services.go b/cf/commands/service/services.go new file mode 100644 index 00000000000..ed997bab4aa --- /dev/null +++ b/cf/commands/service/services.go @@ -0,0 +1,91 @@ +package service + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "strings" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListServices struct { + ui terminal.UI + config core_config.Reader + serviceSummaryRepo api.ServiceSummaryRepository +} + +func NewListServices(ui terminal.UI, config core_config.Reader, serviceSummaryRepo api.ServiceSummaryRepository) (cmd ListServices) { + cmd.ui = ui + cmd.config = config + cmd.serviceSummaryRepo = serviceSummaryRepo + return +} + +func (cmd ListServices) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "services", + ShortName: "s", + Description: T("List all service instances in the target space"), + Usage: "CF_NAME services", + } +} + +func (cmd ListServices) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = append(reqs, + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + ) + return +} + +func (cmd ListServices) Run(c *cli.Context) { + cmd.ui.Say(T("Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + serviceInstances, apiErr := cmd.serviceSummaryRepo.GetSummariesInCurrentSpace() + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + if len(serviceInstances) == 0 { + cmd.ui.Say(T("No services found")) + return + } + + table := terminal.NewTable(cmd.ui, []string{T("name"), T("service"), T("plan"), T("bound apps")}) + + for _, instance := range serviceInstances { + var serviceColumn string + + if instance.IsUserProvided() { + serviceColumn = T("user-provided") + } else { + serviceColumn = instance.ServiceOffering.Label + } + + table.Add( + instance.Name, + serviceColumn, + instance.ServicePlan.Name, + strings.Join(instance.ApplicationNames, ", "), + ) + } + + table.Print() +} diff --git a/cf/commands/service/services_test.go b/cf/commands/service/services_test.go new file mode 100644 index 00000000000..c72c58e120d --- /dev/null +++ b/cf/commands/service/services_test.go @@ -0,0 +1,133 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" +) + +var _ = Describe("services", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.Repository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedSpaceSuccess: true, + TargetedOrgSuccess: true, + } + }) + + Describe("services requirements", func() { + var cmd ListServices + + BeforeEach(func() { + cmd = NewListServices(ui, configRepo, &testapi.FakeServiceSummaryRepo{}) + }) + + Context("when not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(testcmd.RunCommand(cmd, []string{}, requirementsFactory)).To(BeFalse()) + }) + }) + + Context("when no space is targeted", func() { + BeforeEach(func() { + requirementsFactory.TargetedSpaceSuccess = false + }) + + It("fails requirements", func() { + Expect(testcmd.RunCommand(cmd, []string{}, requirementsFactory)).To(BeFalse()) + }) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedSpaceSuccess = true + Expect(testcmd.RunCommand(cmd, []string{"blahblah"}, requirementsFactory)).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + It("lists available services", func() { + plan := models.ServicePlanFields{ + Guid: "spark-guid", + Name: "spark", + } + + plan2 := models.ServicePlanFields{ + Guid: "spark-guid-2", + Name: "spark-2", + } + + offering := models.ServiceOfferingFields{Label: "cleardb"} + + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "my-service-1" + serviceInstance.ServicePlan = plan + serviceInstance.ApplicationNames = []string{"cli1", "cli2"} + serviceInstance.ServiceOffering = offering + + serviceInstance2 := models.ServiceInstance{} + serviceInstance2.Name = "my-service-2" + serviceInstance2.ServicePlan = plan2 + serviceInstance2.ApplicationNames = []string{"cli1"} + serviceInstance2.ServiceOffering = offering + + serviceInstance3 := models.ServiceInstance{} + serviceInstance3.Name = "my-service-provided-by-user" + + serviceInstances := []models.ServiceInstance{serviceInstance, serviceInstance2, serviceInstance3} + serviceSummaryRepo := &testapi.FakeServiceSummaryRepo{ + GetSummariesInCurrentSpaceInstances: serviceInstances, + } + + cmd := NewListServices(ui, configRepo, serviceSummaryRepo) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting services in org", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"my-service-1", "cleardb", "spark", "cli1, cli2"}, + []string{"my-service-2", "cleardb", "spark-2", "cli1"}, + []string{"my-service-provided-by-user", "user-provided"}, + )) + }) + + It("lists no services when none are found", func() { + serviceInstances := []models.ServiceInstance{} + serviceSummaryRepo := &testapi.FakeServiceSummaryRepo{ + GetSummariesInCurrentSpaceInstances: serviceInstances, + } + + cmd := NewListServices(ui, configRepo, serviceSummaryRepo) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting services in org", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"No services found"}, + )) + + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"name", "service", "plan", "bound apps"}, + )) + }) +}) diff --git a/cf/commands/service/unbind_service.go b/cf/commands/service/unbind_service.go new file mode 100644 index 00000000000..f41b4e4b053 --- /dev/null +++ b/cf/commands/service/unbind_service.go @@ -0,0 +1,83 @@ +package service + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnbindService struct { + ui terminal.UI + config core_config.Reader + serviceBindingRepo api.ServiceBindingRepository + appReq requirements.ApplicationRequirement + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +func NewUnbindService(ui terminal.UI, config core_config.Reader, serviceBindingRepo api.ServiceBindingRepository) (cmd *UnbindService) { + cmd = new(UnbindService) + cmd.ui = ui + cmd.config = config + cmd.serviceBindingRepo = serviceBindingRepo + return +} + +func (cmd *UnbindService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unbind-service", + ShortName: "us", + Description: T("Unbind a service instance from an app"), + Usage: T("CF_NAME unbind-service APP SERVICE_INSTANCE"), + } +} + +func (cmd *UnbindService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + appName := c.Args()[0] + serviceName := c.Args()[1] + + cmd.appReq = requirementsFactory.NewApplicationRequirement(appName) + cmd.serviceInstanceReq = requirementsFactory.NewServiceInstanceRequirement(serviceName) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.appReq, + cmd.serviceInstanceReq, + } + return +} + +func (cmd *UnbindService) Run(c *cli.Context) { + app := cmd.appReq.GetApplication() + instance := cmd.serviceInstanceReq.GetServiceInstance() + + cmd.ui.Say(T("Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "AppName": terminal.EntityNameColor(app.Name), + "ServiceName": terminal.EntityNameColor(instance.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + found, apiErr := cmd.serviceBindingRepo.Delete(instance, app.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + + if !found { + cmd.ui.Warn(T("Binding between {{.InstanceName}} and {{.AppName}} did not exist", + map[string]interface{}{"InstanceName": instance.Name, "AppName": app.Name})) + } + +} diff --git a/cf/commands/service/unbind_service_test.go b/cf/commands/service/unbind_service_test.go new file mode 100644 index 00000000000..2868a08bca5 --- /dev/null +++ b/cf/commands/service/unbind_service_test.go @@ -0,0 +1,110 @@ +package service_test + +import ( + "github.com/cloudfoundry/cli/cf/api" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("unbind-service command", func() { + var ( + app models.Application + serviceInstance models.ServiceInstance + requirementsFactory *testreq.FakeReqFactory + serviceBindingRepo *testapi.FakeServiceBindingRepo + ) + + BeforeEach(func() { + app.Name = "my-app" + app.Guid = "my-app-guid" + + serviceInstance.Name = "my-service" + serviceInstance.Guid = "my-service-guid" + + requirementsFactory = &testreq.FakeReqFactory{} + requirementsFactory.Application = app + requirementsFactory.ServiceInstance = serviceInstance + + serviceBindingRepo = &testapi.FakeServiceBindingRepo{} + }) + + Context("when not logged in", func() { + It("fails requirements when not logged in", func() { + cmd := NewUnbindService(&testterm.FakeUI{}, testconfig.NewRepository(), serviceBindingRepo) + Expect(testcmd.RunCommand(cmd, []string{"my-service", "my-app"}, requirementsFactory)).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + Context("when the service instance exists", func() { + It("unbinds a service from an app", func() { + ui := callUnbindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(requirementsFactory.ServiceInstanceName).To(Equal("my-service")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding app", "my-service", "my-app", "my-org", "my-space", "my-user"}, + []string{"OK"}, + )) + Expect(serviceBindingRepo.DeleteServiceInstance).To(Equal(serviceInstance)) + Expect(serviceBindingRepo.DeleteApplicationGuid).To(Equal("my-app-guid")) + }) + }) + + Context("when the service instance does not exist", func() { + BeforeEach(func() { + serviceBindingRepo.DeleteBindingNotFound = true + }) + + It("warns the user the the service instance does not exist", func() { + ui := callUnbindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + + Expect(requirementsFactory.ApplicationName).To(Equal("my-app")) + Expect(requirementsFactory.ServiceInstanceName).To(Equal("my-service")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unbinding app", "my-service", "my-app"}, + []string{"OK"}, + []string{"my-service", "my-app", "did not exist"}, + )) + Expect(serviceBindingRepo.DeleteServiceInstance).To(Equal(serviceInstance)) + Expect(serviceBindingRepo.DeleteApplicationGuid).To(Equal("my-app-guid")) + }) + }) + + It("when no parameters are given the command fails with usage", func() { + ui := callUnbindService([]string{"my-service"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeTrue()) + + ui = callUnbindService([]string{"my-app"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeTrue()) + + ui = callUnbindService([]string{"my-app", "my-service"}, requirementsFactory, serviceBindingRepo) + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + }) +}) + +func callUnbindService(args []string, requirementsFactory *testreq.FakeReqFactory, serviceBindingRepo api.ServiceBindingRepository) (fakeUI *testterm.FakeUI) { + fakeUI = &testterm.FakeUI{} + + config := testconfig.NewRepositoryWithDefaults() + + cmd := NewUnbindService(fakeUI, config, serviceBindingRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} diff --git a/cf/commands/service/update_service.go b/cf/commands/service/update_service.go new file mode 100644 index 00000000000..d53c160dfa4 --- /dev/null +++ b/cf/commands/service/update_service.go @@ -0,0 +1,105 @@ +package service + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors/plan_builder" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateService struct { + ui terminal.UI + config core_config.Reader + serviceRepo api.ServiceRepository + planBuilder plan_builder.PlanBuilder +} + +func NewUpdateService(ui terminal.UI, config core_config.Reader, serviceRepo api.ServiceRepository, planBuilder plan_builder.PlanBuilder) (cmd *UpdateService) { + return &UpdateService{ + ui: ui, + config: config, + serviceRepo: serviceRepo, + planBuilder: planBuilder, + } +} + +func (cmd *UpdateService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-service", + Description: T("Update a service instance"), + Usage: T("CF_NAME update-service SERVICE [-p NEW_PLAN]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Change service plan for a service instance")), + }, + } +} + +func (cmd *UpdateService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedSpaceRequirement(), + } + + return +} + +func (cmd *UpdateService) Run(c *cli.Context) { + serviceInstanceName := c.Args()[0] + + serviceInstance, err := cmd.serviceRepo.FindInstanceByName(serviceInstanceName) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + planName := c.String("p") + + if planName != "" { + cmd.ui.Say(T("Updating service instance {{.ServiceName}} as {{.UserName}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceInstanceName), + "UserName": terminal.EntityNameColor(cmd.config.Username()), + })) + + err := cmd.updateServiceWithPlan(serviceInstance, planName) + switch err.(type) { + case nil: + cmd.ui.Ok() + default: + cmd.ui.Failed(err.Error()) + } + } else { + cmd.ui.Ok() + cmd.ui.Say(T("No changes were made")) + } +} + +func (cmd *UpdateService) updateServiceWithPlan(serviceInstance models.ServiceInstance, planName string) (err error) { + plans, err := cmd.planBuilder.GetPlansForServiceForOrg(serviceInstance.ServiceOffering.Guid, cmd.config.OrganizationFields().Name) + if err != nil { + return + } + + for _, plan := range plans { + if plan.Name == planName { + err = cmd.serviceRepo.UpdateServiceInstance(serviceInstance.Guid, plan.Guid) + return + } + } + err = errors.New(T("Plan does not exist for the {{.ServiceName}} service", + map[string]interface{}{"ServiceName": serviceInstance.ServiceOffering.Label})) + + return +} diff --git a/cf/commands/service/update_service_test.go b/cf/commands/service/update_service_test.go new file mode 100644 index 00000000000..48088eca0ee --- /dev/null +++ b/cf/commands/service/update_service_test.go @@ -0,0 +1,186 @@ +package service_test + +import ( + "errors" + + testplanbuilder "github.com/cloudfoundry/cli/cf/actors/plan_builder/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/service" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("update-service command", func() { + var ( + ui *testterm.FakeUI + config core_config.Repository + requirementsFactory *testreq.FakeReqFactory + serviceRepo *testapi.FakeServiceRepo + planBuilder *testplanbuilder.FakePlanBuilder + offering1 models.ServiceOffering + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} + serviceRepo = &testapi.FakeServiceRepo{} + planBuilder = &testplanbuilder.FakePlanBuilder{} + + offering1 = models.ServiceOffering{} + offering1.Label = "cleardb" + offering1.Plans = []models.ServicePlanFields{{ + Name: "spark", + Guid: "cleardb-spark-guid", + }, { + Name: "flare", + Guid: "cleardb-flare-guid", + }, + } + + //serviceRepo.FindServiceOfferingsForSpaceByLabelReturns.ServiceOfferings = []models.ServiceOffering{offering1} + }) + + var callUpdateService = func(args []string) bool { + cmd := NewUpdateService(ui, config, serviceRepo, planBuilder) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("passes when logged in and a space is targeted", func() { + Expect(callUpdateService([]string{"cleardb"})).To(BeTrue()) + }) + + It("fails with usage when not provided exactly one arg", func() { + Expect(callUpdateService([]string{})).To(BeFalse()) + }) + + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(callUpdateService([]string{"cleardb", "spark", "my-cleardb-service"})).To(BeFalse()) + }) + + It("fails when a space is not targeted", func() { + requirementsFactory.TargetedSpaceSuccess = false + Expect(callUpdateService([]string{"cleardb", "spark", "my-cleardb-service"})).To(BeFalse()) + }) + }) + Context("when no flags are passed", func() { + Context("when there is an err finding the instance", func() { + It("returns an error", func() { + serviceRepo.FindInstanceByNameErr = true + + callUpdateService([]string{"some-stupid-not-real-instance"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Error finding instance"}, + []string{"FAILED"}, + )) + }) + }) + Context("when the instance exists", func() { + It("prints a user indicating it is a no-op", func() { + callUpdateService([]string{"my-service"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"OK"}, + []string{"No changes were made"}, + )) + }) + }) + }) + Context("when the plan flag is passed", func() { + BeforeEach(func() { + serviceInstance := models.ServiceInstance{ + ServiceInstanceFields: models.ServiceInstanceFields{ + Name: "my-service-instance", + Guid: "my-service-instance-guid", + }, + ServiceOffering: models.ServiceOfferingFields{ + Label: "murkydb", + Guid: "murkydb-guid", + }, + } + + servicePlans := []models.ServicePlanFields{{ + Name: "spark", + Guid: "murkydb-spark-guid", + }, { + Name: "flare", + Guid: "murkydb-flare-guid", + }, + } + serviceRepo.FindInstanceByNameServiceInstance = serviceInstance + planBuilder.GetPlansForServiceForOrgReturns(servicePlans, nil) + + }) + It("successfully updates a service", func() { + callUpdateService([]string{"-p", "flare", "my-service-instance"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating service", "my-service", "as", "my-user", "..."}, + []string{"OK"}, + )) + Expect(serviceRepo.FindInstanceByNameName).To(Equal("my-service-instance")) + serviceGuid, orgName := planBuilder.GetPlansForServiceForOrgArgsForCall(0) + Expect(serviceGuid).To(Equal("murkydb-guid")) + Expect(orgName).To(Equal("my-org")) + Expect(serviceRepo.UpdateServiceInstanceArgs.InstanceGuid).To(Equal("my-service-instance-guid")) + Expect(serviceRepo.UpdateServiceInstanceArgs.PlanGuid).To(Equal("murkydb-flare-guid")) + }) + + Context("when there is an err finding the instance", func() { + It("returns an error", func() { + serviceRepo.FindInstanceByNameErr = true + + callUpdateService([]string{"-p", "flare", "some-stupid-not-real-instance"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Error finding instance"}, + []string{"FAILED"}, + )) + }) + }) + Context("when there is an err finding service plans", func() { + It("returns an error", func() { + planBuilder.GetPlansForServiceForOrgReturns(nil, errors.New("Error fetching plans")) + + callUpdateService([]string{"-p", "flare", "some-stupid-not-real-instance"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Error fetching plans"}, + []string{"FAILED"}, + )) + }) + }) + Context("when the plan specified does not exist in the service offering", func() { + It("returns an error", func() { + callUpdateService([]string{"-p", "not-a-real-plan", "instance-without-service-offering"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Plan does not exist for the murkydb service"}, + []string{"FAILED"}, + )) + }) + }) + Context("when there is an error updating the service instance", func() { + It("returns an error", func() { + serviceRepo.UpdateServiceInstanceReturnsErr = true + callUpdateService([]string{"-p", "flare", "my-service-instance"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Error updating service instance"}, + []string{"FAILED"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/service/update_user_provided_service.go b/cf/commands/service/update_user_provided_service.go new file mode 100644 index 00000000000..b26f2849890 --- /dev/null +++ b/cf/commands/service/update_user_provided_service.go @@ -0,0 +1,113 @@ +package service + +import ( + "encoding/json" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateUserProvidedService struct { + ui terminal.UI + config core_config.Reader + userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository + serviceInstanceReq requirements.ServiceInstanceRequirement +} + +func NewUpdateUserProvidedService(ui terminal.UI, config core_config.Reader, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (cmd *UpdateUserProvidedService) { + cmd = new(UpdateUserProvidedService) + cmd.ui = ui + cmd.config = config + cmd.userProvidedServiceInstanceRepo = userProvidedServiceInstanceRepo + return +} + +func (cmd *UpdateUserProvidedService) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-user-provided-service", + ShortName: "uups", + Description: T("Update user-provided service instance name value pairs"), + Usage: T(`CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]' + +EXAMPLE: + CF_NAME update-user-provided-service oracle-db-mine -p '{"username":"admin","password":"pa55woRD"}' + CF_NAME update-user-provided-service my-drain-service -l syslog://example.com`), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Parameters")), + flag_helpers.NewStringFlag("l", T("Syslog Drain Url")), + }, + } +} + +func (cmd *UpdateUserProvidedService) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.serviceInstanceReq = requirementsFactory.NewServiceInstanceRequirement(c.Args()[0]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.serviceInstanceReq, + } + + return +} + +func (cmd *UpdateUserProvidedService) Run(c *cli.Context) { + + serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() + if !serviceInstance.IsUserProvided() { + cmd.ui.Failed(T("Service Instance is not user provided")) + return + } + + drainUrl := c.String("l") + params := c.String("p") + + paramsMap := make(map[string]interface{}) + if params != "" { + + err := json.Unmarshal([]byte(params), ¶msMap) + if err != nil { + cmd.ui.Failed(T("JSON is invalid: {{.ErrorDescription}}", map[string]interface{}{"ErrorDescription": err.Error()})) + return + } + } + + cmd.ui.Say(T("Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceInstance.Name), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + serviceInstance.Params = paramsMap + serviceInstance.SysLogDrainUrl = drainUrl + + apiErr := cmd.userProvidedServiceInstanceRepo.Update(serviceInstance.ServiceInstanceFields) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say(T("TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + map[string]interface{}{ + "CFUnbindCommand": cf.Name() + " unbind-service", + "CFBindComand": cf.Name() + " bind-service", + "CFRestageCommand": cf.Name() + " restage", + })) + + if params == "" && drainUrl == "" { + cmd.ui.Warn(T("No flags specified. No changes were made.")) + } +} diff --git a/cf/commands/service/update_user_provided_service_test.go b/cf/commands/service/update_user_provided_service_test.go new file mode 100644 index 00000000000..38d56007ab5 --- /dev/null +++ b/cf/commands/service/update_user_provided_service_test.go @@ -0,0 +1,120 @@ +package service_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/service" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("update-user-provided-service test", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + serviceRepo *testapi.FakeUserProvidedServiceInstanceRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + serviceRepo = &testapi.FakeUserProvidedServiceInstanceRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUpdateUserProvidedService(ui, configRepo, serviceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided the name of the service to update", func() { + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("whoops")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "service-name" + requirementsFactory.ServiceInstance = serviceInstance + }) + + Context("when no flags are provided", func() { + It("tells the user that no changes occurred", func() { + runCommand("service-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating user provided service", "service-name", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"No changes"}, + )) + }) + }) + + Context("when the user provides valid JSON with the -p flag", func() { + It("updates the user provided service specified", func() { + runCommand("-p", `{"foo":"bar"}`, "-l", "syslog://example.com", "service-name") + + Expect(requirementsFactory.ServiceInstanceName).To(Equal("service-name")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating user provided service", "service-name", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"TIP"}, + )) + Expect(serviceRepo.UpdateServiceInstance.Name).To(Equal("service-name")) + Expect(serviceRepo.UpdateServiceInstance.Params).To(Equal(map[string]interface{}{"foo": "bar"})) + Expect(serviceRepo.UpdateServiceInstance.SysLogDrainUrl).To(Equal("syslog://example.com")) + }) + }) + + Context("when the user provides invalid JSON with the -p flag", func() { + It("tells the user the JSON is invalid", func() { + runCommand("-p", `{"foo":"ba WHOOPS OH MY HOW DID THIS GET HERE???`, "service-name") + + Expect(serviceRepo.UpdateServiceInstance).To(Equal(models.ServiceInstanceFields{})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"JSON is invalid"}, + )) + }) + }) + + Context("when the service with the given name is not user provided", func() { + BeforeEach(func() { + plan := models.ServicePlanFields{Guid: "my-plan-guid"} + serviceInstance := models.ServiceInstance{} + serviceInstance.Name = "found-service-name" + serviceInstance.ServicePlan = plan + + requirementsFactory.ServiceInstance = serviceInstance + }) + + It("fails and tells the user what went wrong", func() { + runCommand("-p", `{"foo":"bar"}`, "service-name") + + Expect(serviceRepo.UpdateServiceInstance).To(Equal(models.ServiceInstanceFields{})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Service Instance is not user provided"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/serviceaccess/disable_service_access.go b/cf/commands/serviceaccess/disable_service_access.go new file mode 100644 index 00000000000..882fa4128f6 --- /dev/null +++ b/cf/commands/serviceaccess/disable_service_access.go @@ -0,0 +1,142 @@ +package serviceaccess + +import ( + "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DisableServiceAccess struct { + ui terminal.UI + config core_config.Reader + actor actors.ServicePlanActor + tokenRefresher authentication.TokenRefresher +} + +func NewDisableServiceAccess(ui terminal.UI, config core_config.Reader, actor actors.ServicePlanActor, tokenRefresher authentication.TokenRefresher) (cmd *DisableServiceAccess) { + return &DisableServiceAccess{ + ui: ui, + config: config, + actor: actor, + tokenRefresher: tokenRefresher, + } +} + +func (cmd *DisableServiceAccess) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{requirementsFactory.NewLoginRequirement()}, nil +} + +func (cmd *DisableServiceAccess) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "disable-service-access", + Description: T("Disable access to a service or service plan for one or all orgs"), + Usage: "CF_NAME disable-service-access SERVICE [-p PLAN] [-o ORG]", + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Disable access to a specified service plan")), + flag_helpers.NewStringFlag("o", T("Disable access for a specified organization")), + }, + } +} + +func (cmd *DisableServiceAccess) Run(c *cli.Context) { + _, err := cmd.tokenRefresher.RefreshAuthToken() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + serviceName := c.Args()[0] + planName := c.String("p") + orgName := c.String("o") + + if planName != "" && orgName != "" { + cmd.disablePlanAndOrgForService(serviceName, planName, orgName) + } else if planName != "" { + cmd.disableSinglePlanForService(serviceName, planName) + } else if orgName != "" { + cmd.disablePlansForSingleOrgForService(serviceName, orgName) + } else { + cmd.disableServiceForAll(serviceName) + } + + cmd.ui.Say(T("OK")) +} + +func (cmd *DisableServiceAccess) disableServiceForAll(serviceName string) { + cmd.ui.Say(T("Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName), "UserName": terminal.EntityNameColor(cmd.config.Username())})) + allPlansAlreadySet, err := cmd.actor.UpdateAllPlansForService(serviceName, false) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if allPlansAlreadySet { + cmd.ui.Say(T("All plans of the service are already inaccessible for all orgs")) + } +} + +func (cmd *DisableServiceAccess) disablePlanAndOrgForService(serviceName string, planName string, orgName string) { + cmd.ui.Say(T("Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", map[string]interface{}{"PlanName": terminal.EntityNameColor(planName), "ServiceName": terminal.EntityNameColor(serviceName), "OrgName": terminal.EntityNameColor(orgName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + planOriginalAccess, err := cmd.actor.UpdatePlanAndOrgForService(serviceName, planName, orgName, false) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if planOriginalAccess == actors.None { + cmd.ui.Say(T("The plan is already inaccessible for this org")) + } else if planOriginalAccess != actors.Limited { + cmd.ui.Say(T("No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + map[string]interface{}{ + "PlanName": terminal.EntityNameColor(planName), + "ServiceName": terminal.EntityNameColor(serviceName), + "OrgName": terminal.EntityNameColor(orgName), + })) + } + return +} + +func (cmd *DisableServiceAccess) disableSinglePlanForService(serviceName string, planName string) { + cmd.ui.Say(T("Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", map[string]interface{}{"PlanName": terminal.EntityNameColor(planName), "ServiceName": terminal.EntityNameColor(serviceName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + planOriginalAccess, err := cmd.actor.UpdateSinglePlanForService(serviceName, planName, false) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if planOriginalAccess == actors.None { + cmd.ui.Say(T("The plan is already inaccessible for all orgs")) + } + return +} + +func (cmd *DisableServiceAccess) disablePlansForSingleOrgForService(serviceName string, orgName string) { + cmd.ui.Say(T("Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName), "OrgName": terminal.EntityNameColor(orgName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + serviceAccess, err := cmd.actor.FindServiceAccess(serviceName, orgName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + if serviceAccess == actors.AllPlansArePublic { + cmd.ui.Say(T("No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + map[string]interface{}{ + "ServiceName": terminal.EntityNameColor(serviceName), + "OrgName": terminal.EntityNameColor(orgName), + })) + return + } + + allPlansWereSet, err := cmd.actor.UpdateOrgForService(serviceName, orgName, false) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if allPlansWereSet { + cmd.ui.Say(T("All plans of the service are already inaccessible for this org")) + } +} diff --git a/cf/commands/serviceaccess/disable_service_access_test.go b/cf/commands/serviceaccess/disable_service_access_test.go new file mode 100644 index 00000000000..865c2fd9bef --- /dev/null +++ b/cf/commands/serviceaccess/disable_service_access_test.go @@ -0,0 +1,220 @@ +package serviceaccess_test + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors" + testactor "github.com/cloudfoundry/cli/cf/actors/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/serviceaccess" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("disable-service-access command", func() { + var ( + ui *testterm.FakeUI + actor *testactor.FakeServicePlanActor + requirementsFactory *testreq.FakeReqFactory + tokenRefresher *testapi.FakeAuthenticationRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{ + Inputs: []string{"yes"}, + } + actor = &testactor.FakeServicePlanActor{} + requirementsFactory = &testreq.FakeReqFactory{} + tokenRefresher = &testapi.FakeAuthenticationRepository{} + }) + + runCommand := func(args []string) bool { + cmd := NewDisableServiceAccess(ui, configuration.NewRepositoryWithDefaults(), actor, tokenRefresher) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + Expect(runCommand([]string{"foo"})).To(BeFalse()) + }) + + It("fails with usage when it does not recieve any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand(nil) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("refreshes the auth token", func() { + runCommand([]string{"service"}) + Expect(tokenRefresher.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when refreshing the auth token fails", func() { + It("fails and returns the error", func() { + tokenRefresher.RefreshTokenError = errors.New("Refreshing went wrong") + runCommand([]string{"service"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Refreshing went wrong"}, + []string{"FAILED"}, + )) + }) + }) + + Context("when the named service exists", func() { + It("tells the user if all plans were already private", func() { + actor.UpdateAllPlansForServiceReturns(true, nil) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"All plans of the service are already inaccessible for all orgs"}, + []string{"OK"}, + )) + }) + + It("tells the user the plans are being updated if they weren't all already private", func() { + actor.UpdateAllPlansForServiceReturns(false, nil) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Disabling access to all plans of service service for all orgs as my-user..."}, + []string{"OK"}, + )) + }) + + It("prints an error if updating one of the plans fails", func() { + actor.UpdateAllPlansForServiceReturns(true, errors.New("Kaboom!")) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Kaboom!"}, + )) + }) + + Context("The user provides a plan", func() { + It("prints an error if the service does not exist", func() { + actor.UpdateSinglePlanForServiceReturns(actors.All, errors.New("could not find service")) + + Expect(runCommand([]string{"-p", "service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find service"}, + )) + }) + + It("tells the user if the plan is already private", func() { + actor.UpdateSinglePlanForServiceReturns(actors.None, nil) + + Expect(runCommand([]string{"-p", "private-service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"The plan is already inaccessible for all orgs"}, + []string{"OK"}, + )) + }) + + It("tells the user the plan is being updated if it is not private", func() { + actor.UpdateSinglePlanForServiceReturns(actors.All, nil) + + Expect(runCommand([]string{"-p", "public-service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Disabling access of plan public-service-plan for service service"}, + []string{"OK"}, + )) + }) + }) + + Context("the user provides an org", func() { + It("fails if the org does not exist", func() { + actor.UpdateOrgForServiceReturns(false, errors.New("could not find org")) + + Expect(runCommand([]string{"-o", "not-findable-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find org"}, + )) + }) + + It("tells the user if the service's plans are already inaccessible", func() { + actor.UpdateOrgForServiceReturns(true, nil) + + Expect(runCommand([]string{"-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"All plans of the service are already inaccessible for this org"}, + []string{"OK"}, + )) + }) + + It("tells the user the service's plans are being updated if they are accessible", func() { + actor.UpdateOrgForServiceReturns(false, nil) + + Expect(runCommand([]string{"-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Disabling access to all plans of service service for the org my-org as"}, + []string{"OK"}, + )) + }) + + It("tells the user if the service's plans are all public", func() { + actor.FindServiceAccessReturns(actors.AllPlansArePublic, nil) + actor.UpdateOrgForServiceReturns(true, nil) + + Expect(runCommand([]string{"-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No action taken. You must disable access to all plans of service service for all orgs and then grant access for all orgs except the my-org org."}, + []string{"OK"}, + )) + }) + }) + + Context("the user provides a plan and org", func() { + It("fails if the org does not exist", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.All, errors.New("could not find org")) + + Expect(runCommand([]string{"-p", "service-plan", "-o", "not-findable-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find org"}, + )) + }) + + It("tells the user if the plan is already private", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.None, nil) + + Expect(runCommand([]string{"-p", "private-service-plan", "-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"The plan is already inaccessible for this org"}, + []string{"OK"}, + )) + }) + + It("tells the user the use if the plan is being updated if the plan is limited", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.Limited, nil) + + Expect(runCommand([]string{"-p", "limited-service-plan", "-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Disabling access to plan limited-service-plan of service service for org my-org as"}, + []string{"OK"}, + )) + }) + + It("tells the user the plan is accessible to all orgs if the plan is public", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.All, nil) + + Expect(runCommand([]string{"-p", "public-service-plan", "-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"No action taken. You must disable access to the public-service-plan plan of service service for all orgs and then grant access for all orgs except the my-org org."}, + []string{"OK"}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/serviceaccess/enable_service_access.go b/cf/commands/serviceaccess/enable_service_access.go new file mode 100644 index 00000000000..9334ea2195b --- /dev/null +++ b/cf/commands/serviceaccess/enable_service_access.go @@ -0,0 +1,120 @@ +package serviceaccess + +import ( + "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" + + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type EnableServiceAccess struct { + ui terminal.UI + config core_config.Reader + actor actors.ServicePlanActor + tokenRefresher authentication.TokenRefresher +} + +func NewEnableServiceAccess(ui terminal.UI, config core_config.Reader, actor actors.ServicePlanActor, tokenRefresher authentication.TokenRefresher) (cmd *EnableServiceAccess) { + return &EnableServiceAccess{ + ui: ui, + config: config, + actor: actor, + tokenRefresher: tokenRefresher, + } +} + +func (cmd *EnableServiceAccess) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{requirementsFactory.NewLoginRequirement()}, nil +} + +func (cmd *EnableServiceAccess) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "enable-service-access", + Description: T("Enable access to a service or service plan for one or all orgs"), + Usage: "CF_NAME enable-service-access SERVICE [-p PLAN] [-o ORG]", + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("p", T("Enable access to a specified service plan")), + flag_helpers.NewStringFlag("o", T("Enable access for a specified organization")), + }, + } +} + +func (cmd *EnableServiceAccess) Run(c *cli.Context) { + _, err := cmd.tokenRefresher.RefreshAuthToken() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + serviceName := c.Args()[0] + planName := c.String("p") + orgName := c.String("o") + + if planName != "" && orgName != "" { + cmd.enablePlanAndOrgForService(serviceName, planName, orgName) + } else if planName != "" { + cmd.enablePlanForService(serviceName, planName) + } else if orgName != "" { + cmd.enableAllPlansForSingleOrgForService(serviceName, orgName) + } else { + cmd.enableAllPlansForService(serviceName) + } + cmd.ui.Say("OK") +} + +func (cmd *EnableServiceAccess) enablePlanAndOrgForService(serviceName string, planName string, orgName string) { + cmd.ui.Say(T("Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", map[string]interface{}{"PlanName": terminal.EntityNameColor(planName), "ServiceName": terminal.EntityNameColor(serviceName), "OrgName": terminal.EntityNameColor(orgName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + planOriginalAccess, err := cmd.actor.UpdatePlanAndOrgForService(serviceName, planName, orgName, true) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if planOriginalAccess == actors.All { + cmd.ui.Say(T("The plan is already accessible for this org")) + } +} + +func (cmd *EnableServiceAccess) enablePlanForService(serviceName string, planName string) { + cmd.ui.Say(T("Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", map[string]interface{}{"PlanName": terminal.EntityNameColor(planName), "ServiceName": terminal.EntityNameColor(serviceName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + planOriginalAccess, err := cmd.actor.UpdateSinglePlanForService(serviceName, planName, true) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if planOriginalAccess == actors.All { + cmd.ui.Say(T("The plan is already accessible for all orgs")) + } +} + +func (cmd *EnableServiceAccess) enableAllPlansForService(serviceName string) { + cmd.ui.Say(T("Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + allPlansInServicePublic, err := cmd.actor.UpdateAllPlansForService(serviceName, true) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if allPlansInServicePublic { + cmd.ui.Say(T("All plans of the service are already accessible for all orgs")) + } +} + +func (cmd *EnableServiceAccess) enableAllPlansForSingleOrgForService(serviceName string, orgName string) { + cmd.ui.Say(T("Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", map[string]interface{}{"ServiceName": terminal.EntityNameColor(serviceName), "OrgName": terminal.EntityNameColor(orgName), "Username": terminal.EntityNameColor(cmd.config.Username())})) + allPlansWereSet, err := cmd.actor.UpdateOrgForService(serviceName, orgName, true) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if allPlansWereSet { + cmd.ui.Say(T("All plans of the service are already accessible for this org")) + } +} diff --git a/cf/commands/serviceaccess/enable_service_access_test.go b/cf/commands/serviceaccess/enable_service_access_test.go new file mode 100644 index 00000000000..4fecb3e1060 --- /dev/null +++ b/cf/commands/serviceaccess/enable_service_access_test.go @@ -0,0 +1,205 @@ +package serviceaccess_test + +import ( + "errors" + + "github.com/cloudfoundry/cli/cf/actors" + testactor "github.com/cloudfoundry/cli/cf/actors/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/serviceaccess" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("enable-service-access command", func() { + var ( + ui *testterm.FakeUI + actor *testactor.FakeServicePlanActor + requirementsFactory *testreq.FakeReqFactory + tokenRefresher *testapi.FakeAuthenticationRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + actor = &testactor.FakeServicePlanActor{} + requirementsFactory = &testreq.FakeReqFactory{} + tokenRefresher = &testapi.FakeAuthenticationRepository{} + }) + + runCommand := func(args []string) bool { + cmd := NewEnableServiceAccess(ui, configuration.NewRepositoryWithDefaults(), actor, tokenRefresher) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + Expect(runCommand([]string{"foo"})).To(BeFalse()) + }) + + It("fails with usage when it does not recieve any arguments", func() { + requirementsFactory.LoginSuccess = true + runCommand(nil) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("Refreshes the auth token", func() { + runCommand([]string{"service"}) + Expect(tokenRefresher.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when refreshing the auth token fails", func() { + It("fails and returns the error", func() { + tokenRefresher.RefreshTokenError = errors.New("Refreshing went wrong") + runCommand([]string{"service"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Refreshing went wrong"}, + []string{"FAILED"}, + )) + }) + }) + + Context("when the named service exists", func() { + It("returns OK when ran successfully", func() { + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"OK"}, + )) + }) + + It("tells the user if all plans were already public", func() { + actor.UpdateAllPlansForServiceReturns(true, nil) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"All plans of the service", "are already accessible for all orgs"}, + []string{"OK"}, + )) + }) + + It("tells the user the plans are being updated if they weren't all already public", func() { + actor.UpdateAllPlansForServiceReturns(false, nil) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Enabling access to all plans of service service for all orgs as my-user..."}, + []string{"OK"}, + )) + }) + + It("prints an error if updating one of the plans fails", func() { + actor.UpdateAllPlansForServiceReturns(true, errors.New("Kaboom!")) + + Expect(runCommand([]string{"service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Kaboom!"}, + )) + }) + + Context("The user provides a plan", func() { + It("prints an error if the service does not exist", func() { + actor.UpdateSinglePlanForServiceReturns(actors.All, errors.New("could not find service")) + + Expect(runCommand([]string{"-p", "service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find service"}, + )) + }) + + It("tells the user if the plan is already public", func() { + actor.UpdateSinglePlanForServiceReturns(actors.All, nil) + + Expect(runCommand([]string{"-p", "public-service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"The plan is already accessible for all orgs"}, + []string{"OK"}, + )) + }) + + It("tells the user the plan is being updated if it is not public", func() { + actor.UpdateSinglePlanForServiceReturns(actors.None, nil) + + Expect(runCommand([]string{"-p", "private-service-plan", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Enabling access of plan private-service-plan for service service"}, + []string{"OK"}, + )) + }) + }) + + Context("the user provides a plan and org", func() { + It("fails if the org does not exist", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.All, errors.New("could not find org")) + + Expect(runCommand([]string{"-p", "service-plan", "-o", "not-findable-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find org"}, + )) + }) + + It("tells the user if the plan is already public", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.All, nil) + + Expect(runCommand([]string{"-p", "public-service-plan", "-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"The plan is already accessible for this org"}, + []string{"OK"}, + )) + }) + + It("tells the user the plan is being updated if it is not public", func() { + actor.UpdatePlanAndOrgForServiceReturns(actors.None, nil) + + Expect(runCommand([]string{"-p", "private-service-plan", "-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Enabling access to plan private-service-plan of service service for org my-org as"}, + []string{"OK"}, + )) + }) + }) + + Context("the user provides an org", func() { + It("fails if the org does not exist", func() { + actor.UpdateOrgForServiceReturns(false, errors.New("could not find org")) + + Expect(runCommand([]string{"-o", "not-findable-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"could not find org"}, + )) + }) + + It("tells the user if the service's plans are already accessible", func() { + actor.UpdateOrgForServiceReturns(true, nil) + + Expect(runCommand([]string{"-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"All plans of the service are already accessible for this org"}, + []string{"OK"}, + )) + }) + + It("tells the user the service's plans are being updated if it is not accessible", func() { + actor.UpdateOrgForServiceReturns(false, nil) + + Expect(runCommand([]string{"-o", "my-org", "service"})).To(BeTrue()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Enabling access to all plans of service service for the org my-org as"}, + []string{"OK"}, + )) + }) + }) + }) + }) +}) diff --git a/cf/commands/serviceaccess/service_access.go b/cf/commands/serviceaccess/service_access.go new file mode 100644 index 00000000000..db4e513201a --- /dev/null +++ b/cf/commands/serviceaccess/service_access.go @@ -0,0 +1,143 @@ +package serviceaccess + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/cf/actors" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ServiceAccess struct { + ui terminal.UI + config core_config.Reader + actor actors.ServiceActor + tokenRefresher authentication.TokenRefresher +} + +func NewServiceAccess(ui terminal.UI, config core_config.Reader, actor actors.ServiceActor, tokenRefresher authentication.TokenRefresher) (cmd *ServiceAccess) { + return &ServiceAccess{ + ui: ui, + config: config, + actor: actor, + tokenRefresher: tokenRefresher, + } +} + +func (cmd *ServiceAccess) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "service-access", + Description: T("List service access settings"), + Usage: "CF_NAME service-access [-b BROKER] [-e SERVICE] [-o ORG]", + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("b", T("access for plans of a particular broker")), + flag_helpers.NewStringFlag("e", T("access for plans of a particular service offering")), + flag_helpers.NewStringFlag("o", T("plans accessible by a particular organization")), + }, + } +} + +func (cmd *ServiceAccess) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd *ServiceAccess) Run(c *cli.Context) { + _, err := cmd.tokenRefresher.RefreshAuthToken() + if err != nil { + cmd.ui.Failed(err.Error()) + } + + brokerName := c.String("b") + serviceName := c.String("e") + orgName := c.String("o") + + if brokerName != "" && serviceName != "" && orgName != "" { + cmd.ui.Say(T("Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", map[string]interface{}{ + "Broker": terminal.EntityNameColor(brokerName), + "Service": terminal.EntityNameColor(serviceName), + "Organization": terminal.EntityNameColor(orgName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if serviceName != "" && orgName != "" { + cmd.ui.Say(T("Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", map[string]interface{}{ + "Service": terminal.EntityNameColor(serviceName), + "Organization": terminal.EntityNameColor(orgName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if brokerName != "" && orgName != "" { + cmd.ui.Say(T("Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", map[string]interface{}{ + "Broker": terminal.EntityNameColor(brokerName), + "Organization": terminal.EntityNameColor(orgName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if brokerName != "" && serviceName != "" { + cmd.ui.Say(T("Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", map[string]interface{}{ + "Broker": terminal.EntityNameColor(brokerName), + "Service": terminal.EntityNameColor(serviceName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if brokerName != "" { + cmd.ui.Say(T("Getting service access for broker {{.Broker}} as {{.Username}}...", map[string]interface{}{ + "Broker": terminal.EntityNameColor(brokerName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if serviceName != "" { + cmd.ui.Say(T("Getting service access for service {{.Service}} as {{.Username}}...", map[string]interface{}{ + "Service": terminal.EntityNameColor(serviceName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else if orgName != "" { + cmd.ui.Say(T("Getting service access for organization {{.Organization}} as {{.Username}}...", map[string]interface{}{ + "Organization": terminal.EntityNameColor(orgName), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } else { + cmd.ui.Say(T("Getting service access as {{.Username}}...", map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username())})) + } + + brokers, err := cmd.actor.FilterBrokers(brokerName, serviceName, orgName) + if err != nil { + cmd.ui.Failed(T("Failed fetching service brokers.\n{{.Error}}", map[string]interface{}{"Error": err})) + return + } + cmd.printTable(brokers) +} + +func (cmd ServiceAccess) printTable(brokers []models.ServiceBroker) { + for _, serviceBroker := range brokers { + cmd.ui.Say(fmt.Sprintf(T("broker: {{.Name}}", map[string]interface{}{"Name": serviceBroker.Name}))) + + table := terminal.NewTable(cmd.ui, []string{"", T("service"), T("plan"), T("access"), T("orgs")}) + for _, service := range serviceBroker.Services { + if len(service.Plans) > 0 { + for _, plan := range service.Plans { + table.Add("", service.Label, plan.Name, cmd.formatAccess(plan.Public, plan.OrgNames), strings.Join(plan.OrgNames, ",")) + } + } else { + table.Add("", service.Label, "", "", "") + } + } + table.Print() + + cmd.ui.Say("") + } + return +} + +func (cmd ServiceAccess) formatAccess(public bool, orgNames []string) string { + if public { + return T("all") + } + if len(orgNames) > 0 { + return T("limited") + } + return T("none") +} diff --git a/cf/commands/serviceaccess/service_access_test.go b/cf/commands/serviceaccess/service_access_test.go new file mode 100644 index 00000000000..0d14e6526bf --- /dev/null +++ b/cf/commands/serviceaccess/service_access_test.go @@ -0,0 +1,196 @@ +package serviceaccess_test + +import ( + "errors" + + testactor "github.com/cloudfoundry/cli/cf/actors/fakes" + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/serviceaccess" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("service-access command", func() { + var ( + ui *testterm.FakeUI + actor *testactor.FakeServiceActor + requirementsFactory *testreq.FakeReqFactory + serviceBroker1 models.ServiceBroker + serviceBroker2 models.ServiceBroker + tokenRefresher *testapi.FakeAuthenticationRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + actor = &testactor.FakeServiceActor{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + tokenRefresher = &testapi.FakeAuthenticationRepository{} + }) + + runCommand := func(args ...string) bool { + cmd := NewServiceAccess(ui, testconfig.NewRepositoryWithDefaults(), actor, tokenRefresher) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when logged in", func() { + BeforeEach(func() { + serviceBroker1 = models.ServiceBroker{ + Guid: "broker1", + Name: "brokername1", + Services: []models.ServiceOffering{ + { + ServiceOfferingFields: models.ServiceOfferingFields{Label: "my-service-1"}, + Plans: []models.ServicePlanFields{ + {Name: "beep", Public: true}, + {Name: "burp", Public: false}, + {Name: "boop", Public: false, OrgNames: []string{"fwip", "brzzt"}}, + }, + }, + { + ServiceOfferingFields: models.ServiceOfferingFields{Label: "my-service-2"}, + Plans: []models.ServicePlanFields{ + {Name: "petaloideous-noncelebration", Public: false}, + }, + }, + }, + } + serviceBroker2 = models.ServiceBroker{ + Guid: "broker2", + Name: "brokername2", + Services: []models.ServiceOffering{ + {ServiceOfferingFields: models.ServiceOfferingFields{Label: "my-service-3"}}, + }, + } + + actor.FilterBrokersReturns([]models.ServiceBroker{ + serviceBroker1, + serviceBroker2, + }, + nil, + ) + }) + + It("refreshes the auth token", func() { + runCommand() + Expect(tokenRefresher.RefreshTokenCalled).To(BeTrue()) + }) + + Context("when refreshing the auth token fails", func() { + It("fails and returns the error", func() { + tokenRefresher.RefreshTokenError = errors.New("Refreshing went wrong") + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Refreshing went wrong"}, + []string{"FAILED"}, + )) + }) + }) + + Context("When no flags are provided", func() { + It("tells the user it is obtaining the service access", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access as", "my-user"}, + )) + }) + + It("prints all of the brokers", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"broker: brokername1"}, + []string{"service", "plan", "access", "orgs"}, + []string{"my-service-1", "beep", "all"}, + []string{"my-service-1", "burp", "none"}, + []string{"my-service-1", "boop", "limited", "fwip", "brzzt"}, + []string{"my-service-2", "petaloideous-noncelebration"}, + []string{"broker: brokername2"}, + []string{"service", "plan", "access", "orgs"}, + []string{"my-service-3"}, + )) + }) + }) + + Context("When the broker flag is provided", func() { + It("tells the user it is obtaining the services access for a particular broker", func() { + runCommand("-b", "brokername1") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for broker brokername1 as", "my-user"}, + )) + }) + }) + + Context("when the service flag is provided", func() { + It("tells the user it is obtaining the service access for a particular service", func() { + runCommand("-e", "my-service-1") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for service my-service-1 as", "my-user"}, + )) + }) + }) + + Context("when the org flag is provided", func() { + It("tells the user it is obtaining the service access for a particular org", func() { + runCommand("-o", "fwip") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for organization fwip as", "my-user"}, + )) + }) + }) + + Context("when the broker and service flag are both provided", func() { + It("tells the user it is obtaining the service access for a particular broker and service", func() { + runCommand("-b", "brokername1", "-e", "my-service-1") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for broker brokername1", "and service my-service-1", "as", "my-user"}, + )) + }) + }) + + Context("when the broker and org name are both provided", func() { + It("tells the user it is obtaining the service access for a particular broker and org", func() { + runCommand("-b", "brokername1", "-o", "fwip") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for broker brokername1", "and organization fwip", "as", "my-user"}, + )) + }) + }) + + Context("when the service and org name are both provided", func() { + It("tells the user it is obtaining the service access for a particular service and org", func() { + runCommand("-e", "my-service-1", "-o", "fwip") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for service my-service-1", "and organization fwip", "as", "my-user"}, + )) + }) + }) + + Context("when all flags are provided", func() { + It("tells the user it is filtering on all options", func() { + runCommand("-b", "brokername1", "-e", "my-service-1", "-o", "fwip") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service access", "for broker brokername1", "and service my-service-1", "and organization fwip", "as", "my-user"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/serviceaccess/serviceaccess_suite_test.go b/cf/commands/serviceaccess/serviceaccess_suite_test.go new file mode 100644 index 00000000000..47f699e9732 --- /dev/null +++ b/cf/commands/serviceaccess/serviceaccess_suite_test.go @@ -0,0 +1,20 @@ +package serviceaccess_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestServiceAccess(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Service Access Suite") +} diff --git a/cf/commands/serviceauthtoken/create_service_auth_token.go b/cf/commands/serviceauthtoken/create_service_auth_token.go new file mode 100644 index 00000000000..5e3e9e74421 --- /dev/null +++ b/cf/commands/serviceauthtoken/create_service_auth_token.go @@ -0,0 +1,66 @@ +package serviceauthtoken + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateServiceAuthTokenFields struct { + ui terminal.UI + config core_config.Reader + authTokenRepo api.ServiceAuthTokenRepository +} + +func NewCreateServiceAuthToken(ui terminal.UI, config core_config.Reader, authTokenRepo api.ServiceAuthTokenRepository) (cmd CreateServiceAuthTokenFields) { + cmd.ui = ui + cmd.config = config + cmd.authTokenRepo = authTokenRepo + return +} + +func (cmd CreateServiceAuthTokenFields) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-service-auth-token", + Description: T("Create a service auth token"), + Usage: T("CF_NAME create-service-auth-token LABEL PROVIDER TOKEN"), + } +} + +func (cmd CreateServiceAuthTokenFields) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + return + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd CreateServiceAuthTokenFields) Run(c *cli.Context) { + cmd.ui.Say(T("Creating service auth token as {{.CurrentUser}}...", + map[string]interface{}{ + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + serviceAuthTokenRepo := models.ServiceAuthTokenFields{ + Label: c.Args()[0], + Provider: c.Args()[1], + Token: c.Args()[2], + } + + apiErr := cmd.authTokenRepo.Create(serviceAuthTokenRepo) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/serviceauthtoken/create_service_auth_token_test.go b/cf/commands/serviceauthtoken/create_service_auth_token_test.go new file mode 100644 index 00000000000..7205e9fca86 --- /dev/null +++ b/cf/commands/serviceauthtoken/create_service_auth_token_test.go @@ -0,0 +1,71 @@ +package serviceauthtoken_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/serviceauthtoken" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-service-auth-token command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + authTokenRepo *testapi.FakeAuthTokenRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + authTokenRepo = &testapi.FakeAuthTokenRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewCreateServiceAuthToken(ui, configRepo, authTokenRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not invoked with exactly three args", func() { + requirementsFactory.LoginSuccess = true + runCommand("whoops", "i-accidentally-an-arg") + + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("just", "enough", "args")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("creates a service auth token, obviously", func() { + runCommand("a label", "a provider", "a value") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service auth token as", "my-user"}, + []string{"OK"}, + )) + + authToken := models.ServiceAuthTokenFields{} + authToken.Label = "a label" + authToken.Provider = "a provider" + authToken.Token = "a value" + Expect(authTokenRepo.CreatedServiceAuthTokenFields).To(Equal(authToken)) + }) + }) +}) diff --git a/cf/commands/serviceauthtoken/delete_service_auth_token.go b/cf/commands/serviceauthtoken/delete_service_auth_token.go new file mode 100644 index 00000000000..eb080c5cc57 --- /dev/null +++ b/cf/commands/serviceauthtoken/delete_service_auth_token.go @@ -0,0 +1,83 @@ +package serviceauthtoken + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteServiceAuthTokenFields struct { + ui terminal.UI + config core_config.Reader + authTokenRepo api.ServiceAuthTokenRepository +} + +func NewDeleteServiceAuthToken(ui terminal.UI, config core_config.Reader, authTokenRepo api.ServiceAuthTokenRepository) (cmd DeleteServiceAuthTokenFields) { + cmd.ui = ui + cmd.config = config + cmd.authTokenRepo = authTokenRepo + return +} + +func (cmd DeleteServiceAuthTokenFields) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-service-auth-token", + Description: T("Delete a service auth token"), + Usage: T("CF_NAME delete-service-auth-token LABEL PROVIDER [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd DeleteServiceAuthTokenFields) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + return + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd DeleteServiceAuthTokenFields) Run(c *cli.Context) { + tokenLabel := c.Args()[0] + tokenProvider := c.Args()[1] + + if c.Bool("f") == false { + if !cmd.ui.ConfirmDelete(T("service auth token"), fmt.Sprintf("%s %s", tokenLabel, tokenProvider)) { + return + } + } + + cmd.ui.Say(T("Deleting service auth token as {{.CurrentUser}}", + map[string]interface{}{ + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + token, apiErr := cmd.authTokenRepo.FindByLabelAndProvider(tokenLabel, tokenProvider) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Service Auth Token {{.Label}} {{.Provider}} does not exist.", map[string]interface{}{"Label": tokenLabel, "Provider": tokenProvider})) + return + default: + cmd.ui.Failed(apiErr.Error()) + } + + apiErr = cmd.authTokenRepo.Delete(token) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/serviceauthtoken/delete_service_auth_token_test.go b/cf/commands/serviceauthtoken/delete_service_auth_token_test.go new file mode 100644 index 00000000000..6a0ba65de5f --- /dev/null +++ b/cf/commands/serviceauthtoken/delete_service_auth_token_test.go @@ -0,0 +1,129 @@ +package serviceauthtoken_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/serviceauthtoken" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-service-auth-token command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + authTokenRepo *testapi.FakeAuthTokenRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + authTokenRepo = &testapi.FakeAuthTokenRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteServiceAuthToken(ui, configRepo, authTokenRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when fewer than two arguments are given", func() { + runCommand("yurp") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).To(BeFalse()) + }) + }) + + Context("when the service auth token exists", func() { + BeforeEach(func() { + authTokenRepo.FindByLabelAndProviderServiceAuthTokenFields = models.ServiceAuthTokenFields{ + Guid: "the-guid", + Label: "a label", + Provider: "a provider", + } + }) + + It("deletes the service auth token", func() { + runCommand("a label", "a provider") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service auth token as", "my-user"}, + []string{"OK"}, + )) + + Expect(authTokenRepo.FindByLabelAndProviderLabel).To(Equal("a label")) + Expect(authTokenRepo.FindByLabelAndProviderProvider).To(Equal("a provider")) + Expect(authTokenRepo.DeletedServiceAuthTokenFields.Guid).To(Equal("the-guid")) + }) + + It("does nothing when the user does not confirm", func() { + ui.Inputs = []string{"nope"} + runCommand("a label", "a provider") + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete", "service auth token", "a label", "a provider"}, + )) + Expect(ui.Outputs).To(BeEmpty()) + Expect(authTokenRepo.DeletedServiceAuthTokenFields).To(Equal(models.ServiceAuthTokenFields{})) + }) + + It("does not prompt the user when the -f flag is given", func() { + ui.Inputs = []string{} + runCommand("-f", "a label", "a provider") + + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting"}, + []string{"OK"}, + )) + + Expect(authTokenRepo.DeletedServiceAuthTokenFields.Guid).To(Equal("the-guid")) + }) + }) + + Context("when the service auth token does not exist", func() { + BeforeEach(func() { + authTokenRepo.FindByLabelAndProviderApiResponse = errors.NewModelNotFoundError("Service Auth Token", "") + }) + + It("warns the user when the specified service auth token does not exist", func() { + runCommand("a label", "a provider") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service auth token as", "my-user"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"does not exist"})) + }) + }) + + Context("when there is an error deleting the service auth token", func() { + BeforeEach(func() { + authTokenRepo.FindByLabelAndProviderApiResponse = errors.New("OH NOES") + }) + + It("shows the user an error", func() { + runCommand("a label", "a provider") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service auth token as", "my-user"}, + []string{"FAILED"}, + []string{"OH NOES"}, + )) + }) + }) +}) diff --git a/cf/commands/serviceauthtoken/service_auth_tokens.go b/cf/commands/serviceauthtoken/service_auth_tokens.go new file mode 100644 index 00000000000..f5185a640d9 --- /dev/null +++ b/cf/commands/serviceauthtoken/service_auth_tokens.go @@ -0,0 +1,64 @@ +package serviceauthtoken + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListServiceAuthTokens struct { + ui terminal.UI + config core_config.Reader + authTokenRepo api.ServiceAuthTokenRepository +} + +func NewListServiceAuthTokens(ui terminal.UI, config core_config.Reader, authTokenRepo api.ServiceAuthTokenRepository) (cmd ListServiceAuthTokens) { + cmd.ui = ui + cmd.config = config + cmd.authTokenRepo = authTokenRepo + return +} + +func (cmd ListServiceAuthTokens) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "service-auth-tokens", + Description: T("List service auth tokens"), + Usage: T("CF_NAME service-auth-tokens"), + } +} + +func (cmd ListServiceAuthTokens) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd ListServiceAuthTokens) Run(c *cli.Context) { + cmd.ui.Say(T("Getting service auth tokens as {{.CurrentUser}}...", + map[string]interface{}{ + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + authTokens, apiErr := cmd.authTokenRepo.FindAll() + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("label"), T("provider")}) + + for _, authToken := range authTokens { + table.Add(authToken.Label, authToken.Provider) + } + + table.Print() +} diff --git a/cf/commands/serviceauthtoken/service_auth_tokens_test.go b/cf/commands/serviceauthtoken/service_auth_tokens_test.go new file mode 100644 index 00000000000..7a8c8b0aeb4 --- /dev/null +++ b/cf/commands/serviceauthtoken/service_auth_tokens_test.go @@ -0,0 +1,81 @@ +package serviceauthtoken_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/serviceauthtoken" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func callListServiceAuthTokens(requirementsFactory *testreq.FakeReqFactory, authTokenRepo *testapi.FakeAuthTokenRepo) (ui *testterm.FakeUI) { + ui = &testterm.FakeUI{} + + config := testconfig.NewRepositoryWithDefaults() + + cmd := NewListServiceAuthTokens(ui, config, authTokenRepo) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + return +} + +var _ = Describe("service-auth-tokens command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + authTokenRepo *testapi.FakeAuthTokenRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + authTokenRepo = &testapi.FakeAuthTokenRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewListServiceAuthTokens(ui, configRepo, authTokenRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + Expect(runCommand()).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in and some service auth tokens exist", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + authTokenRepo.FindAllAuthTokens = []models.ServiceAuthTokenFields{ + models.ServiceAuthTokenFields{Label: "a label", Provider: "a provider"}, + models.ServiceAuthTokenFields{Label: "a second label", Provider: "a second provider"}, + } + }) + + It("shows you the service auth tokens", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service auth tokens as", "my-user"}, + []string{"OK"}, + []string{"label", "provider"}, + []string{"a label", "a provider"}, + []string{"a second label", "a second provider"}, + )) + }) + }) +}) diff --git a/cf/commands/serviceauthtoken/serviceauthtoken_suite_test.go b/cf/commands/serviceauthtoken/serviceauthtoken_suite_test.go new file mode 100644 index 00000000000..6df621cbcec --- /dev/null +++ b/cf/commands/serviceauthtoken/serviceauthtoken_suite_test.go @@ -0,0 +1,19 @@ +package serviceauthtoken_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestServiceauthtoken(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Serviceauthtoken Suite") +} diff --git a/cf/commands/serviceauthtoken/update_service_auth_token.go b/cf/commands/serviceauthtoken/update_service_auth_token.go new file mode 100644 index 00000000000..e293e9a111b --- /dev/null +++ b/cf/commands/serviceauthtoken/update_service_auth_token.go @@ -0,0 +1,63 @@ +package serviceauthtoken + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateServiceAuthTokenFields struct { + ui terminal.UI + config core_config.Reader + authTokenRepo api.ServiceAuthTokenRepository +} + +func NewUpdateServiceAuthToken(ui terminal.UI, config core_config.Reader, authTokenRepo api.ServiceAuthTokenRepository) (cmd UpdateServiceAuthTokenFields) { + cmd.ui = ui + cmd.config = config + cmd.authTokenRepo = authTokenRepo + return +} + +func (cmd UpdateServiceAuthTokenFields) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-service-auth-token", + Description: T("Update a service auth token"), + Usage: T("CF_NAME update-service-auth-token LABEL PROVIDER TOKEN"), + } +} + +func (cmd UpdateServiceAuthTokenFields) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + } + return +} + +func (cmd UpdateServiceAuthTokenFields) Run(c *cli.Context) { + cmd.ui.Say(T("Updating service auth token as {{.CurrentUser}}...", map[string]interface{}{"CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) + + serviceAuthToken, apiErr := cmd.authTokenRepo.FindByLabelAndProvider(c.Args()[0], c.Args()[1]) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + serviceAuthToken.Token = c.Args()[2] + + apiErr = cmd.authTokenRepo.Update(serviceAuthToken) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/serviceauthtoken/update_service_auth_token_test.go b/cf/commands/serviceauthtoken/update_service_auth_token_test.go new file mode 100644 index 00000000000..b84bdac9685 --- /dev/null +++ b/cf/commands/serviceauthtoken/update_service_auth_token_test.go @@ -0,0 +1,79 @@ +package serviceauthtoken_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/serviceauthtoken" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("update-service-auth-token command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + authTokenRepo *testapi.FakeAuthTokenRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + authTokenRepo = &testapi.FakeAuthTokenRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewUpdateServiceAuthToken(ui, configRepo, authTokenRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not provided exactly three args", func() { + requirementsFactory.LoginSuccess = true + runCommand("some-token-label", "a-provider") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("label", "provider", "token")).To(BeFalse()) + }) + }) + + Context("when logged in and the service auth token exists", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + foundAuthToken := models.ServiceAuthTokenFields{} + foundAuthToken.Guid = "found-auth-token-guid" + foundAuthToken.Label = "found label" + foundAuthToken.Provider = "found provider" + authTokenRepo.FindByLabelAndProviderServiceAuthTokenFields = foundAuthToken + }) + + It("updates the service auth token with the provided args", func() { + runCommand("a label", "a provider", "a value") + + expectedAuthToken := models.ServiceAuthTokenFields{} + expectedAuthToken.Guid = "found-auth-token-guid" + expectedAuthToken.Label = "found label" + expectedAuthToken.Provider = "found provider" + expectedAuthToken.Token = "a value" + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating service auth token as", "my-user"}, + []string{"OK"}, + )) + + Expect(authTokenRepo.FindByLabelAndProviderLabel).To(Equal("a label")) + Expect(authTokenRepo.FindByLabelAndProviderProvider).To(Equal("a provider")) + Expect(authTokenRepo.UpdatedServiceAuthTokenFields).To(Equal(expectedAuthToken)) + Expect(authTokenRepo.UpdatedServiceAuthTokenFields).To(Equal(expectedAuthToken)) + }) + }) +}) diff --git a/cf/commands/servicebroker/create_service_broker.go b/cf/commands/servicebroker/create_service_broker.go new file mode 100644 index 00000000000..209015aa825 --- /dev/null +++ b/cf/commands/servicebroker/create_service_broker.go @@ -0,0 +1,63 @@ +package servicebroker + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateServiceBroker struct { + ui terminal.UI + config core_config.Reader + serviceBrokerRepo api.ServiceBrokerRepository +} + +func NewCreateServiceBroker(ui terminal.UI, config core_config.Reader, serviceBrokerRepo api.ServiceBrokerRepository) (cmd CreateServiceBroker) { + cmd.ui = ui + cmd.config = config + cmd.serviceBrokerRepo = serviceBrokerRepo + return +} + +func (cmd CreateServiceBroker) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-service-broker", + Description: T("Create a service broker"), + Usage: T("CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL"), + } +} + +func (cmd CreateServiceBroker) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + + if len(c.Args()) != 4 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + + return +} + +func (cmd CreateServiceBroker) Run(c *cli.Context) { + name := c.Args()[0] + username := c.Args()[1] + password := c.Args()[2] + url := c.Args()[3] + + cmd.ui.Say(T("Creating service broker {{.Name}} as {{.Username}}...", + map[string]interface{}{ + "Name": terminal.EntityNameColor(name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr := cmd.serviceBrokerRepo.Create(name, url, username, password) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/servicebroker/create_service_broker_test.go b/cf/commands/servicebroker/create_service_broker_test.go new file mode 100644 index 00000000000..76870b44360 --- /dev/null +++ b/cf/commands/servicebroker/create_service_broker_test.go @@ -0,0 +1,68 @@ +package servicebroker_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/servicebroker" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-service-broker command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + serviceBrokerRepo *testapi.FakeServiceBrokerRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + serviceBrokerRepo = &testapi.FakeServiceBrokerRepo{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewCreateServiceBroker(ui, configRepo, serviceBrokerRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when called without exactly four args", func() { + requirementsFactory.LoginSuccess = true + runCommand("whoops", "not-enough", "args") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("Just", "Enough", "Args", "Provided")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("creates a service broker, obviously", func() { + runCommand("my-broker", "my-username", "my-password", "http://example.com") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating service broker", "my-broker", "my-user"}, + []string{"OK"}, + )) + + Expect(serviceBrokerRepo.CreateName).To(Equal("my-broker")) + Expect(serviceBrokerRepo.CreateUrl).To(Equal("http://example.com")) + Expect(serviceBrokerRepo.CreateUsername).To(Equal("my-username")) + Expect(serviceBrokerRepo.CreatePassword).To(Equal("my-password")) + }) + }) +}) diff --git a/cf/commands/servicebroker/delete_service_broker.go b/cf/commands/servicebroker/delete_service_broker.go new file mode 100644 index 00000000000..1cae04ce644 --- /dev/null +++ b/cf/commands/servicebroker/delete_service_broker.go @@ -0,0 +1,80 @@ +package servicebroker + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteServiceBroker struct { + ui terminal.UI + config core_config.Reader + repo api.ServiceBrokerRepository +} + +func NewDeleteServiceBroker(ui terminal.UI, config core_config.Reader, repo api.ServiceBrokerRepository) (cmd DeleteServiceBroker) { + cmd.ui = ui + cmd.config = config + cmd.repo = repo + return +} + +func (cmd DeleteServiceBroker) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-service-broker", + Description: T("Delete a service broker"), + Usage: T("CF_NAME delete-service-broker SERVICE_BROKER [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd DeleteServiceBroker) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd DeleteServiceBroker) Run(c *cli.Context) { + brokerName := c.Args()[0] + if !c.Bool("f") && !cmd.ui.ConfirmDelete(T("service-broker"), brokerName) { + return + } + + cmd.ui.Say(T("Deleting service broker {{.Name}} as {{.Username}}...", + map[string]interface{}{ + "Name": terminal.EntityNameColor(brokerName), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + broker, apiErr := cmd.repo.FindByName(brokerName) + + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Service Broker {{.Name}} does not exist.", map[string]interface{}{"Name": brokerName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + apiErr = cmd.repo.Delete(broker.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + return +} diff --git a/cf/commands/servicebroker/delete_service_broker_test.go b/cf/commands/servicebroker/delete_service_broker_test.go new file mode 100644 index 00000000000..3b7c25ee28d --- /dev/null +++ b/cf/commands/servicebroker/delete_service_broker_test.go @@ -0,0 +1,104 @@ +package servicebroker_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/servicebroker" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-service-broker command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + brokerRepo *testapi.FakeServiceBrokerRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + brokerRepo = &testapi.FakeServiceBrokerRepo{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteServiceBroker(ui, configRepo, brokerRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when called without a broker's name", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("-f", "my-broker")).To(BeFalse()) + }) + }) + + Context("when the service broker exists", func() { + BeforeEach(func() { + brokerRepo.FindByNameServiceBroker = models.ServiceBroker{ + Name: "service-broker-to-delete", + Guid: "service-broker-to-delete-guid", + } + }) + + It("deletes the service broker with the given name", func() { + runCommand("service-broker-to-delete") + + Expect(brokerRepo.FindByNameName).To(Equal("service-broker-to-delete")) + Expect(brokerRepo.DeletedServiceBrokerGuid).To(Equal("service-broker-to-delete-guid")) + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the service-broker service-broker-to-delete"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service broker", "service-broker-to-delete", "my-user"}, + []string{"OK"}, + )) + }) + + It("does not prompt when the -f flag is provided", func() { + runCommand("-f", "service-broker-to-delete") + + Expect(brokerRepo.FindByNameName).To(Equal("service-broker-to-delete")) + Expect(brokerRepo.DeletedServiceBrokerGuid).To(Equal("service-broker-to-delete-guid")) + + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service broker", "service-broker-to-delete", "my-user"}, + []string{"OK"}, + )) + }) + }) + + Context("when the service broker does not exist", func() { + BeforeEach(func() { + brokerRepo.FindByNameNotFound = true + }) + + It("warns the user", func() { + ui.Inputs = []string{} + runCommand("-f", "service-broker-to-delete") + + Expect(brokerRepo.FindByNameName).To(Equal("service-broker-to-delete")) + Expect(brokerRepo.DeletedServiceBrokerGuid).To(Equal("")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting service broker", "service-broker-to-delete"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"service-broker-to-delete", "does not exist"})) + }) + }) +}) diff --git a/cf/commands/servicebroker/rename_service_broker.go b/cf/commands/servicebroker/rename_service_broker.go new file mode 100644 index 00000000000..a211884196b --- /dev/null +++ b/cf/commands/servicebroker/rename_service_broker.go @@ -0,0 +1,67 @@ +package servicebroker + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameServiceBroker struct { + ui terminal.UI + config core_config.Reader + repo api.ServiceBrokerRepository +} + +func NewRenameServiceBroker(ui terminal.UI, config core_config.Reader, repo api.ServiceBrokerRepository) (cmd RenameServiceBroker) { + cmd.ui = ui + cmd.config = config + cmd.repo = repo + return +} + +func (cmd RenameServiceBroker) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename-service-broker", + Description: T("Rename a service broker"), + Usage: T("CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER"), + } +} + +func (cmd RenameServiceBroker) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + + return +} + +func (cmd RenameServiceBroker) Run(c *cli.Context) { + serviceBroker, apiErr := cmd.repo.FindByName(c.Args()[0]) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Say(T("Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + map[string]interface{}{ + "OldName": terminal.EntityNameColor(serviceBroker.Name), + "NewName": terminal.EntityNameColor(c.Args()[1]), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + newName := c.Args()[1] + + apiErr = cmd.repo.Rename(serviceBroker.Guid, newName) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/servicebroker/rename_service_broker_test.go b/cf/commands/servicebroker/rename_service_broker_test.go new file mode 100644 index 00000000000..ec3d1166b9b --- /dev/null +++ b/cf/commands/servicebroker/rename_service_broker_test.go @@ -0,0 +1,71 @@ +package servicebroker_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/servicebroker" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("rename-service-broker command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + serviceBrokerRepo *testapi.FakeServiceBrokerRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + serviceBrokerRepo = &testapi.FakeServiceBrokerRepo{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewRenameServiceBroker(ui, configRepo, serviceBrokerRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when not invoked with exactly two args", func() { + requirementsFactory.LoginSuccess = true + runCommand("welp") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("okay", "DO---IIIIT")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + broker := models.ServiceBroker{} + broker.Name = "my-found-broker" + broker.Guid = "my-found-broker-guid" + serviceBrokerRepo.FindByNameServiceBroker = broker + }) + + It("renames the given service broker", func() { + runCommand("my-broker", "my-new-broker") + Expect(serviceBrokerRepo.FindByNameName).To(Equal("my-broker")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming service broker", "my-found-broker", "my-new-broker", "my-user"}, + []string{"OK"}, + )) + + Expect(serviceBrokerRepo.RenamedServiceBrokerGuid).To(Equal("my-found-broker-guid")) + Expect(serviceBrokerRepo.RenamedServiceBrokerName).To(Equal("my-new-broker")) + }) + }) +}) diff --git a/cf/commands/servicebroker/service_brokers.go b/cf/commands/servicebroker/service_brokers.go new file mode 100644 index 00000000000..e184ea0faed --- /dev/null +++ b/cf/commands/servicebroker/service_brokers.go @@ -0,0 +1,66 @@ +package servicebroker + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListServiceBrokers struct { + ui terminal.UI + config core_config.Reader + repo api.ServiceBrokerRepository +} + +func NewListServiceBrokers(ui terminal.UI, config core_config.Reader, repo api.ServiceBrokerRepository) (cmd ListServiceBrokers) { + cmd.ui = ui + cmd.config = config + cmd.repo = repo + return +} + +func (cmd ListServiceBrokers) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "service-brokers", + Description: T("List service brokers"), + Usage: "CF_NAME service-brokers", + } +} + +func (cmd ListServiceBrokers) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd ListServiceBrokers) Run(c *cli.Context) { + cmd.ui.Say(T("Getting service brokers as {{.Username}}...\n", + map[string]interface{}{ + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + table := cmd.ui.Table([]string{T("name"), T("url")}) + foundBrokers := false + apiErr := cmd.repo.ListServiceBrokers(func(serviceBroker models.ServiceBroker) bool { + table.Add(serviceBroker.Name, serviceBroker.Url) + foundBrokers = true + return true + }) + table.Print() + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching service brokers.\n{{.Error}}", map[string]interface{}{"Error": apiErr})) + return + } + + if !foundBrokers { + cmd.ui.Say(T("No service brokers found")) + } +} diff --git a/cf/commands/servicebroker/service_brokers_test.go b/cf/commands/servicebroker/service_brokers_test.go new file mode 100644 index 00000000000..2b4b7ebcf9e --- /dev/null +++ b/cf/commands/servicebroker/service_brokers_test.go @@ -0,0 +1,100 @@ +package servicebroker_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/servicebroker" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +func callListServiceBrokers(args []string, serviceBrokerRepo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { + ui = &testterm.FakeUI{} + config := testconfig.NewRepositoryWithDefaults() + cmd := NewListServiceBrokers(ui, config, serviceBrokerRepo) + testcmd.RunCommand(cmd, args, &testreq.FakeReqFactory{}) + + return +} + +var _ = Describe("service-brokers command", func() { + var ( + ui *testterm.FakeUI + config core_config.Repository + cmd ListServiceBrokers + repo *testapi.FakeServiceBrokerRepo + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config = testconfig.NewRepositoryWithDefaults() + repo = &testapi.FakeServiceBrokerRepo{} + cmd = NewListServiceBrokers(ui, config, repo) + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + Describe("login requirements", func() { + It("fails if the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(testcmd.RunCommand(cmd, []string{}, requirementsFactory)).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(testcmd.RunCommand(cmd, []string{"blahblah"}, requirementsFactory)).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + It("lists service brokers", func() { + repo.ServiceBrokers = []models.ServiceBroker{models.ServiceBroker{ + Name: "service-broker-to-list-a", + Guid: "service-broker-to-list-guid-a", + Url: "http://service-a-url.com", + }, models.ServiceBroker{ + Name: "service-broker-to-list-b", + Guid: "service-broker-to-list-guid-b", + Url: "http://service-b-url.com", + }, models.ServiceBroker{ + Name: "service-broker-to-list-c", + Guid: "service-broker-to-list-guid-c", + Url: "http://service-c-url.com", + }} + + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service brokers as", "my-user"}, + []string{"name", "url"}, + []string{"service-broker-to-list-a", "http://service-a-url.com"}, + []string{"service-broker-to-list-b", "http://service-b-url.com"}, + []string{"service-broker-to-list-c", "http://service-c-url.com"}, + )) + }) + + It("says when no service brokers were found", func() { + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service brokers as", "my-user"}, + []string{"No service brokers found"}, + )) + }) + + It("reports errors when listing service brokers", func() { + repo.ListErr = true + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting service brokers as ", "my-user"}, + []string{"FAILED"}, + )) + }) +}) diff --git a/cf/commands/servicebroker/servicebroker_suite_test.go b/cf/commands/servicebroker/servicebroker_suite_test.go new file mode 100644 index 00000000000..f5abe52b4c7 --- /dev/null +++ b/cf/commands/servicebroker/servicebroker_suite_test.go @@ -0,0 +1,19 @@ +package servicebroker_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestServicebroker(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Servicebroker Suite") +} diff --git a/cf/commands/servicebroker/update_service_broker.go b/cf/commands/servicebroker/update_service_broker.go new file mode 100644 index 00000000000..98950bef05f --- /dev/null +++ b/cf/commands/servicebroker/update_service_broker.go @@ -0,0 +1,68 @@ +package servicebroker + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateServiceBroker struct { + ui terminal.UI + config core_config.Reader + repo api.ServiceBrokerRepository +} + +func NewUpdateServiceBroker(ui terminal.UI, config core_config.Reader, repo api.ServiceBrokerRepository) (cmd UpdateServiceBroker) { + cmd.ui = ui + cmd.config = config + cmd.repo = repo + return +} + +func (cmd UpdateServiceBroker) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-service-broker", + Description: T("Update a service broker"), + Usage: T("CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL"), + } +} + +func (cmd UpdateServiceBroker) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 4 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + + return +} + +func (cmd UpdateServiceBroker) Run(c *cli.Context) { + serviceBroker, apiErr := cmd.repo.FindByName(c.Args()[0]) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Say(T("Updating service broker {{.Name}} as {{.Username}}...", + map[string]interface{}{ + "Name": terminal.EntityNameColor(serviceBroker.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + serviceBroker.Username = c.Args()[1] + serviceBroker.Password = c.Args()[2] + serviceBroker.Url = c.Args()[3] + + apiErr = cmd.repo.Update(serviceBroker) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/servicebroker/update_service_broker_test.go b/cf/commands/servicebroker/update_service_broker_test.go new file mode 100644 index 00000000000..28eec2387fa --- /dev/null +++ b/cf/commands/servicebroker/update_service_broker_test.go @@ -0,0 +1,80 @@ +package servicebroker_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/servicebroker" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("update-service-broker command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + serviceBrokerRepo *testapi.FakeServiceBrokerRepo + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + serviceBrokerRepo = &testapi.FakeServiceBrokerRepo{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewUpdateServiceBroker(ui, configRepo, serviceBrokerRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when invoked without exactly four args", func() { + requirementsFactory.LoginSuccess = true + + runCommand("arg1", "arg2", "arg3") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("heeeeeeey", "yooouuuuuuu", "guuuuuuuuys", "ヾ(@*ー⌒ー*@)ノ")).To(BeFalse()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + broker := models.ServiceBroker{} + broker.Name = "my-found-broker" + broker.Guid = "my-found-broker-guid" + serviceBrokerRepo.FindByNameServiceBroker = broker + }) + + It("updates the service broker with the provided properties", func() { + runCommand("my-broker", "new-username", "new-password", "new-url") + + Expect(serviceBrokerRepo.FindByNameName).To(Equal("my-broker")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating service broker", "my-found-broker", "my-user"}, + []string{"OK"}, + )) + + expectedServiceBroker := models.ServiceBroker{} + expectedServiceBroker.Name = "my-found-broker" + expectedServiceBroker.Username = "new-username" + expectedServiceBroker.Password = "new-password" + expectedServiceBroker.Url = "new-url" + expectedServiceBroker.Guid = "my-found-broker-guid" + + Expect(serviceBrokerRepo.UpdatedServiceBroker).To(Equal(expectedServiceBroker)) + }) + }) +}) diff --git a/cf/commands/space/create_space.go b/cf/commands/space/create_space.go new file mode 100644 index 00000000000..d7ad91ff830 --- /dev/null +++ b/cf/commands/space/create_space.go @@ -0,0 +1,140 @@ +package space + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateSpace struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository + orgRepo organizations.OrganizationRepository + userRepo api.UserRepository + spaceRoleSetter user.SpaceRoleSetter + spaceQuotaRepo space_quotas.SpaceQuotaRepository +} + +func NewCreateSpace(ui terminal.UI, config core_config.Reader, spaceRoleSetter user.SpaceRoleSetter, spaceRepo spaces.SpaceRepository, orgRepo organizations.OrganizationRepository, userRepo api.UserRepository, spaceQuotaRepo space_quotas.SpaceQuotaRepository) (cmd CreateSpace) { + cmd.ui = ui + cmd.config = config + cmd.spaceRoleSetter = spaceRoleSetter + cmd.spaceRepo = spaceRepo + cmd.orgRepo = orgRepo + cmd.userRepo = userRepo + cmd.spaceQuotaRepo = spaceQuotaRepo + return +} + +func (cmd CreateSpace) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-space", + Description: T("Create a space"), + Usage: T("CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("o", T("Organization")), + flag_helpers.NewStringFlag("q", T("Quota to assign to the newly created space (excluding this option results in assignment of default quota)")), + }, + } +} + +func (cmd CreateSpace) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement()} + if c.String("o") == "" { + reqs = append(reqs, requirementsFactory.NewTargetedOrgRequirement()) + } + + return +} + +func (cmd CreateSpace) Run(c *cli.Context) { + spaceName := c.Args()[0] + orgName := c.String("o") + spaceQuotaName := c.String("q") + orgGuid := "" + if orgName == "" { + orgName = cmd.config.OrganizationFields().Name + orgGuid = cmd.config.OrganizationFields().Guid + } + + cmd.ui.Say(T("Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "SpaceName": terminal.EntityNameColor(spaceName), + "OrgName": terminal.EntityNameColor(orgName), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + var spaceQuotaGuid string + if spaceQuotaName != "" { + spaceQuota, err := cmd.spaceQuotaRepo.FindByName(spaceQuotaName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + spaceQuotaGuid = spaceQuota.Guid + } + + if orgGuid == "" { + org, apiErr := cmd.orgRepo.FindByName(orgName) + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Failed(T("Org {{.OrgName}} does not exist or is not accessible", map[string]interface{}{"OrgName": orgName})) + return + default: + cmd.ui.Failed(T("Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + map[string]interface{}{ + "OrgName": orgName, + "ErrorDescription": apiErr.Error(), + })) + return + } + + orgGuid = org.Guid + } + + space, err := cmd.spaceRepo.Create(spaceName, orgGuid, spaceQuotaGuid) + if err != nil { + if httpErr, ok := err.(errors.HttpError); ok && httpErr.ErrorCode() == errors.SPACE_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Space {{.SpaceName}} already exists", map[string]interface{}{"SpaceName": spaceName})) + return + } + cmd.ui.Failed(err.Error()) + return + } + cmd.ui.Ok() + + err = cmd.spaceRoleSetter.SetSpaceRole(space, models.SPACE_MANAGER, cmd.config.UserGuid(), cmd.config.Username()) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + err = cmd.spaceRoleSetter.SetSpaceRole(space, models.SPACE_DEVELOPER, cmd.config.UserGuid(), cmd.config.Username()) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } + + cmd.ui.Say(T("\nTIP: Use '{{.CFTargetCommand}}' to target new space", + map[string]interface{}{ + "CFTargetCommand": terminal.CommandColor(cf.Name() + " target -o " + orgName + " -s " + space.Name), + })) +} diff --git a/cf/commands/space/create_space_test.go b/cf/commands/space/create_space_test.go new file mode 100644 index 00000000000..7a2161d18a9 --- /dev/null +++ b/cf/commands/space/create_space_test.go @@ -0,0 +1,209 @@ +package space_test + +import ( + "errors" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + fake_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/space" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("create-space command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configSpace models.SpaceFields + configOrg models.OrganizationFields + configRepo core_config.ReadWriter + spaceRepo *testapi.FakeSpaceRepository + orgRepo *fake_org.FakeOrganizationRepository + userRepo *testapi.FakeUserRepository + spaceRoleSetter user.SpaceRoleSetter + spaceQuotaRepo *fakes.FakeSpaceQuotaRepository + ) + + runCommand := func(args ...string) bool { + cmd := NewCreateSpace(ui, configRepo, spaceRoleSetter, spaceRepo, orgRepo, userRepo, spaceQuotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = &testterm.FakeUI{} + configRepo = testconfig.NewRepositoryWithDefaults() + + orgRepo = &fake_org.FakeOrganizationRepository{} + userRepo = &testapi.FakeUserRepository{} + spaceRoleSetter = user.NewSetSpaceRole(ui, configRepo, spaceRepo, userRepo) + spaceQuotaRepo = &fakes.FakeSpaceQuotaRepository{} + + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} + configOrg = models.OrganizationFields{ + Name: "my-org", + Guid: "my-org-guid", + } + + configSpace = models.SpaceFields{ + Name: "config-space", + Guid: "config-space-guid", + } + + spaceRepo = &testapi.FakeSpaceRepository{ + CreateSpaceSpace: maker.NewSpace(maker.Overrides{"name": "my-space", "guid": "my-space-guid", "organization": configOrg}), + } + Expect(spaceRepo.CreateSpaceSpace.Name).To(Equal("my-space")) + }) + + Describe("Requirements", func() { + It("fails with usage when not provided exactly one argument", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("when not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("some-space")).To(BeFalse()) + }) + }) + + Context("when a org is not targeted", func() { + BeforeEach(func() { + requirementsFactory.TargetedOrgSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("what-is-space?")).To(BeFalse()) + }) + }) + }) + + It("creates a space", func() { + runCommand("my-space") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating space", "my-space", "my-org", "my-user"}, + []string{"OK"}, + []string{"Assigning", models.SpaceRoleToUserInput[models.SPACE_MANAGER], "my-user", "my-space"}, + []string{"Assigning", models.SpaceRoleToUserInput[models.SPACE_DEVELOPER], "my-user", "my-space"}, + []string{"TIP"}, + )) + + Expect(spaceRepo.CreateSpaceName).To(Equal("my-space")) + Expect(spaceRepo.CreateSpaceOrgGuid).To(Equal("my-org-guid")) + Expect(userRepo.SetSpaceRoleUserGuid).To(Equal("my-user-guid")) + Expect(userRepo.SetSpaceRoleSpaceGuid).To(Equal("my-space-guid")) + Expect(userRepo.SetSpaceRoleRole).To(Equal(models.SPACE_DEVELOPER)) + }) + + It("warns the user when a space with that name already exists", func() { + spaceRepo.CreateSpaceExists = true + runCommand("my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating space", "my-space"}, + []string{"OK"}, + )) + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"my-space", "already exists"})) + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"Assigning", "my-user", "my-space", models.SpaceRoleToUserInput[models.SPACE_MANAGER]}, + )) + + Expect(spaceRepo.CreateSpaceName).To(Equal("")) + Expect(spaceRepo.CreateSpaceOrgGuid).To(Equal("")) + Expect(userRepo.SetSpaceRoleUserGuid).To(Equal("")) + Expect(userRepo.SetSpaceRoleSpaceGuid).To(Equal("")) + }) + + Context("when the -o flag is provided", func() { + It("creates a space within that org", func() { + org := models.Organization{ + OrganizationFields: models.OrganizationFields{ + Name: "other-org", + Guid: "org-guid-1", + }} + orgRepo.FindByNameReturns(org, nil) + + runCommand("-o", "other-org", "my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating space", "my-space", "other-org", "my-user"}, + []string{"OK"}, + []string{"Assigning", "my-user", "my-space", models.SpaceRoleToUserInput[models.SPACE_MANAGER]}, + []string{"Assigning", "my-user", "my-space", models.SpaceRoleToUserInput[models.SPACE_DEVELOPER]}, + []string{"TIP"}, + )) + + Expect(spaceRepo.CreateSpaceName).To(Equal("my-space")) + Expect(spaceRepo.CreateSpaceOrgGuid).To(Equal(org.Guid)) + Expect(userRepo.SetSpaceRoleUserGuid).To(Equal("my-user-guid")) + Expect(userRepo.SetSpaceRoleSpaceGuid).To(Equal("my-space-guid")) + Expect(userRepo.SetSpaceRoleRole).To(Equal(models.SPACE_DEVELOPER)) + }) + + It("fails when the org provided does not exist", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.New("cool-organization does not exist")) + runCommand("-o", "cool-organization", "my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"cool-organization", "does not exist"}, + )) + + Expect(spaceRepo.CreateSpaceName).To(Equal("")) + }) + + It("fails when finding the org returns an error", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.New("cool-organization does not exist")) + runCommand("-o", "cool-organization", "my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Error"}, + )) + + Expect(spaceRepo.CreateSpaceName).To(Equal("")) + }) + }) + + Context("when the -q flag is provided", func() { + It("assigns the space-quota specified to the space", func() { + spaceQuota := models.SpaceQuota{ + Name: "my-space-quota", + Guid: "my-space-quota-guid", + } + spaceQuotaRepo.FindByNameReturns(spaceQuota, nil) + runCommand("-q", "my-space-quota", "my-space") + + Expect(spaceQuotaRepo.FindByNameArgsForCall(0)).To(Equal(spaceQuota.Name)) + Expect(spaceRepo.CreateSpaceSpaceQuotaGuid).To(Equal(spaceQuota.Guid)) + + }) + + Context("when the space-quota provided does not exist", func() { + It("fails", func() { + spaceQuotaRepo.FindByNameReturns(models.SpaceQuota{}, errors.New("Error")) + runCommand("-q", "my-space-quota", "my-space") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Error"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/space/delete_space.go b/cf/commands/space/delete_space.go new file mode 100644 index 00000000000..77633846e79 --- /dev/null +++ b/cf/commands/space/delete_space.go @@ -0,0 +1,88 @@ +package space + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteSpace struct { + ui terminal.UI + config core_config.ReadWriter + spaceRepo spaces.SpaceRepository + spaceReq requirements.SpaceRequirement +} + +func NewDeleteSpace(ui terminal.UI, config core_config.ReadWriter, spaceRepo spaces.SpaceRepository) (cmd *DeleteSpace) { + cmd = new(DeleteSpace) + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + return +} + +func (cmd *DeleteSpace) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-space", + Description: T("Delete a space"), + Usage: T("CF_NAME delete-space SPACE [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd *DeleteSpace) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.spaceReq = requirementsFactory.NewSpaceRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + cmd.spaceReq, + } + return +} + +func (cmd *DeleteSpace) Run(c *cli.Context) { + spaceName := c.Args()[0] + + if !c.Bool("f") { + if !cmd.ui.ConfirmDelete(T("space"), spaceName) { + return + } + } + + cmd.ui.Say(T("Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + map[string]interface{}{ + "TargetSpace": terminal.EntityNameColor(spaceName), + "TargetOrg": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + space := cmd.spaceReq.GetSpace() + + apiErr := cmd.spaceRepo.Delete(space.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + + if cmd.config.SpaceFields().Name == spaceName { + cmd.config.SetSpaceFields(models.SpaceFields{}) + cmd.ui.Say(T("TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + map[string]interface{}{"CfTargetCommand": cf.Name() + " target -s"})) + } + + return +} diff --git a/cf/commands/space/delete_space_test.go b/cf/commands/space/delete_space_test.go new file mode 100644 index 00000000000..6049fb3a1b0 --- /dev/null +++ b/cf/commands/space/delete_space_test.go @@ -0,0 +1,97 @@ +package space_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/space" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-space command", func() { + var ( + ui *testterm.FakeUI + space models.Space + config core_config.ReadWriter + spaceRepo *testapi.FakeSpaceRepository + requirementsFactory *testreq.FakeReqFactory + ) + + runCommand := func(args ...string) bool { + cmd := NewDeleteSpace(ui, config, spaceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = &testterm.FakeUI{} + spaceRepo = &testapi.FakeSpaceRepository{} + config = testconfig.NewRepositoryWithDefaults() + + space = maker.NewSpace(maker.Overrides{ + "name": "space-to-delete", + "guid": "space-to-delete-guid", + }) + + requirementsFactory = &testreq.FakeReqFactory{ + LoginSuccess: true, + TargetedOrgSuccess: true, + Space: space, + } + }) + + Describe("requirements", func() { + BeforeEach(func() { + ui.Inputs = []string{"y"} + }) + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("my-space")).To(BeFalse()) + }) + + It("fails when not targeting a space", func() { + requirementsFactory.TargetedOrgSuccess = false + + Expect(runCommand("my-space")).To(BeFalse()) + }) + }) + + It("deletes a space, given its name", func() { + ui.Inputs = []string{"yes"} + runCommand("space-to-delete") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the space space-to-delete"})) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting space", "space-to-delete", "my-org", "my-user"}, + []string{"OK"}, + )) + Expect(spaceRepo.DeletedSpaceGuid).To(Equal("space-to-delete-guid")) + Expect(config.HasSpace()).To(Equal(true)) + }) + + It("does not prompt when the -f flag is given", func() { + runCommand("-f", "space-to-delete") + + Expect(ui.Prompts).To(BeEmpty()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "space-to-delete"}, + []string{"OK"}, + )) + Expect(spaceRepo.DeletedSpaceGuid).To(Equal("space-to-delete-guid")) + }) + + It("clears the space from the config, when deleting the space currently targeted", func() { + config.SetSpaceFields(space.SpaceFields) + runCommand("-f", "space-to-delete") + + Expect(config.HasSpace()).To(Equal(false)) + }) +}) diff --git a/cf/commands/space/rename_space.go b/cf/commands/space/rename_space.go new file mode 100644 index 00000000000..1819e959489 --- /dev/null +++ b/cf/commands/space/rename_space.go @@ -0,0 +1,73 @@ +package space + +import ( + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type RenameSpace struct { + ui terminal.UI + config core_config.ReadWriter + spaceRepo spaces.SpaceRepository + spaceReq requirements.SpaceRequirement +} + +func NewRenameSpace(ui terminal.UI, config core_config.ReadWriter, spaceRepo spaces.SpaceRepository) (cmd *RenameSpace) { + cmd = new(RenameSpace) + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + return +} + +func (cmd *RenameSpace) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "rename-space", + Description: T("Rename a space"), + Usage: T("CF_NAME rename-space SPACE NEW_SPACE"), + } +} + +func (cmd *RenameSpace) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + cmd.spaceReq = requirementsFactory.NewSpaceRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + cmd.spaceReq, + } + return +} + +func (cmd *RenameSpace) Run(c *cli.Context) { + space := cmd.spaceReq.GetSpace() + newName := c.Args()[1] + cmd.ui.Say(T("Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "OldSpaceName": terminal.EntityNameColor(space.Name), + "NewSpaceName": terminal.EntityNameColor(newName), + "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr := cmd.spaceRepo.Rename(space.Guid, newName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + if cmd.config.SpaceFields().Guid == space.Guid { + space.Name = newName + cmd.config.SetSpaceFields(space.SpaceFields) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/space/rename_space_test.go b/cf/commands/space/rename_space_test.go new file mode 100644 index 00000000000..e8d35f2106c --- /dev/null +++ b/cf/commands/space/rename_space_test.go @@ -0,0 +1,94 @@ +package space_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/space" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("rename-space command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + spaceRepo *testapi.FakeSpaceRepository + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} + spaceRepo = &testapi.FakeSpaceRepository{} + }) + + var callRenameSpace = func(args []string) bool { + cmd := NewRenameSpace(ui, configRepo, spaceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("when the user is not logged in", func() { + It("does not pass requirements", func() { + requirementsFactory.LoginSuccess = false + + Expect(callRenameSpace([]string{"my-space", "my-new-space"})).To(BeFalse()) + }) + }) + + Describe("when the user has not targeted an org", func() { + It("does not pass requirements", func() { + requirementsFactory.TargetedOrgSuccess = false + + Expect(callRenameSpace([]string{"my-space", "my-new-space"})).To(BeFalse()) + }) + }) + + Describe("when the user provides fewer than two args", func() { + It("fails with usage", func() { + callRenameSpace([]string{"foo"}) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Describe("when the user is logged in and has provided an old and new space name", func() { + BeforeEach(func() { + space := models.Space{} + space.Name = "the-old-space-name" + space.Guid = "the-old-space-guid" + requirementsFactory.Space = space + }) + + It("renames a space", func() { + originalSpaceName := configRepo.SpaceFields().Name + callRenameSpace([]string{"the-old-space-name", "my-new-space"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Renaming space", "the-old-space-name", "my-new-space", "my-org", "my-user"}, + []string{"OK"}, + )) + + Expect(spaceRepo.RenameSpaceGuid).To(Equal("the-old-space-guid")) + Expect(spaceRepo.RenameNewName).To(Equal("my-new-space")) + Expect(configRepo.SpaceFields().Name).To(Equal(originalSpaceName)) + }) + + Describe("renaming the space the user has targeted", func() { + BeforeEach(func() { + configRepo.SetSpaceFields(requirementsFactory.Space.SpaceFields) + }) + + It("renames the targeted space", func() { + callRenameSpace([]string{"the-old-space-name", "my-new-space-name"}) + Expect(configRepo.SpaceFields().Name).To(Equal("my-new-space-name")) + }) + }) + }) +}) diff --git a/cf/commands/space/space.go b/cf/commands/space/space.go new file mode 100644 index 00000000000..f3d6aebce52 --- /dev/null +++ b/cf/commands/space/space.go @@ -0,0 +1,132 @@ +package space + +import ( + "fmt" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ShowSpace struct { + ui terminal.UI + config core_config.Reader + spaceReq requirements.SpaceRequirement + quotaRepo space_quotas.SpaceQuotaRepository +} + +func NewShowSpace(ui terminal.UI, config core_config.Reader, quotaRepo space_quotas.SpaceQuotaRepository) (cmd *ShowSpace) { + cmd = new(ShowSpace) + cmd.ui = ui + cmd.config = config + cmd.quotaRepo = quotaRepo + return +} + +func (cmd *ShowSpace) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "space", + Description: T("Show space info"), + Usage: T("CF_NAME space SPACE"), + } +} + +func (cmd *ShowSpace) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + cmd.spaceReq = requirementsFactory.NewSpaceRequirement(c.Args()[0]) + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + cmd.spaceReq, + } + return +} + +func (cmd *ShowSpace) Run(c *cli.Context) { + space := cmd.spaceReq.GetSpace() + cmd.ui.Say(T("Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + map[string]interface{}{ + "TargetSpace": terminal.EntityNameColor(space.Name), + "OrgName": terminal.EntityNameColor(space.Organization.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + quotaString := cmd.quotaString(space) + + cmd.ui.Ok() + cmd.ui.Say("") + table := terminal.NewTable(cmd.ui, []string{terminal.EntityNameColor(space.Name), "", ""}) + table.Add("", T("Org:"), terminal.EntityNameColor(space.Organization.Name)) + + apps := []string{} + for _, app := range space.Applications { + apps = append(apps, terminal.EntityNameColor(app.Name)) + } + table.Add("", T("Apps:"), strings.Join(apps, ", ")) + + domains := []string{} + for _, domain := range space.Domains { + domains = append(domains, terminal.EntityNameColor(domain.Name)) + } + table.Add("", T("Domains:"), strings.Join(domains, ", ")) + + services := []string{} + for _, service := range space.ServiceInstances { + services = append(services, terminal.EntityNameColor(service.Name)) + } + table.Add("", T("Services:"), strings.Join(services, ", ")) + + securityGroups := []string{} + for _, group := range space.SecurityGroups { + securityGroups = append(securityGroups, terminal.EntityNameColor(group.Name)) + } + table.Add("", T("Security Groups:"), strings.Join(securityGroups, ", ")) + + table.Add("", T("Space Quota:"), quotaString) + + table.Print() +} + +func (cmd *ShowSpace) quotaString(space models.Space) string { + var instance_memory string + + if space.SpaceQuotaGuid == "" { + return "" + } + + quota, err := cmd.quotaRepo.FindByGuid(space.SpaceQuotaGuid) + if err != nil { + cmd.ui.Failed(err.Error()) + return "" + } + + if quota.InstanceMemoryLimit == -1 { + instance_memory = "-1" + } else { + instance_memory = formatters.ByteSize(quota.InstanceMemoryLimit * formatters.MEGABYTE) + } + memory := formatters.ByteSize(quota.MemoryLimit * formatters.MEGABYTE) + + spaceQuota := fmt.Sprintf("%s (%s memory limit, %s instance memory limit, %d routes, %d services, paid services %s)", quota.Name, memory, instance_memory, quota.RoutesLimit, quota.ServicesLimit, formatters.Allowed(quota.NonBasicServicesAllowed)) + // spaceQuota := fmt.Sprintf(T("{{.QuotaName}} ({{.MemoryLimit}} memory limit, {{.InstanceMemoryLimit}} instance memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + // map[string]interface{}{ + // "QuotaName": quota.Name, + // "MemoryLimit": memory, + // "InstanceMemoryLimit": instance_memory, + // "RoutesLimit": quota.RoutesLimit, + // "ServicesLimit": quota.ServicesLimit, + // "NonBasicServicesAllowed": formatters.Allowed(quota.NonBasicServicesAllowed)})) + + return spaceQuota +} diff --git a/cf/commands/space/space_suite_test.go b/cf/commands/space/space_suite_test.go new file mode 100644 index 00000000000..f405930777a --- /dev/null +++ b/cf/commands/space/space_suite_test.go @@ -0,0 +1,19 @@ +package space_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpace(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Space Suite") +} diff --git a/cf/commands/space/space_test.go b/cf/commands/space/space_test.go new file mode 100644 index 00000000000..9a0c6fda1d2 --- /dev/null +++ b/cf/commands/space/space_test.go @@ -0,0 +1,136 @@ +package space_test + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + . "github.com/cloudfoundry/cli/cf/commands/space" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("space command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + quotaRepo *fakes.FakeSpaceQuotaRepository + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewShowSpace(ui, configRepo, quotaRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand("some-space")).To(BeFalse()) + }) + + It("fails when an org is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand("some-space")).To(BeFalse()) + }) + }) + + Context("when logged in and an org is targeted", func() { + BeforeEach(func() { + org := models.OrganizationFields{} + org.Name = "my-org" + + app := models.ApplicationFields{} + app.Name = "app1" + app.Guid = "app1-guid" + apps := []models.ApplicationFields{app} + + domain := models.DomainFields{} + domain.Name = "domain1" + domain.Guid = "domain1-guid" + domains := []models.DomainFields{domain} + + serviceInstance := models.ServiceInstanceFields{} + serviceInstance.Name = "service1" + serviceInstance.Guid = "service1-guid" + services := []models.ServiceInstanceFields{serviceInstance} + + securityGroup1 := models.SecurityGroupFields{Name: "Nacho Security"} + securityGroup2 := models.SecurityGroupFields{Name: "Nacho Prime"} + securityGroups := []models.SecurityGroupFields{securityGroup1, securityGroup2} + + space := models.Space{} + space.Name = "whose-space-is-it-anyway" + space.Organization = org + space.Applications = apps + space.Domains = domains + space.ServiceInstances = services + space.SecurityGroups = securityGroups + space.SpaceQuotaGuid = "runaway-guid" + + quota := models.SpaceQuota{} + quota.Guid = "runaway-guid" + quota.Name = "runaway" + quota.MemoryLimit = 102400 + quota.InstanceMemoryLimit = -1 + quota.RoutesLimit = 111 + quota.ServicesLimit = 222 + quota.NonBasicServicesAllowed = false + + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + requirementsFactory.Space = space + + quotaRepo.FindByGuidReturns(quota, nil) + }) + + Context("when the space has a space quota", func() { + It("shows information about the given space", func() { + runCommand("whose-space-is-it-anyway") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting info for space", "whose-space-is-it-anyway", "my-org", "my-user"}, + []string{"OK"}, + []string{"whose-space-is-it-anyway"}, + []string{"Org", "my-org"}, + []string{"Apps", "app1"}, + []string{"Domains", "domain1"}, + []string{"Services", "service1"}, + []string{"Security Groups", "Nacho Security", "Nacho Prime"}, + []string{"Space Quota", "runaway (100G memory limit, -1 instance memory limit, 111 routes, 222 services, paid services disallowed)"}, + )) + }) + }) + + Context("when the space does not have a space quota", func() { + It("shows information without a space quota", func() { + requirementsFactory.Space.SpaceQuotaGuid = "" + runCommand("whose-space-is-it-anyway") + Expect(quotaRepo.FindByGuidCallCount()).To(Equal(0)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting info for space", "whose-space-is-it-anyway", "my-org", "my-user"}, + []string{"OK"}, + []string{"whose-space-is-it-anyway"}, + []string{"Org", "my-org"}, + []string{"Apps", "app1"}, + []string{"Domains", "domain1"}, + []string{"Services", "service1"}, + []string{"Security Groups", "Nacho Security", "Nacho Prime"}, + []string{"Space Quota"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/space/spaces.go b/cf/commands/space/spaces.go new file mode 100644 index 00000000000..adba5a77c95 --- /dev/null +++ b/cf/commands/space/spaces.go @@ -0,0 +1,73 @@ +package space + +import ( + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListSpaces struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository +} + +func NewListSpaces(ui terminal.UI, config core_config.Reader, spaceRepo spaces.SpaceRepository) (cmd ListSpaces) { + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + return +} + +func (cmd ListSpaces) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "spaces", + Description: T("List all spaces in an org"), + Usage: T("CF_NAME spaces"), + } +} + +func (cmd ListSpaces) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + } + return +} + +func (cmd ListSpaces) Run(c *cli.Context) { + cmd.ui.Say(T("Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + map[string]interface{}{ + "TargetOrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + foundSpaces := false + table := cmd.ui.Table([]string{T("name")}) + apiErr := cmd.spaceRepo.ListSpaces(func(space models.Space) bool { + table.Add(space.Name) + foundSpaces = true + return true + }) + table.Print() + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching spaces.\n{{.ErrorDescription}}", + map[string]interface{}{ + "ErrorDescription": apiErr.Error(), + })) + return + } + + if !foundSpaces { + cmd.ui.Say(T("No spaces found")) + } +} diff --git a/cf/commands/space/spaces_test.go b/cf/commands/space/spaces_test.go new file mode 100644 index 00000000000..ba35eabd4f5 --- /dev/null +++ b/cf/commands/space/spaces_test.go @@ -0,0 +1,102 @@ +package space_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/space" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func callSpaces(args []string, requirementsFactory *testreq.FakeReqFactory, config core_config.Reader, spaceRepo spaces.SpaceRepository) (ui *testterm.FakeUI) { + ui = new(testterm.FakeUI) + cmd := NewListSpaces(ui, config, spaceRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} + +var _ = Describe("spaces command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + spaceRepo *testapi.FakeSpaceRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + spaceRepo = &testapi.FakeSpaceRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewListSpaces(ui, configRepo, spaceRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.TargetedOrgSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + + It("fails when an org is not targeted", func() { + requirementsFactory.LoginSuccess = true + + Expect(runCommand()).To(BeFalse()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in and an org is targeted", func() { + BeforeEach(func() { + space := models.Space{} + space.Name = "space1" + space2 := models.Space{} + space2.Name = "space2" + space3 := models.Space{} + space3.Name = "space3" + spaceRepo.Spaces = []models.Space{space, space2, space3} + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + }) + + It("lists all of the spaces", func() { + runCommand() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting spaces in org", "my-org", "my-user"}, + []string{"space1"}, + []string{"space2"}, + []string{"space3"}, + )) + }) + + Context("when there are no spaces", func() { + BeforeEach(func() { + spaceRepo.Spaces = []models.Space{} + }) + + It("politely tells the user", func() { + runCommand() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting spaces in org", "my-org", "my-user"}, + []string{"No spaces found"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/spacequota/create_quota.go b/cf/commands/spacequota/create_quota.go new file mode 100644 index 00000000000..855b725f73b --- /dev/null +++ b/cf/commands/spacequota/create_quota.go @@ -0,0 +1,125 @@ +package spacequota + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateSpaceQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo space_quotas.SpaceQuotaRepository + orgRepo organizations.OrganizationRepository +} + +func NewCreateSpaceQuota(ui terminal.UI, config core_config.Reader, quotaRepo space_quotas.SpaceQuotaRepository, orgRepo organizations.OrganizationRepository) CreateSpaceQuota { + return CreateSpaceQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + orgRepo: orgRepo, + } +} + +func (cmd CreateSpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-space-quota", + Description: T("Define a new space resource quota"), + Usage: T("CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("i", T("Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)")), + flag_helpers.NewStringFlag("m", T("Total amount of memory a space can have (e.g. 1024M, 1G, 10G)")), + flag_helpers.NewIntFlag("r", T("Total number of routes")), + flag_helpers.NewIntFlag("s", T("Total number of service instances")), + cli.BoolFlag{Name: "allow-paid-service-plans", Usage: T("Can provision instances of paid service plans (Default: disallowed)")}, + }, + } +} + +func (cmd CreateSpaceQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + }, nil +} + +func (cmd CreateSpaceQuota) Run(context *cli.Context) { + name := context.Args()[0] + org := cmd.config.OrganizationFields() + + cmd.ui.Say(T("Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(name), + "OrgName": terminal.EntityNameColor(org.Name), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + quota := models.SpaceQuota{ + Name: name, + OrgGuid: org.Guid, + } + + memoryLimit := context.String("m") + if memoryLimit != "" { + parsedMemory, errr := formatters.ToMegabytes(memoryLimit) + if errr != nil { + cmd.ui.Failed(T("Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", map[string]interface{}{"MemoryLimit": memoryLimit, "Err": errr})) + } + + quota.MemoryLimit = parsedMemory + } + + instanceMemoryLimit := context.String("i") + var parsedMemory int64 + var err error + if instanceMemoryLimit == "-1" || instanceMemoryLimit == "" { + parsedMemory = -1 + } else { + parsedMemory, err = formatters.ToMegabytes(instanceMemoryLimit) + if err != nil { + cmd.ui.Failed(T("Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", map[string]interface{}{"MemoryLimit": instanceMemoryLimit, "Err": err})) + } + } + + quota.InstanceMemoryLimit = parsedMemory + + if context.IsSet("r") { + quota.RoutesLimit = context.Int("r") + } + + if context.IsSet("s") { + quota.ServicesLimit = context.Int("s") + } + + if context.IsSet("allow-paid-service-plans") { + quota.NonBasicServicesAllowed = true + } + + err = cmd.quotaRepo.Create(quota) + + httpErr, ok := err.(errors.HttpError) + if ok && httpErr.ErrorCode() == errors.QUOTA_EXISTS { + cmd.ui.Ok() + cmd.ui.Warn(T("Space Quota Definition {{.QuotaName}} already exists", map[string]interface{}{"QuotaName": quota.Name})) + return + } + + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/spacequota/create_quota_test.go b/cf/commands/spacequota/create_quota_test.go new file mode 100644 index 00000000000..6d580ea24b2 --- /dev/null +++ b/cf/commands/spacequota/create_quota_test.go @@ -0,0 +1,166 @@ +package spacequota_test + +import ( + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" +) + +var _ = Describe("create-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + orgRepo *test_org.FakeOrganizationRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + orgRepo = &test_org.FakeOrganizationRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + + org := models.Organization{} + org.Name = "my-org" + org.Guid = "my-org-guid" + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + orgRepo.FindByNameReturns(org, nil) + }) + + runCommand := func(args ...string) bool { + cmd := NewCreateSpaceQuota(ui, configuration.NewRepositoryWithDefaults(), quotaRepo, orgRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("my-quota", "-m", "50G")).To(BeFalse()) + }) + + It("requires the user to target an org", func() { + requirementsFactory.TargetedOrgSuccess = false + + Expect(runCommand("my-quota", "-m", "50G")).To(BeFalse()) + }) + }) + + Context("when requirements have been met", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + }) + + It("fails requirements when called without a quota name", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("creates a quota with a given name", func() { + runCommand("my-quota") + Expect(quotaRepo.CreateArgsForCall(0).Name).To(Equal("my-quota")) + Expect(quotaRepo.CreateArgsForCall(0).OrgGuid).To(Equal("my-org-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating space quota", "my-org", "my-quota", "my-user", "..."}, + []string{"OK"}, + )) + }) + + Context("when the -i flag is not provided", func() { + It("sets the instance memory limit to unlimiited", func() { + runCommand("my-quota") + + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + }) + + Context("when the -m flag is provided", func() { + It("sets the memory limit", func() { + runCommand("-m", "50G", "erryday makin fitty jeez") + Expect(quotaRepo.CreateArgsForCall(0).MemoryLimit).To(Equal(int64(51200))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("-m", "whoops", "wit mah hussle") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when the -i flag is provided", func() { + It("sets the memory limit", func() { + runCommand("-i", "50G", "erryday makin fitty jeez") + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(51200))) + }) + + It("accepts -1 without units as an appropriate value", func() { + runCommand("-i", "-1", "wit mah hussle") + Expect(quotaRepo.CreateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("-i", "whoops", "yo", "12") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + It("sets the route limit", func() { + runCommand("-r", "12", "ecstatic") + + Expect(quotaRepo.CreateArgsForCall(0).RoutesLimit).To(Equal(12)) + }) + + It("sets the service instance limit", func() { + runCommand("-s", "42", "black star") + Expect(quotaRepo.CreateArgsForCall(0).ServicesLimit).To(Equal(42)) + }) + + It("defaults to not allowing paid service plans", func() { + runCommand("my-pro-bono-quota") + Expect(quotaRepo.CreateArgsForCall(0).NonBasicServicesAllowed).To(BeFalse()) + }) + + Context("when requesting to allow paid service plans", func() { + It("creates the quota with paid service plans allowed", func() { + runCommand("--allow-paid-service-plans", "my-for-profit-quota") + Expect(quotaRepo.CreateArgsForCall(0).NonBasicServicesAllowed).To(BeTrue()) + }) + }) + + Context("when creating a quota returns an error", func() { + It("alerts the user when creating the quota fails", func() { + quotaRepo.CreateReturns(errors.New("WHOOP THERE IT IS")) + runCommand("my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating space quota", "my-quota", "my-org"}, + []string{"FAILED"}, + )) + }) + + It("warns the user when quota already exists", func() { + quotaRepo.CreateReturns(errors.NewHttpError(400, "240002", "Quota Definition is taken: quota-sct")) + runCommand("Banana") + + Expect(ui.Outputs).ToNot(ContainSubstrings( + []string{"FAILED"}, + )) + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"already exists"})) + }) + + }) + }) +}) diff --git a/cf/commands/spacequota/delete_quota.go b/cf/commands/spacequota/delete_quota.go new file mode 100644 index 00000000000..0ab8621e73d --- /dev/null +++ b/cf/commands/spacequota/delete_quota.go @@ -0,0 +1,81 @@ +package spacequota + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" + + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type DeleteSpaceQuota struct { + ui terminal.UI + config core_config.Reader + spaceQuotaRepo space_quotas.SpaceQuotaRepository +} + +func NewDeleteSpaceQuota(ui terminal.UI, config core_config.Reader, spaceQuotaRepo space_quotas.SpaceQuotaRepository) DeleteSpaceQuota { + return DeleteSpaceQuota{ + ui: ui, + config: config, + spaceQuotaRepo: spaceQuotaRepo, + } +} +func (cmd DeleteSpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-space-quota", + Description: T("Delete a space quota definition and unassign the space quota from all spaces"), + Usage: T("CF_NAME delete-space-quota SPACE-QUOTA-NAME"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force delete (do not prompt for confirmation)")}, + }, + } +} + +func (cmd DeleteSpaceQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 1 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + }, nil +} + +func (cmd DeleteSpaceQuota) Run(context *cli.Context) { + quotaName := context.Args()[0] + + if !context.Bool("f") { + response := cmd.ui.ConfirmDelete("quota", quotaName) + if !response { + return + } + } + + cmd.ui.Say(T("Deleting space quota {{.QuotaName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(quotaName), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + quota, apiErr := cmd.spaceQuotaRepo.FindByName(quotaName) + switch (apiErr).(type) { + case nil: // no error + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("Quota {{.QuotaName}} does not exist", map[string]interface{}{"QuotaName": quotaName})) + return + default: + cmd.ui.Failed(apiErr.Error()) + } + + apiErr = cmd.spaceQuotaRepo.Delete(quota.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/spacequota/delete_quota_test.go b/cf/commands/spacequota/delete_quota_test.go new file mode 100644 index 00000000000..969cbe4ee56 --- /dev/null +++ b/cf/commands/spacequota/delete_quota_test.go @@ -0,0 +1,155 @@ +package spacequota_test + +import ( + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("delete-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + orgRepo *test_org.FakeOrganizationRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + orgRepo = &test_org.FakeOrganizationRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + + org := models.Organization{} + org.Name = "my-org" + org.Guid = "my-org-guid" + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + orgRepo.FindByNameReturns(org, nil) + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteSpaceQuota(ui, configuration.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Context("when the user is not logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = false + }) + + It("fails requirements", func() { + Expect(runCommand("my-quota")).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + It("fails requirements when called without a quota name", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when an org is not targeted", func() { + requirementsFactory.TargetedOrgSuccess = false + Expect(runCommand()).To(BeFalse()) + }) + + Context("When the quota provided exists", func() { + BeforeEach(func() { + quota := models.SpaceQuota{} + quota.Name = "my-quota" + quota.Guid = "my-quota-guid" + quota.OrgGuid = "my-org-guid" + quotaRepo.FindByNameReturns(quota, nil) + }) + + It("deletes a quota with a given name when the user confirms", func() { + ui.Inputs = []string{"y"} + + runCommand("my-quota") + Expect(quotaRepo.DeleteArgsForCall(0)).To(Equal("my-quota-guid")) + + Expect(ui.Prompts).To(ContainSubstrings( + []string{"Really delete the quota", "my-quota"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting space quota", "my-quota", "as", "my-user"}, + []string{"OK"}, + )) + }) + + It("does not prompt when the -f flag is provided", func() { + runCommand("-f", "my-quota") + + Expect(quotaRepo.DeleteArgsForCall(0)).To(Equal("my-quota-guid")) + + Expect(ui.Prompts).To(BeEmpty()) + }) + + It("shows an error when deletion fails", func() { + quotaRepo.DeleteReturns(errors.New("some error")) + + runCommand("-f", "my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "my-quota"}, + []string{"FAILED"}, + )) + }) + }) + + Context("when finding the quota fails", func() { + Context("when the quota provided does not exist", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.SpaceQuota{}, errors.NewModelNotFoundError("Quota", "non-existent-quota")) + }) + + It("warns the user when that the quota does not exist", func() { + runCommand("-f", "non-existent-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting", "non-existent-quota"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"non-existent-quota", "does not exist"}, + )) + }) + }) + + Context("when other types of error occur", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.SpaceQuota{}, errors.New("some error")) + }) + + It("shows an error", func() { + runCommand("-f", "my-quota") + + Expect(ui.WarnOutputs).ToNot(ContainSubstrings( + []string{"my-quota", "does not exist"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + + }) + }) + }) + }) +}) diff --git a/cf/commands/spacequota/set_space_quota.go b/cf/commands/spacequota/set_space_quota.go new file mode 100644 index 00000000000..5b171aafc19 --- /dev/null +++ b/cf/commands/spacequota/set_space_quota.go @@ -0,0 +1,80 @@ +package spacequota + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetSpaceQuota struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository + quotaRepo space_quotas.SpaceQuotaRepository +} + +func NewSetSpaceQuota(ui terminal.UI, config core_config.Reader, spaceRepo spaces.SpaceRepository, quotaRepo space_quotas.SpaceQuotaRepository) SetSpaceQuota { + return SetSpaceQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + spaceRepo: spaceRepo, + } +} + +func (cmd SetSpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-space-quota", + Description: T("Assign a space quota definition to a space"), + Usage: T("CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME"), + } +} + +func (cmd SetSpaceQuota) GetRequirements(requirementsFactory requirements.Factory, context *cli.Context) ([]requirements.Requirement, error) { + if len(context.Args()) != 2 { + cmd.ui.FailWithUsage(context) + } + + return []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + }, nil +} + +func (cmd SetSpaceQuota) Run(context *cli.Context) { + + spaceName := context.Args()[0] + quotaName := context.Args()[1] + + cmd.ui.Say(T("Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(quotaName), + "SpaceName": terminal.EntityNameColor(spaceName), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + space, err := cmd.spaceRepo.FindByName(spaceName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + if space.SpaceQuotaGuid != "" { + cmd.ui.Failed(T("This space already has an assigned space quota.")) + } + + quota, err := cmd.quotaRepo.FindByName(quotaName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + err = cmd.quotaRepo.AssociateSpaceWithQuota(space.Guid, quota.Guid) + if err != nil { + cmd.ui.Failed(err.Error()) + } + + cmd.ui.Ok() +} diff --git a/cf/commands/spacequota/set_space_quota_test.go b/cf/commands/spacequota/set_space_quota_test.go new file mode 100644 index 00000000000..ce0ca33f74a --- /dev/null +++ b/cf/commands/spacequota/set_space_quota_test.go @@ -0,0 +1,165 @@ +package spacequota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/fakes" + quotafakes "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("set-space-quota command", func() { + var ( + ui *testterm.FakeUI + spaceRepo *fakes.FakeSpaceRepository + quotaRepo *quotafakes.FakeSpaceQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + spaceRepo = &fakes.FakeSpaceRepository{} + quotaRepo = "afakes.FakeSpaceQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewSetSpaceQuota(ui, testconfig.NewRepositoryWithDefaults(), spaceRepo, quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("space", "space-quota")).ToNot(HavePassedRequirements()) + }) + + It("requires the user to target an org", func() { + requirementsFactory.TargetedOrgSuccess = false + Expect(runCommand("space", "space-quota")).ToNot(HavePassedRequirements()) + }) + + It("fails with usage if the user does not provide a quota and space", func() { + requirementsFactory.TargetedOrgSuccess = true + requirementsFactory.LoginSuccess = true + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + JustBeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand("my-space", "quota-name")).To(HavePassedRequirements()) + }) + + Context("when the space and quota both exist", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns( + models.SpaceQuota{ + Name: "quota-name", + Guid: "quota-guid", + MemoryLimit: 1024, + InstanceMemoryLimit: 512, + RoutesLimit: 111, + ServicesLimit: 222, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + }, nil) + + spaceRepo.Spaces = []models.Space{ + models.Space{ + SpaceFields: models.SpaceFields{ + Name: "my-space", + Guid: "my-space-guid", + }, + SpaceQuotaGuid: "", + }, + } + }) + + Context("when the space quota was not previously assigned to a space", func() { + It("associates the provided space with the provided space quota", func() { + spaceGuid, quotaGuid := quotaRepo.AssociateSpaceWithQuotaArgsForCall(0) + + Expect(spaceGuid).To(Equal("my-space-guid")) + Expect(quotaGuid).To(Equal("quota-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning space quota", "to space", "my-user"}, + []string{"OK"}, + )) + }) + }) + + Context("when the space quota was previously assigned to a space", func() { + BeforeEach(func() { + spaceRepo.Spaces = []models.Space{ + models.Space{ + SpaceFields: models.SpaceFields{ + Name: "my-space", + Guid: "my-space-guid", + }, + SpaceQuotaGuid: "another-quota", + }, + } + }) + + It("warns the user that the operation was not performed", func() { + Expect(quotaRepo.UpdateCallCount()).To(Equal(0)) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning space quota", "to space", "my-user"}, + []string{"FAILED"}, + []string{"This space already has an assigned space quota."}, + )) + }) + }) + }) + + Context("when an error occurs fetching the space", func() { + BeforeEach(func() { + spaceRepo.FindByNameErr = true + }) + + It("prints an error", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning space quota", "to space", "my-user"}, + []string{"FAILED"}, + []string{"Error finding space by name"}, + )) + }) + }) + + Context("when an error occurs fetching the quota", func() { + BeforeEach(func() { + spaceRepo.Spaces = []models.Space{ + models.Space{ + SpaceFields: models.SpaceFields{ + Name: "my-space", + Guid: "my-space-guid", + }, + SpaceQuotaGuid: "", + }, + } + spaceRepo.FindByNameErr = false + quotaRepo.FindByNameReturns(models.SpaceQuota{}, errors.New("I can't find my quota name!")) + }) + + It("prints an error", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning space quota", "to space", "my-user"}, + []string{"FAILED"}, + []string{"I can't find my quota name!"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/spacequota/space_quota.go b/cf/commands/spacequota/space_quota.go new file mode 100644 index 00000000000..c0a682a00a6 --- /dev/null +++ b/cf/commands/spacequota/space_quota.go @@ -0,0 +1,91 @@ +package spacequota + +import ( + "fmt" + "strconv" + + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SpaceQuota struct { + ui terminal.UI + config core_config.Reader + spaceQuotaRepo space_quotas.SpaceQuotaRepository +} + +func NewSpaceQuota(ui terminal.UI, config core_config.Reader, spaceQuotaRepo space_quotas.SpaceQuotaRepository) (cmd *SpaceQuota) { + return &SpaceQuota{ + ui: ui, + config: config, + spaceQuotaRepo: spaceQuotaRepo, + } +} + +func (cmd *SpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "space-quota", + Description: T("Show space quota info"), + Usage: T("CF_NAME space-quota SPACE_QUOTA_NAME"), + } +} + +func (cmd *SpaceQuota) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + } + return +} + +func (cmd *SpaceQuota) Run(c *cli.Context) { + name := c.Args()[0] + + cmd.ui.Say(T("Getting space quota {{.Quota}} info as {{.Username}}...", + map[string]interface{}{ + "Quota": terminal.EntityNameColor(name), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + spaceQuota, apiErr := cmd.spaceQuotaRepo.FindByName(name) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + var megabytes string + + table := terminal.NewTable(cmd.ui, []string{"", ""}) + table.Add(T("total memory limit"), formatters.ByteSize(spaceQuota.MemoryLimit*formatters.MEGABYTE)) + if spaceQuota.InstanceMemoryLimit == -1 { + megabytes = T("unlimited") + } else { + megabytes = formatters.ByteSize(spaceQuota.InstanceMemoryLimit * formatters.MEGABYTE) + } + + servicesLimit := strconv.Itoa(spaceQuota.ServicesLimit) + if servicesLimit == "-1" { + servicesLimit = T("unlimited") + } + + table.Add(T("instance memory limit"), megabytes) + table.Add(T("routes"), fmt.Sprintf("%d", spaceQuota.RoutesLimit)) + table.Add(T("services"), servicesLimit) + table.Add(T("non basic services"), formatters.Allowed(spaceQuota.NonBasicServicesAllowed)) + + table.Print() + +} diff --git a/cf/commands/spacequota/space_quota_test.go b/cf/commands/spacequota/space_quota_test.go new file mode 100644 index 00000000000..ffd4c6ae989 --- /dev/null +++ b/cf/commands/spacequota/space_quota_test.go @@ -0,0 +1,131 @@ +package spacequota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("quotas command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewSpaceQuota(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand("foo")).ToNot(HavePassedRequirements()) + }) + + It("requires the user to target an org", func() { + requirementsFactory.TargetedOrgSuccess = false + Expect(runCommand("bar")).ToNot(HavePassedRequirements()) + }) + + It("fails when a quota name is not provided", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + }) + + Context("when logged in", func() { + JustBeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand("quota-name")).To(HavePassedRequirements()) + }) + + Context("when quotas exist", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns( + models.SpaceQuota{ + Name: "quota-name", + MemoryLimit: 1024, + InstanceMemoryLimit: -1, + RoutesLimit: 111, + ServicesLimit: 222, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + }, nil) + }) + + It("lists the specific quota info", func() { + Expect(quotaRepo.FindByNameArgsForCall(0)).To(Equal("quota-name")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting space quota quota-name info as", "my-user"}, + []string{"OK"}, + []string{"total memory limit", "1G"}, + []string{"instance memory limit", "unlimited"}, + []string{"routes", "111"}, + []string{"service", "222"}, + []string{"non basic services", "allowed"}, + )) + }) + + Context("when the services are unlimited", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns( + models.SpaceQuota{ + Name: "quota-name", + MemoryLimit: 1024, + InstanceMemoryLimit: 14, + RoutesLimit: 111, + ServicesLimit: -1, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + }, nil) + + }) + + It("replaces -1 with unlimited", func() { + Expect(quotaRepo.FindByNameArgsForCall(0)).To(Equal("quota-name")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting space quota quota-name info as", "my-user"}, + []string{"OK"}, + []string{"total memory limit", "1G"}, + []string{"instance memory limit", "14M"}, + []string{"routes", "111"}, + []string{"service", "unlimited"}, + []string{"non basic services", "allowed"}, + )) + }) + }) + }) + Context("when an error occurs fetching quotas", func() { + BeforeEach(func() { + quotaRepo.FindByNameReturns(models.SpaceQuota{}, errors.New("I haz a borken!")) + }) + + It("prints an error", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting space quota quota-name info as", "my-user"}, + []string{"FAILED"}, + )) + }) + }) + }) + +}) diff --git a/cf/commands/spacequota/space_quotas.go b/cf/commands/spacequota/space_quotas.go new file mode 100644 index 00000000000..2e4208a2a2e --- /dev/null +++ b/cf/commands/spacequota/space_quotas.go @@ -0,0 +1,90 @@ +package spacequota + +import ( + "fmt" + "strconv" + + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListSpaceQuotas struct { + ui terminal.UI + config core_config.Reader + spaceQuotaRepo space_quotas.SpaceQuotaRepository +} + +func NewListSpaceQuotas(ui terminal.UI, config core_config.Reader, spaceQuotaRepo space_quotas.SpaceQuotaRepository) (cmd *ListSpaceQuotas) { + return &ListSpaceQuotas{ + ui: ui, + config: config, + spaceQuotaRepo: spaceQuotaRepo, + } +} + +func (cmd *ListSpaceQuotas) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "space-quotas", + Description: T("List available space resource quotas"), + Usage: T("CF_NAME space-quotas"), + } +} + +func (cmd *ListSpaceQuotas) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + cmd.ui.FailWithUsage(c) + } + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + } + return +} + +func (cmd *ListSpaceQuotas) Run(c *cli.Context) { + cmd.ui.Say(T("Getting space quotas as {{.Username}}...", map[string]interface{}{"Username": terminal.EntityNameColor(cmd.config.Username())})) + + quotas, apiErr := cmd.spaceQuotaRepo.FindByOrg(cmd.config.OrganizationFields().Guid) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("name"), T("total memory limit"), T("instance memory limit"), T("routes"), T("service instances"), T("paid service plans")}) + var megabytes string + + for _, quota := range quotas { + if quota.InstanceMemoryLimit == -1 { + megabytes = T("unlimited") + } else { + megabytes = formatters.ByteSize(quota.InstanceMemoryLimit * formatters.MEGABYTE) + } + + servicesLimit := strconv.Itoa(quota.ServicesLimit) + if servicesLimit == "-1" { + servicesLimit = T("unlimited") + } + + table.Add( + quota.Name, + formatters.ByteSize(quota.MemoryLimit*formatters.MEGABYTE), + megabytes, + fmt.Sprintf("%d", quota.RoutesLimit), + fmt.Sprintf(servicesLimit), + formatters.Allowed(quota.NonBasicServicesAllowed), + ) + } + + table.Print() + +} diff --git a/cf/commands/spacequota/space_quotas_test.go b/cf/commands/spacequota/space_quotas_test.go new file mode 100644 index 00000000000..8a2a449c642 --- /dev/null +++ b/cf/commands/spacequota/space_quotas_test.go @@ -0,0 +1,134 @@ +package spacequota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("quotas command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + }) + + runCommand := func(args ...string) bool { + cmd := NewListSpaceQuotas(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + + It("requires the user to target an org", func() { + requirementsFactory.TargetedOrgSuccess = false + Expect(runCommand()).ToNot(HavePassedRequirements()) + }) + It("should fail with usage when provided any arguments", func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand("blahblah")).To(BeFalse()) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when requirements have been met", func() { + JustBeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand()).To(HavePassedRequirements()) + }) + + Context("when quotas exist", func() { + BeforeEach(func() { + quotaRepo.FindByOrgReturns([]models.SpaceQuota{ + models.SpaceQuota{ + Name: "quota-name", + MemoryLimit: 1024, + InstanceMemoryLimit: 512, + RoutesLimit: 111, + ServicesLimit: 222, + NonBasicServicesAllowed: true, + OrgGuid: "my-org-guid", + }, + models.SpaceQuota{ + Name: "quota-non-basic-not-allowed", + MemoryLimit: 434, + InstanceMemoryLimit: -1, + RoutesLimit: 1, + ServicesLimit: 2, + NonBasicServicesAllowed: false, + OrgGuid: "my-org-guid", + }, + }, nil) + }) + + It("lists quotas", func() { + Expect(quotaRepo.FindByOrgArgsForCall(0)).To(Equal("my-org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting space quotas as", "my-user"}, + []string{"OK"}, + []string{"name", "total memory limit", "instance memory limit", "routes", "service instances", "paid service plans"}, + []string{"quota-name", "1G", "512M", "111", "222", "allowed"}, + []string{"quota-non-basic-not-allowed", "434M", "unlimited", "1", "2", "disallowed"}, + )) + }) + Context("when services are unlimited", func() { + BeforeEach(func() { + quotaRepo.FindByOrgReturns([]models.SpaceQuota{ + models.SpaceQuota{ + Name: "quota-non-basic-not-allowed", + MemoryLimit: 434, + InstanceMemoryLimit: 57, + RoutesLimit: 1, + ServicesLimit: -1, + NonBasicServicesAllowed: false, + OrgGuid: "my-org-guid", + }, + }, nil) + }) + It("replaces -1 with unlimited", func() { + Expect(quotaRepo.FindByOrgArgsForCall(0)).To(Equal("my-org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + + []string{"quota-non-basic-not-allowed", "434M", "57M ", "1", "unlimited", "disallowed"}, + )) + }) + + }) + }) + + Context("when an error occurs fetching quotas", func() { + BeforeEach(func() { + quotaRepo.FindByOrgReturns([]models.SpaceQuota{}, errors.New("I haz a borken!")) + }) + + It("prints an error", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting space quotas as", "my-user"}, + []string{"FAILED"}, + )) + }) + }) + }) + +}) diff --git a/cf/commands/spacequota/spacequota_suite_test.go b/cf/commands/spacequota/spacequota_suite_test.go new file mode 100644 index 00000000000..e22720d7a12 --- /dev/null +++ b/cf/commands/spacequota/spacequota_suite_test.go @@ -0,0 +1,19 @@ +package spacequota_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSpacequota(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Spacequota Suite") +} diff --git a/cf/commands/spacequota/unset_space_quota.go b/cf/commands/spacequota/unset_space_quota.go new file mode 100644 index 00000000000..41aaced9aea --- /dev/null +++ b/cf/commands/spacequota/unset_space_quota.go @@ -0,0 +1,79 @@ +package spacequota + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnsetSpaceQuota struct { + ui terminal.UI + config core_config.Reader + quotaRepo space_quotas.SpaceQuotaRepository + spaceRepo spaces.SpaceRepository +} + +func NewUnsetSpaceQuota(ui terminal.UI, config core_config.Reader, quotaRepo space_quotas.SpaceQuotaRepository, spaceRepo spaces.SpaceRepository) UnsetSpaceQuota { + return UnsetSpaceQuota{ + ui: ui, + config: config, + quotaRepo: quotaRepo, + spaceRepo: spaceRepo, + } +} + +func (cmd UnsetSpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unset-space-quota", + Description: T("Unassign a quota from a space"), + Usage: T("CF_NAME unset-space-quota SPACE QUOTA\n\n"), + } +} + +func (cmd UnsetSpaceQuota) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + } + return +} + +func (cmd UnsetSpaceQuota) Run(c *cli.Context) { + spaceName := c.Args()[0] + quotaName := c.Args()[1] + + space, apiErr := cmd.spaceRepo.FindByName(spaceName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + quota, apiErr := cmd.quotaRepo.FindByName(quotaName) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Say(T("Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{ + "QuotaName": terminal.EntityNameColor(quota.Name), + "SpaceName": terminal.EntityNameColor(space.Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + apiErr = cmd.quotaRepo.UnassignQuotaFromSpace(space.Guid, quota.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/spacequota/unset_space_quota_test.go b/cf/commands/spacequota/unset_space_quota_test.go new file mode 100644 index 00000000000..ed3e1feb46c --- /dev/null +++ b/cf/commands/spacequota/unset_space_quota_test.go @@ -0,0 +1,93 @@ +package spacequota_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("unset-space-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + spaceRepo *testapi.FakeSpaceRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + spaceRepo = &testapi.FakeSpaceRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + cmd := NewUnsetSpaceQuota(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo, spaceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails with usage when provided too many or two few args", func() { + runCommand("space") + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("space", "quota", "extra-stuff") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Describe("requirements", func() { + It("requires the user to be logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("space", "quota")).To(BeFalse()) + }) + + It("requires the user to target an org", func() { + requirementsFactory.TargetedOrgSuccess = false + + Expect(runCommand("space", "quota")).To(BeFalse()) + }) + }) + + Context("when requirements are met", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + }) + + It("unassigns a quota from a space", func() { + space := models.Space{ + SpaceFields: models.SpaceFields{ + Name: "my-space", + Guid: "my-space-guid", + }, + } + + quota := models.SpaceQuota{Name: "my-quota", Guid: "my-quota-guid"} + + quotaRepo.FindByNameReturns(quota, nil) + spaceRepo.FindByNameName = space.Name + spaceRepo.Spaces = []models.Space{space} + + runCommand("my-space", "my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Unassigning space quota", "my-quota", "my-space", "my-user"}, + []string{"OK"}, + )) + + Expect(quotaRepo.FindByNameArgsForCall(0)).To(Equal("my-quota")) + spaceGuid, quotaGuid := quotaRepo.UnassignQuotaFromSpaceArgsForCall(0) + Expect(spaceGuid).To(Equal("my-space-guid")) + Expect(quotaGuid).To(Equal("my-quota-guid")) + }) + }) +}) diff --git a/cf/commands/spacequota/update_space_quota.go b/cf/commands/spacequota/update_space_quota.go new file mode 100644 index 00000000000..3d74bca7a2c --- /dev/null +++ b/cf/commands/spacequota/update_space_quota.go @@ -0,0 +1,134 @@ +package spacequota + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UpdateSpaceQuota struct { + ui terminal.UI + config core_config.Reader + spaceQuotaRepo space_quotas.SpaceQuotaRepository +} + +func NewUpdateSpaceQuota(ui terminal.UI, config core_config.Reader, spaceQuotaRepo space_quotas.SpaceQuotaRepository) (cmd *UpdateSpaceQuota) { + return &UpdateSpaceQuota{ + ui: ui, + config: config, + spaceQuotaRepo: spaceQuotaRepo, + } +} + +func (cmd *UpdateSpaceQuota) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "update-space-quota", + Description: T("update an existing space quota"), + Usage: T("CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("i", T("Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.")), + flag_helpers.NewStringFlag("m", T("Total amount of memory a space can have (e.g. 1024M, 1G, 10G)")), + flag_helpers.NewStringFlag("n", T("New name")), + flag_helpers.NewIntFlag("r", T("Total number of routes")), + flag_helpers.NewIntFlag("s", T("Total number of service instances")), + cli.BoolFlag{Name: "allow-paid-service-plans", Usage: T("Can provision instances of paid service plans")}, + cli.BoolFlag{Name: "disallow-paid-service-plans", Usage: T("Can not provision instances of paid service plans")}, + }, + } +} + +func (cmd *UpdateSpaceQuota) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + cmd.ui.FailWithUsage(c) + } + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + requirementsFactory.NewTargetedOrgRequirement(), + } + return +} + +func (cmd *UpdateSpaceQuota) Run(c *cli.Context) { + name := c.Args()[0] + + spaceQuota, apiErr := cmd.spaceQuotaRepo.FindByName(name) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + allowPaidServices := c.Bool("allow-paid-service-plans") + disallowPaidServices := c.Bool("disallow-paid-service-plans") + if allowPaidServices && disallowPaidServices { + cmd.ui.Failed(T("Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.")) + } + + if allowPaidServices { + spaceQuota.NonBasicServicesAllowed = true + } + + if disallowPaidServices { + spaceQuota.NonBasicServicesAllowed = false + } + + if c.String("i") != "" { + var memory int64 + var formatError error + + memFlag := c.String("i") + + if memFlag == "-1" { + memory = -1 + } else { + memory, formatError = formatters.ToMegabytes(memFlag) + if formatError != nil { + cmd.ui.FailWithUsage(c) + } + } + + spaceQuota.InstanceMemoryLimit = memory + } + + if c.String("m") != "" { + memory, formatError := formatters.ToMegabytes(c.String("m")) + + if formatError != nil { + cmd.ui.FailWithUsage(c) + } + + spaceQuota.MemoryLimit = memory + } + + if c.String("n") != "" { + spaceQuota.Name = c.String("n") + } + + if c.IsSet("s") { + spaceQuota.ServicesLimit = c.Int("s") + } + + if c.IsSet("r") { + spaceQuota.RoutesLimit = c.Int("r") + } + + cmd.ui.Say(T("Updating space quota {{.Quota}} as {{.Username}}...", + map[string]interface{}{ + "Quota": terminal.EntityNameColor(name), + "Username": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr = cmd.spaceQuotaRepo.Update(spaceQuota) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/spacequota/update_space_quota_test.go b/cf/commands/spacequota/update_space_quota_test.go new file mode 100644 index 00000000000..ca1e648076a --- /dev/null +++ b/cf/commands/spacequota/update_space_quota_test.go @@ -0,0 +1,171 @@ +package spacequota_test + +import ( + "github.com/cloudfoundry/cli/cf/api/space_quotas/fakes" + . "github.com/cloudfoundry/cli/cf/commands/spacequota" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("update-space-quota command", func() { + var ( + ui *testterm.FakeUI + quotaRepo *fakes.FakeSpaceQuotaRepository + requirementsFactory *testreq.FakeReqFactory + + quota models.SpaceQuota + quotaPaidService models.SpaceQuota + ) + + runCommand := func(args ...string) bool { + cmd := NewUpdateSpaceQuota(ui, configuration.NewRepositoryWithDefaults(), quotaRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = &testterm.FakeUI{} + quotaRepo = &fakes.FakeSpaceQuotaRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + }) + + Describe("requirements", func() { + It("fails when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + requirementsFactory.TargetedOrgSuccess = true + Expect(runCommand("my-quota", "-m", "50G")).NotTo(HavePassedRequirements()) + }) + + It("fails when the user does not have an org targeted", func() { + requirementsFactory.TargetedOrgSuccess = false + requirementsFactory.LoginSuccess = true + Expect(runCommand()).NotTo(HavePassedRequirements()) + Expect(runCommand("my-quota", "-m", "50G")).NotTo(HavePassedRequirements()) + }) + + It("fails with usage if space quota name is not provided", func() { + requirementsFactory.TargetedOrgSuccess = true + requirementsFactory.LoginSuccess = true + runCommand() + + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + quota = models.SpaceQuota{ + Guid: "my-quota-guid", + Name: "my-quota", + MemoryLimit: 1024, + InstanceMemoryLimit: 512, + RoutesLimit: 111, + ServicesLimit: 222, + NonBasicServicesAllowed: false, + OrgGuid: "my-org-guid", + } + + quotaPaidService = models.SpaceQuota{NonBasicServicesAllowed: true} + + requirementsFactory.LoginSuccess = true + requirementsFactory.TargetedOrgSuccess = true + }) + + JustBeforeEach(func() { + quotaRepo.FindByNameReturns(quota, nil) + }) + + Context("when the -m flag is provided", func() { + It("updates the memory limit", func() { + runCommand("-m", "15G", "my-quota") + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("my-quota")) + Expect(quotaRepo.UpdateArgsForCall(0).MemoryLimit).To(Equal(int64(15360))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("-m", "whoops", "wit mah hussle", "my-org") + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when the -i flag is provided", func() { + It("sets the memory limit", func() { + runCommand("-i", "50G", "my-quota") + Expect(quotaRepo.UpdateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(51200))) + }) + + It("sets the memory limit to -1", func() { + runCommand("-i", "-1", "my-quota") + Expect(quotaRepo.UpdateArgsForCall(0).InstanceMemoryLimit).To(Equal(int64(-1))) + }) + + It("alerts the user when parsing the memory limit fails", func() { + runCommand("-i", "whoops", "my-quota") + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + }) + }) + + Context("when the -r flag is provided", func() { + It("sets the route limit", func() { + runCommand("-r", "12", "ecstatic") + Expect(quotaRepo.UpdateArgsForCall(0).RoutesLimit).To(Equal(12)) + }) + }) + + Context("when the -s flag is provided", func() { + It("sets the service instance limit", func() { + runCommand("-s", "42", "my-quota") + Expect(quotaRepo.UpdateArgsForCall(0).ServicesLimit).To(Equal(42)) + }) + }) + + Context("when the -n flag is provided", func() { + It("sets the service instance name", func() { + runCommand("-n", "foo", "my-quota") + Expect(quotaRepo.UpdateArgsForCall(0).Name).To(Equal("foo")) + }) + }) + + Context("when --allow-non-basic-services is provided", func() { + It("updates the quota to allow paid service plans", func() { + runCommand("--allow-paid-service-plans", "my-for-profit-quota") + Expect(quotaRepo.UpdateArgsForCall(0).NonBasicServicesAllowed).To(BeTrue()) + }) + }) + + Context("when --disallow-non-basic-services is provided", func() { + It("updates the quota to disallow paid service plans", func() { + quotaRepo.FindByNameReturns(quotaPaidService, nil) + + runCommand("--disallow-paid-service-plans", "my-for-profit-quota") + Expect(quotaRepo.UpdateArgsForCall(0).NonBasicServicesAllowed).To(BeFalse()) + }) + }) + + Context("when updating a quota returns an error", func() { + It("alerts the user when creating the quota fails", func() { + quotaRepo.UpdateReturns(errors.New("WHOOP THERE IT IS")) + runCommand("my-quota") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Updating space quota", "my-quota", "my-user"}, + []string{"FAILED"}, + )) + }) + + It("fails if the allow and disallow flag are both passed", func() { + runCommand("--disallow-paid-service-plans", "--allow-paid-service-plans", "my-for-profit-quota") + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/stacks.go b/cf/commands/stacks.go new file mode 100644 index 00000000000..b6bb3e60f29 --- /dev/null +++ b/cf/commands/stacks.go @@ -0,0 +1,61 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf/api/stacks" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type ListStacks struct { + ui terminal.UI + config core_config.Reader + stacksRepo stacks.StackRepository +} + +func NewListStacks(ui terminal.UI, config core_config.Reader, stacksRepo stacks.StackRepository) (cmd ListStacks) { + cmd.ui = ui + cmd.config = config + cmd.stacksRepo = stacksRepo + return +} + +func (cmd ListStacks) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "stacks", + Description: T("List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)"), + Usage: T("CF_NAME stacks"), + } +} + +func (cmd ListStacks) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + return +} + +func (cmd ListStacks) Run(c *cli.Context) { + cmd.ui.Say(T("Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + map[string]interface{}{"OrganizationName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), + "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), + "Username": terminal.EntityNameColor(cmd.config.Username())})) + + stacks, apiErr := cmd.stacksRepo.FindAll() + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() + cmd.ui.Say("") + + table := terminal.NewTable(cmd.ui, []string{T("name"), T("description")}) + + for _, stack := range stacks { + table.Add(stack.Name, stack.Description) + } + + table.Print() +} diff --git a/cf/commands/stacks_test.go b/cf/commands/stacks_test.go new file mode 100644 index 00000000000..caf8608d0d2 --- /dev/null +++ b/cf/commands/stacks_test.go @@ -0,0 +1,61 @@ +package commands_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/stacks/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("stacks command", func() { + var ( + ui *testterm.FakeUI + cmd ListStacks + repo *testapi.FakeStackRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + config := testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + repo = &testapi.FakeStackRepository{} + cmd = NewListStacks(ui, config, repo) + }) + + Describe("login requirements", func() { + It("fails if the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(testcmd.RunCommand(cmd, []string{}, requirementsFactory)).To(BeFalse()) + }) + }) + + It("lists the stacks", func() { + stack1 := models.Stack{ + Name: "Stack-1", + Description: "Stack 1 Description", + } + stack2 := models.Stack{ + Name: "Stack-2", + Description: "Stack 2 Description", + } + + repo.FindAllReturns([]models.Stack{stack1, stack2}, nil) + testcmd.RunCommand(cmd, []string{}, requirementsFactory) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting stacks in org", "my-org", "my-space", "my-user"}, + []string{"OK"}, + []string{"Stack-1", "Stack 1 Description"}, + []string{"Stack-2", "Stack 2 Description"}, + )) + }) +}) diff --git a/cf/commands/target.go b/cf/commands/target.go new file mode 100644 index 00000000000..6f2ff1998ed --- /dev/null +++ b/cf/commands/target.go @@ -0,0 +1,120 @@ +package commands + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type Target struct { + ui terminal.UI + config core_config.ReadWriter + orgRepo organizations.OrganizationRepository + spaceRepo spaces.SpaceRepository +} + +func NewTarget(ui terminal.UI, + config core_config.ReadWriter, + orgRepo organizations.OrganizationRepository, + spaceRepo spaces.SpaceRepository) (cmd Target) { + + cmd.ui = ui + cmd.config = config + cmd.orgRepo = orgRepo + cmd.spaceRepo = spaceRepo + + return +} + +func (cmd Target) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "target", + ShortName: "t", + Description: T("Set or view the targeted org or space"), + Usage: T("CF_NAME target [-o ORG] [-s SPACE]"), + Flags: []cli.Flag{ + flag_helpers.NewStringFlag("o", T("organization")), + flag_helpers.NewStringFlag("s", T("space")), + }, + } +} + +func (cmd Target) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 0 { + err = errors.New(T("incorrect usage")) + cmd.ui.FailWithUsage(c) + return + } + + reqs = append(reqs, requirementsFactory.NewApiEndpointRequirement()) + if c.String("o") != "" || c.String("s") != "" { + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + } + + return +} + +func (cmd Target) Run(c *cli.Context) { + orgName := c.String("o") + spaceName := c.String("s") + + if orgName != "" { + err := cmd.setOrganization(orgName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + } + + if spaceName != "" { + err := cmd.setSpace(spaceName) + if err != nil { + cmd.ui.Failed(err.Error()) + } + } + + cmd.ui.ShowConfiguration(cmd.config) + if !cmd.config.IsLoggedIn() { + cmd.ui.PanicQuietly() + } + return +} + +func (cmd Target) setOrganization(orgName string) error { + // setting an org necessarily invalidates any space you had previously targeted + cmd.config.SetOrganizationFields(models.OrganizationFields{}) + cmd.config.SetSpaceFields(models.SpaceFields{}) + + org, apiErr := cmd.orgRepo.FindByName(orgName) + if apiErr != nil { + return errors.NewWithFmt(T("Could not target org.\n{{.ApiErr}}", + map[string]interface{}{"ApiErr": apiErr.Error()})) + } + + cmd.config.SetOrganizationFields(org.OrganizationFields) + return nil +} + +func (cmd Target) setSpace(spaceName string) error { + cmd.config.SetSpaceFields(models.SpaceFields{}) + + if !cmd.config.HasOrganization() { + return errors.New(T("An org must be targeted before targeting a space")) + } + + space, apiErr := cmd.spaceRepo.FindByName(spaceName) + if apiErr != nil { + return errors.NewWithFmt(T("Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + map[string]interface{}{"SpaceName": spaceName, "ApiErr": apiErr.Error()})) + } + + cmd.config.SetSpaceFields(space.SpaceFields) + return nil +} diff --git a/cf/commands/target_test.go b/cf/commands/target_test.go new file mode 100644 index 00000000000..f09f1017e9b --- /dev/null +++ b/cf/commands/target_test.go @@ -0,0 +1,234 @@ +package commands_test + +import ( + "errors" + + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + fake_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + . "github.com/cloudfoundry/cli/cf/commands" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("target command", func() { + var ( + orgRepo *fake_org.FakeOrganizationRepository + spaceRepo *testapi.FakeSpaceRepository + requirementsFactory *testreq.FakeReqFactory + config core_config.ReadWriter + ui *testterm.FakeUI + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + orgRepo = new(fake_org.FakeOrganizationRepository) + spaceRepo = new(testapi.FakeSpaceRepository) + requirementsFactory = new(testreq.FakeReqFactory) + config = testconfig.NewRepositoryWithDefaults() + requirementsFactory.ApiEndpointSuccess = true + }) + + var callTarget = func(args []string) bool { + cmd := NewTarget(ui, config, orgRepo, spaceRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("fails with usage when called with an argument but no flags", func() { + callTarget([]string{"some-argument"}) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Describe("when there is no api endpoint set", func() { + BeforeEach(func() { + requirementsFactory.ApiEndpointSuccess = false + }) + + It("fails requirements", func() { + Expect(callTarget([]string{})).To(BeFalse()) + }) + }) + + Describe("when the user is not logged in", func() { + BeforeEach(func() { + config.SetAccessToken("") + }) + + It("prints the target info when no org or space is specified", func() { + Expect(callTarget([]string{})).To(BeTrue()) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("panics silently so that it returns an exit code of 1", func() { + callTarget([]string{}) + Expect(ui.PanickedQuietly).To(BeTrue()) + }) + + It("fails requirements when targeting a space or org", func() { + Expect(callTarget([]string{"-o", "some-crazy-org-im-not-in"})).To(BeFalse()) + + Expect(callTarget([]string{"-s", "i-love-space"})).To(BeFalse()) + }) + }) + + Context("when the user is logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + }) + + var expectOrgToBeCleared = func() { + Expect(config.OrganizationFields()).To(Equal(models.OrganizationFields{})) + } + + var expectSpaceToBeCleared = func() { + Expect(config.SpaceFields()).To(Equal(models.SpaceFields{})) + } + + Context("there are no errors", func() { + BeforeEach(func() { + org := models.Organization{} + org.Name = "my-organization" + org.Guid = "my-organization-guid" + + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + orgRepo.FindByNameReturns(org, nil) + }) + + It("it updates the organization in the config", func() { + callTarget([]string{"-o", "my-organization"}) + + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-organization")) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + + Expect(config.OrganizationFields().Guid).To(Equal("my-organization-guid")) + }) + + It("updates the space in the config", func() { + space := models.Space{} + space.Name = "my-space" + space.Guid = "my-space-guid" + + spaceRepo.Spaces = []models.Space{space} + spaceRepo.FindByNameSpace = space + + callTarget([]string{"-s", "my-space"}) + + Expect(spaceRepo.FindByNameName).To(Equal("my-space")) + Expect(config.SpaceFields().Guid).To(Equal("my-space-guid")) + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("updates both the organization and the space in the config", func() { + space := models.Space{} + space.Name = "my-space" + space.Guid = "my-space-guid" + spaceRepo.Spaces = []models.Space{space} + + callTarget([]string{"-o", "my-organization", "-s", "my-space"}) + + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-organization")) + Expect(config.OrganizationFields().Guid).To(Equal("my-organization-guid")) + + Expect(spaceRepo.FindByNameName).To(Equal("my-space")) + Expect(config.SpaceFields().Guid).To(Equal("my-space-guid")) + + Expect(ui.ShowConfigurationCalled).To(BeTrue()) + }) + + It("only updates the organization in the config when the space can't be found", func() { + config.SetSpaceFields(models.SpaceFields{}) + + spaceRepo.FindByNameErr = true + + callTarget([]string{"-o", "my-organization", "-s", "my-space"}) + + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-organization")) + Expect(config.OrganizationFields().Guid).To(Equal("my-organization-guid")) + + Expect(spaceRepo.FindByNameName).To(Equal("my-space")) + Expect(config.SpaceFields().Guid).To(Equal("")) + + Expect(ui.ShowConfigurationCalled).To(BeFalse()) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Unable to access space", "my-space"}, + )) + }) + }) + + Context("there are errors", func() { + It("fails when the user does not have access to the specified organization", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.New("Invalid access")) + + callTarget([]string{"-o", "my-organization"}) + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + expectOrgToBeCleared() + expectSpaceToBeCleared() + }) + + It("fails when the organization is not found", func() { + orgRepo.FindByNameReturns(models.Organization{}, errors.New("my-organization not found")) + + callTarget([]string{"-o", "my-organization"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"my-organization", "not found"}, + )) + + expectOrgToBeCleared() + expectSpaceToBeCleared() + }) + + It("fails to target a space if no organization is targeted", func() { + config.SetOrganizationFields(models.OrganizationFields{}) + + callTarget([]string{"-s", "my-space"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"An org must be targeted before targeting a space"}, + )) + + expectSpaceToBeCleared() + }) + + It("fails when the user doesn't have access to the space", func() { + spaceRepo.FindByNameErr = true + + callTarget([]string{"-s", "my-space"}) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"Unable to access space", "my-space"}, + )) + + Expect(config.SpaceFields().Guid).To(Equal("")) + Expect(ui.ShowConfigurationCalled).To(BeFalse()) + + Expect(config.OrganizationFields().Guid).NotTo(BeEmpty()) + expectSpaceToBeCleared() + }) + + It("fails when the space is not found", func() { + spaceRepo.FindByNameNotFound = true + + callTarget([]string{"-s", "my-space"}) + + expectSpaceToBeCleared() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"my-space", "not found"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/user/create_user.go b/cf/commands/user/create_user.go new file mode 100644 index 00000000000..a1a319061ba --- /dev/null +++ b/cf/commands/user/create_user.go @@ -0,0 +1,71 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type CreateUser struct { + ui terminal.UI + config core_config.Reader + userRepo api.UserRepository +} + +func NewCreateUser(ui terminal.UI, config core_config.Reader, userRepo api.UserRepository) (cmd CreateUser) { + cmd.ui = ui + cmd.config = config + cmd.userRepo = userRepo + return +} + +func (cmd CreateUser) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "create-user", + Description: T("Create a new user"), + Usage: T("CF_NAME create-user USERNAME PASSWORD"), + } +} + +func (cmd CreateUser) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + + return +} + +func (cmd CreateUser) Run(c *cli.Context) { + username := c.Args()[0] + password := c.Args()[1] + + cmd.ui.Say(T("Creating user {{.TargetUser}} as {{.CurrentUser}}...", + map[string]interface{}{ + "TargetUser": terminal.EntityNameColor(username), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + err := cmd.userRepo.Create(username, password) + switch err.(type) { + case nil: + case *errors.ModelAlreadyExistsError: + cmd.ui.Warn("%s", err.Error()) + default: + cmd.ui.Failed(T("Error creating user {{.TargetUser}}.\n{{.Error}}", + map[string]interface{}{ + "TargetUser": terminal.EntityNameColor(username), + "Error": err.Error(), + })) + } + + cmd.ui.Ok() + cmd.ui.Say(T("\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", map[string]interface{}{"CurrentUser": cf.Name()})) +} diff --git a/cf/commands/user/create_user_test.go b/cf/commands/user/create_user_test.go new file mode 100644 index 00000000000..33da4198a53 --- /dev/null +++ b/cf/commands/user/create_user_test.go @@ -0,0 +1,89 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("Create user command", func() { + var ( + requirementsFactory *testreq.FakeReqFactory + ui *testterm.FakeUI + userRepo *testapi.FakeUserRepository + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + ui = new(testterm.FakeUI) + userRepo = &testapi.FakeUserRepository{} + configRepo = testconfig.NewRepositoryWithDefaults() + accessToken, _ := testconfig.EncodeAccessToken(core_config.TokenInfo{ + Username: "current-user", + }) + configRepo.SetAccessToken(accessToken) + }) + + runCommand := func(args ...string) bool { + cmd := NewCreateUser(ui, configRepo, userRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + It("creates a user", func() { + runCommand("my-user", "my-password") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Creating user", "my-user", "current-user"}, + []string{"OK"}, + []string{"TIP"}, + )) + + Expect(userRepo.CreateUserUsername).To(Equal("my-user")) + }) + + Context("when creating the user returns an error", func() { + It("prints a warning when the given user already exists", func() { + userRepo.CreateUserExists = true + + runCommand("my-user", "my-password") + + Expect(ui.WarnOutputs).To(ContainSubstrings( + []string{"already exists"}, + )) + + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"FAILED"})) + }) + It("fails when any error other than alreadyExists is returned", func() { + userRepo.CreateUserReturnsHttpError = true + + runCommand("my-user", "my-password") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Forbidden"}, + )) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"FAILED"})) + + }) + }) + + It("fails when no arguments are passed", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when the user is not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("my-user", "my-password")).To(BeFalse()) + }) +}) diff --git a/cf/commands/user/delete_user.go b/cf/commands/user/delete_user.go new file mode 100644 index 00000000000..baf6dd2f7a5 --- /dev/null +++ b/cf/commands/user/delete_user.go @@ -0,0 +1,83 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type DeleteUser struct { + ui terminal.UI + config core_config.Reader + userRepo api.UserRepository +} + +func NewDeleteUser(ui terminal.UI, config core_config.Reader, userRepo api.UserRepository) (cmd DeleteUser) { + cmd.ui = ui + cmd.config = config + cmd.userRepo = userRepo + return +} + +func (cmd DeleteUser) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "delete-user", + Description: T("Delete a user"), + Usage: T("CF_NAME delete-user USERNAME [-f]"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: T("Force deletion without confirmation")}, + }, + } +} + +func (cmd DeleteUser) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + err = errors.New(T("Invalid usage")) + cmd.ui.FailWithUsage(c) + return + } + + reqs = append(reqs, requirementsFactory.NewLoginRequirement()) + + return +} + +func (cmd DeleteUser) Run(c *cli.Context) { + username := c.Args()[0] + force := c.Bool("f") + + if !force && !cmd.ui.ConfirmDelete(T("user"), username) { + return + } + + cmd.ui.Say(T("Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + map[string]interface{}{ + "TargetUser": terminal.EntityNameColor(username), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + user, apiErr := cmd.userRepo.FindByUsername(username) + switch apiErr.(type) { + case nil: + case *errors.ModelNotFoundError: + cmd.ui.Ok() + cmd.ui.Warn(T("User {{.TargetUser}} does not exist.", map[string]interface{}{"TargetUser": username})) + return + default: + cmd.ui.Failed(apiErr.Error()) + return + } + + apiErr = cmd.userRepo.Delete(user.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/user/delete_user_test.go b/cf/commands/user/delete_user_test.go new file mode 100644 index 00000000000..b2cde413710 --- /dev/null +++ b/cf/commands/user/delete_user_test.go @@ -0,0 +1,122 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("delete-user command", func() { + var ( + ui *testterm.FakeUI + configRepo core_config.ReadWriter + userRepo *testapi.FakeUserRepository + requirementsFactory *testreq.FakeReqFactory + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{Inputs: []string{"y"}} + userRepo = &testapi.FakeUserRepository{} + requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true} + configRepo = testconfig.NewRepositoryWithDefaults() + + token, err := testconfig.EncodeAccessToken(core_config.TokenInfo{ + UserGuid: "admin-user-guid", + Username: "admin-user", + }) + Expect(err).ToNot(HaveOccurred()) + configRepo.SetAccessToken(token) + }) + + runCommand := func(args ...string) bool { + cmd := NewDeleteUser(ui, configRepo, userRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("my-user")).To(BeFalse()) + }) + + It("fails with usage when no arguments are given", func() { + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when the given user exists", func() { + BeforeEach(func() { + userRepo.FindByUsernameUserFields = models.UserFields{ + Username: "user-name", + Guid: "user-guid", + } + }) + + It("deletes a user with the given name", func() { + runCommand("user-name") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete the user user-name"})) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting user", "user-name", "admin-user"}, + []string{"OK"}, + )) + + Expect(userRepo.FindByUsernameUsername).To(Equal("user-name")) + Expect(userRepo.DeleteUserGuid).To(Equal("user-guid")) + }) + + It("does not delete the user when no confirmation is given", func() { + ui.Inputs = []string{"nope"} + runCommand("user") + + Expect(ui.Prompts).To(ContainSubstrings([]string{"Really delete"})) + Expect(userRepo.FindByUsernameUsername).To(Equal("")) + Expect(userRepo.DeleteUserGuid).To(Equal("")) + }) + + It("deletes without confirmation when the -f flag is given", func() { + ui.Inputs = []string{} + runCommand("-f", "user-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting user", "user-name"}, + []string{"OK"}, + )) + + Expect(userRepo.FindByUsernameUsername).To(Equal("user-name")) + Expect(userRepo.DeleteUserGuid).To(Equal("user-guid")) + }) + }) + + Context("when the given user does not exist", func() { + BeforeEach(func() { + userRepo.FindByUsernameNotFound = true + }) + + It("prints a warning", func() { + runCommand("-f", "user-name") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Deleting user", "user-name"}, + []string{"OK"}, + )) + + Expect(ui.WarnOutputs).To(ContainSubstrings([]string{"user-name", "does not exist"})) + + Expect(userRepo.FindByUsernameUsername).To(Equal("user-name")) + Expect(userRepo.DeleteUserGuid).To(Equal("")) + }) + }) +}) diff --git a/cf/commands/user/org_users.go b/cf/commands/user/org_users.go new file mode 100644 index 00000000000..c900fd5ce66 --- /dev/null +++ b/cf/commands/user/org_users.go @@ -0,0 +1,100 @@ +package user + +import ( + "errors" + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +var orgRoles = []string{models.ORG_MANAGER, models.BILLING_MANAGER, models.ORG_AUDITOR} + +type OrgUsers struct { + ui terminal.UI + config core_config.Reader + orgReq requirements.OrganizationRequirement + userRepo api.UserRepository +} + +func NewOrgUsers(ui terminal.UI, config core_config.Reader, userRepo api.UserRepository) (cmd *OrgUsers) { + cmd = new(OrgUsers) + cmd.ui = ui + cmd.config = config + cmd.userRepo = userRepo + return +} + +func (cmd *OrgUsers) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "org-users", + Description: T("Show org users by role"), + Usage: T("CF_NAME org-users ORG"), + Flags: []cli.Flag{ + cli.BoolFlag{Name: "a", Usage: T("List all users in the org")}, + }, + } +} + +func (cmd *OrgUsers) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 1 { + err = errors.New(T("Incorrect usage")) + cmd.ui.FailWithUsage(c) + return + } + + orgName := c.Args()[0] + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(orgName) + reqs = append(reqs, requirementsFactory.NewLoginRequirement(), cmd.orgReq) + + return +} + +func (cmd *OrgUsers) Run(c *cli.Context) { + org := cmd.orgReq.GetOrganization() + all := c.Bool("a") + + cmd.ui.Say(T("Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + map[string]interface{}{ + "TargetOrg": terminal.EntityNameColor(org.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + roles := orgRoles + if all { + roles = []string{models.ORG_USER} + } + + var orgRoleToDisplayName = map[string]string{ + models.ORG_USER: T("USERS"), + models.ORG_MANAGER: T("ORG MANAGER"), + models.BILLING_MANAGER: T("BILLING MANAGER"), + models.ORG_AUDITOR: T("ORG AUDITOR"), + } + + for _, role := range roles { + displayName := orgRoleToDisplayName[role] + + users, apiErr := cmd.userRepo.ListUsersInOrgForRole(org.Guid, role) + + cmd.ui.Say("") + cmd.ui.Say("%s", terminal.HeaderColor(displayName)) + + for _, user := range users { + cmd.ui.Say(" %s", user.Username) + } + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + map[string]interface{}{ + "Error": apiErr.Error(), + "OrgRoleToDisplayName": displayName, + })) + return + } + } +} diff --git a/cf/commands/user/org_users_test.go b/cf/commands/user/org_users_test.go new file mode 100644 index 00000000000..20f509010d4 --- /dev/null +++ b/cf/commands/user/org_users_test.go @@ -0,0 +1,114 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("org-users command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + configRepo core_config.ReadWriter + userRepo *testapi.FakeUserRepository + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + userRepo = &testapi.FakeUserRepository{} + configRepo = testconfig.NewRepositoryWithDefaults() + requirementsFactory = &testreq.FakeReqFactory{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewOrgUsers(ui, configRepo, userRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails with usage when invoked without an org name", func() { + requirementsFactory.LoginSuccess = true + + runCommand() + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails when not logged in", func() { + Expect(runCommand("say-hello-to-my-little-org")).To(BeFalse()) + }) + }) + + Context("when logged in and given an org with users", func() { + BeforeEach(func() { + org := models.Organization{} + org.Name = "the-org" + org.Guid = "the-org-guid" + + user := models.UserFields{} + user.Username = "user1" + user2 := models.UserFields{} + user2.Username = "user2" + user3 := models.UserFields{} + user3.Username = "user3" + user4 := models.UserFields{} + user4.Username = "user4" + userRepo.ListUsersByRole = map[string][]models.UserFields{ + models.ORG_MANAGER: []models.UserFields{user, user2}, + models.BILLING_MANAGER: []models.UserFields{user4}, + models.ORG_AUDITOR: []models.UserFields{user3}, + } + + requirementsFactory.LoginSuccess = true + requirementsFactory.Organization = org + }) + + It("shows the special users in the given org", func() { + runCommand("the-org") + + Expect(userRepo.ListUsersOrganizationGuid).To(Equal("the-org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting users in org", "the-org", "my-user"}, + []string{"ORG MANAGER"}, + []string{"user1"}, + []string{"user2"}, + []string{"BILLING MANAGER"}, + []string{"user4"}, + []string{"ORG AUDITOR"}, + []string{"user3"}, + )) + }) + + Context("when the -a flag is provided", func() { + BeforeEach(func() { + user := models.UserFields{} + user.Username = "user1" + user2 := models.UserFields{} + user2.Username = "user2" + userRepo.ListUsersByRole = map[string][]models.UserFields{ + models.ORG_USER: []models.UserFields{user, user2}, + } + }) + + It("lists all org users, regardless of role", func() { + runCommand("-a", "the-org") + + Expect(userRepo.ListUsersOrganizationGuid).To(Equal("the-org-guid")) + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting users in org", "the-org", "my-user"}, + []string{"USERS"}, + []string{"user1"}, + []string{"user2"}, + )) + }) + }) + }) +}) diff --git a/cf/commands/user/set_org_role.go b/cf/commands/user/set_org_role.go new file mode 100644 index 00000000000..ee084d2e062 --- /dev/null +++ b/cf/commands/user/set_org_role.go @@ -0,0 +1,79 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SetOrgRole struct { + ui terminal.UI + config core_config.Reader + userRepo api.UserRepository + userReq requirements.UserRequirement + orgReq requirements.OrganizationRequirement +} + +func NewSetOrgRole(ui terminal.UI, config core_config.Reader, userRepo api.UserRepository) (cmd *SetOrgRole) { + cmd = new(SetOrgRole) + cmd.ui = ui + cmd.config = config + cmd.userRepo = userRepo + return +} + +func (cmd *SetOrgRole) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-org-role", + Description: T("Assign an org role to a user"), + Usage: T("CF_NAME set-org-role USERNAME ORG ROLE\n\n") + + T("ROLES:\n") + + T(" OrgManager - Invite and manage users, select and change plans, and set spending limits\n") + + T(" BillingManager - Create and manage the billing account and payment info\n") + + T(" OrgAuditor - Read-only access to org info and reports\n"), + } +} + +func (cmd *SetOrgRole) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + } + + cmd.userReq = requirementsFactory.NewUserRequirement(c.Args()[0]) + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[1]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.userReq, + cmd.orgReq, + } + + return +} + +func (cmd *SetOrgRole) Run(c *cli.Context) { + user := cmd.userReq.GetUser() + org := cmd.orgReq.GetOrganization() + role := models.UserInputToOrgRole[c.Args()[2]] + + cmd.ui.Say(T("Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + map[string]interface{}{ + "Role": terminal.EntityNameColor(role), + "TargetUser": terminal.EntityNameColor(user.Username), + "TargetOrg": terminal.EntityNameColor(org.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr := cmd.userRepo.SetOrgRole(user.Guid, org.Guid, role) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/user/set_org_role_test.go b/cf/commands/user/set_org_role_test.go new file mode 100644 index 00000000000..5c87105c0d7 --- /dev/null +++ b/cf/commands/user/set_org_role_test.go @@ -0,0 +1,80 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +func callSetOrgRole(args []string, requirementsFactory *testreq.FakeReqFactory, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { + ui = new(testterm.FakeUI) + config := testconfig.NewRepositoryWithDefaults() + cmd := NewSetOrgRole(ui, config, userRepo) + testcmd.RunCommand(cmd, args, requirementsFactory) + return +} + +var _ = Describe("set-org-role command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + userRepo *testapi.FakeUserRepository + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + userRepo = &testapi.FakeUserRepository{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewSetOrgRole(ui, configRepo, userRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + Expect(runCommand("hey", "there", "jude")).To(BeFalse()) + }) + + It("fails with usage when not provided exactly three args", func() { + runCommand("one fish", "two fish") // red fish, blue fish + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + org := models.Organization{} + org.Guid = "my-org-guid" + org.Name = "my-org" + requirementsFactory.UserFields = models.UserFields{Guid: "my-user-guid", Username: "my-user"} + requirementsFactory.Organization = org + }) + + It("sets the given org role on the given user", func() { + runCommand("some-user", "some-org", "OrgManager") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning role", "OrgManager", "my-user", "my-org", "my-user"}, + []string{"OK"}, + )) + Expect(userRepo.SetOrgRoleUserGuid).To(Equal("my-user-guid")) + Expect(userRepo.SetOrgRoleOrganizationGuid).To(Equal("my-org-guid")) + Expect(userRepo.SetOrgRoleRole).To(Equal(models.ORG_MANAGER)) + }) + }) +}) diff --git a/cf/commands/user/set_space_role.go b/cf/commands/user/set_space_role.go new file mode 100644 index 00000000000..9ac8d30a92c --- /dev/null +++ b/cf/commands/user/set_space_role.go @@ -0,0 +1,104 @@ +package user + +import ( + "errors" + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type SpaceRoleSetter interface { + SetSpaceRole(space models.Space, role, userGuid, userName string) (err error) +} + +type SetSpaceRole struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository + userRepo api.UserRepository + userReq requirements.UserRequirement + orgReq requirements.OrganizationRequirement +} + +func NewSetSpaceRole(ui terminal.UI, config core_config.Reader, spaceRepo spaces.SpaceRepository, userRepo api.UserRepository) (cmd *SetSpaceRole) { + cmd = new(SetSpaceRole) + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + cmd.userRepo = userRepo + return +} + +func (cmd *SetSpaceRole) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "set-space-role", + Description: T("Assign a space role to a user"), + Usage: T("CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n") + + T("ROLES:\n") + + T(" SpaceManager - Invite and manage users, and enable features for a given space\n") + + T(" SpaceDeveloper - Create and manage apps and services, and see logs and reports\n") + + T(" SpaceAuditor - View logs, reports, and settings on this space\n"), + } +} + +func (cmd *SetSpaceRole) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 4 { + cmd.ui.FailWithUsage(c) + } + + cmd.userReq = requirementsFactory.NewUserRequirement(c.Args()[0]) + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[1]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.userReq, + cmd.orgReq, + } + return +} + +func (cmd *SetSpaceRole) Run(c *cli.Context) { + spaceName := c.Args()[2] + role := models.UserInputToSpaceRole[c.Args()[3]] + user := cmd.userReq.GetUser() + org := cmd.orgReq.GetOrganization() + + space, apiErr := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + err := cmd.SetSpaceRole(space, role, user.Guid, user.Username) + if err != nil { + cmd.ui.Failed(err.Error()) + return + } +} + +func (cmd *SetSpaceRole) SetSpaceRole(space models.Space, role, userGuid, userName string) (err error) { + cmd.ui.Say(T("Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + map[string]interface{}{ + "Role": terminal.EntityNameColor(role), + "TargetUser": terminal.EntityNameColor(userName), + "TargetOrg": terminal.EntityNameColor(space.Organization.Name), + "TargetSpace": terminal.EntityNameColor(space.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr := cmd.userRepo.SetSpaceRole(userGuid, space.Guid, space.Organization.Guid, role) + if apiErr != nil { + err = errors.New(apiErr.Error()) + return + } + + cmd.ui.Ok() + return +} diff --git a/cf/commands/user/set_space_role_test.go b/cf/commands/user/set_space_role_test.go new file mode 100644 index 00000000000..c1f8e3c4447 --- /dev/null +++ b/cf/commands/user/set_space_role_test.go @@ -0,0 +1,101 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("set-space-role command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + spaceRepo *testapi.FakeSpaceRepository + userRepo *testapi.FakeUserRepository + configRepo core_config.ReadWriter + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + accessToken, err := testconfig.EncodeAccessToken(core_config.TokenInfo{Username: "current-user"}) + Expect(err).NotTo(HaveOccurred()) + configRepo.SetAccessToken(accessToken) + + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + spaceRepo = &testapi.FakeSpaceRepository{} + userRepo = &testapi.FakeUserRepository{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewSetSpaceRole(ui, configRepo, spaceRepo, userRepo), args, requirementsFactory) + } + + It("fails with usage when not provided exactly four args", func() { + runCommand("foo", "bar", "baz") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("does not fail with usage when provided four args", func() { + runCommand("whatever", "these", "are", "args") + Expect(ui.FailedWithUsage).To(BeFalse()) + }) + + Describe("requirements", func() { + It("fails when not logged in", func() { + Expect(runCommand("username", "org", "space", "role")).To(BeFalse()) + }) + + It("succeeds when logged in", func() { + requirementsFactory.LoginSuccess = true + passed := runCommand("username", "org", "space", "role") + + Expect(passed).To(BeTrue()) + Expect(requirementsFactory.UserUsername).To(Equal("username")) + Expect(requirementsFactory.OrganizationName).To(Equal("org")) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + org := models.Organization{} + org.Guid = "my-org-guid" + org.Name = "my-org" + + requirementsFactory.UserFields = models.UserFields{Guid: "my-user-guid", Username: "my-user"} + requirementsFactory.Organization = org + + spaceRepo.FindByNameInOrgSpace = models.Space{} + spaceRepo.FindByNameInOrgSpace.Guid = "my-space-guid" + spaceRepo.FindByNameInOrgSpace.Name = "my-space" + spaceRepo.FindByNameInOrgSpace.Organization = org.OrganizationFields + }) + + It("sets the given space role on the given user", func() { + runCommand("some-user", "some-org", "some-space", "SpaceManager") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Assigning role ", "SpaceManager", "my-user", "my-org", "my-space", "current-user"}, + []string{"OK"}, + )) + + Expect(spaceRepo.FindByNameInOrgName).To(Equal("some-space")) + Expect(spaceRepo.FindByNameInOrgOrgGuid).To(Equal("my-org-guid")) + + Expect(userRepo.SetSpaceRoleUserGuid).To(Equal("my-user-guid")) + Expect(userRepo.SetSpaceRoleSpaceGuid).To(Equal("my-space-guid")) + Expect(userRepo.SetSpaceRoleRole).To(Equal(models.SPACE_MANAGER)) + }) + }) +}) diff --git a/cf/commands/user/space_users.go b/cf/commands/user/space_users.go new file mode 100644 index 00000000000..4daac9a94e8 --- /dev/null +++ b/cf/commands/user/space_users.go @@ -0,0 +1,97 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +var spaceRoles = []string{models.SPACE_MANAGER, models.SPACE_DEVELOPER, models.SPACE_AUDITOR} + +type SpaceUsers struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository + userRepo api.UserRepository + orgReq requirements.OrganizationRequirement +} + +func NewSpaceUsers(ui terminal.UI, config core_config.Reader, spaceRepo spaces.SpaceRepository, userRepo api.UserRepository) (cmd *SpaceUsers) { + cmd = new(SpaceUsers) + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + cmd.userRepo = userRepo + return +} + +func (cmd *SpaceUsers) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "space-users", + Description: T("Show space users by role"), + Usage: T("CF_NAME space-users ORG SPACE"), + } +} + +func (cmd *SpaceUsers) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 2 { + cmd.ui.FailWithUsage(c) + } + + orgName := c.Args()[0] + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(orgName) + reqs = append(reqs, requirementsFactory.NewLoginRequirement(), cmd.orgReq) + + return +} + +func (cmd *SpaceUsers) Run(c *cli.Context) { + spaceName := c.Args()[1] + org := cmd.orgReq.GetOrganization() + + space, apiErr := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + } + + cmd.ui.Say(T("Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + map[string]interface{}{ + "TargetOrg": terminal.EntityNameColor(org.Name), + "TargetSpace": terminal.EntityNameColor(space.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + var spaceRoleToDisplayName = map[string]string{ + models.SPACE_MANAGER: T("SPACE MANAGER"), + models.SPACE_DEVELOPER: T("SPACE DEVELOPER"), + models.SPACE_AUDITOR: T("SPACE AUDITOR"), + } + + for _, role := range spaceRoles { + displayName := spaceRoleToDisplayName[role] + + users, apiErr := cmd.userRepo.ListUsersInSpaceForRole(space.Guid, role) + + cmd.ui.Say("") + cmd.ui.Say("%s", terminal.HeaderColor(displayName)) + + for _, user := range users { + cmd.ui.Say(" %s", user.Username) + } + + if apiErr != nil { + cmd.ui.Failed(T("Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + map[string]interface{}{ + "Error": apiErr.Error(), + "SpaceRoleToDisplayName": displayName, + })) + return + } + } +} diff --git a/cf/commands/user/space_users_test.go b/cf/commands/user/space_users_test.go new file mode 100644 index 00000000000..a13fb5a19c3 --- /dev/null +++ b/cf/commands/user/space_users_test.go @@ -0,0 +1,106 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("space-users command", func() { + var ( + ui *testterm.FakeUI + requirementsFactory *testreq.FakeReqFactory + spaceRepo *testapi.FakeSpaceRepository + userRepo *testapi.FakeUserRepository + config core_config.ReadWriter + ) + + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + ui = &testterm.FakeUI{} + requirementsFactory = &testreq.FakeReqFactory{} + spaceRepo = &testapi.FakeSpaceRepository{} + userRepo = &testapi.FakeUserRepository{} + }) + + runCommand := func(args ...string) bool { + return testcmd.RunCommand(NewSpaceUsers(ui, config, spaceRepo, userRepo), args, requirementsFactory) + } + + Describe("requirements", func() { + It("fails when not logged in", func() { + Expect(runCommand("my-org", "my-space")).To(BeFalse()) + }) + + It("succeeds when logged in", func() { + requirementsFactory.LoginSuccess = true + passed := runCommand("some-org", "whatever-space") + + Expect(passed).To(BeTrue()) + Expect("some-org").To(Equal(requirementsFactory.OrganizationName)) + }) + }) + + It("fails with usage when not invoked with exactly two args", func() { + runCommand("my-org") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Context("when logged in and given some users in the org and space", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + org := models.Organization{} + org.Name = "Org1" + org.Guid = "org1-guid" + space := models.Space{} + space.Name = "Space1" + space.Guid = "space1-guid" + + requirementsFactory.Organization = org + spaceRepo.FindByNameInOrgSpace = space + + user := models.UserFields{} + user.Username = "user1" + user2 := models.UserFields{} + user2.Username = "user2" + user3 := models.UserFields{} + user3.Username = "user3" + user4 := models.UserFields{} + user4.Username = "user4" + userRepo.ListUsersByRole = map[string][]models.UserFields{ + models.SPACE_MANAGER: []models.UserFields{user, user2}, + models.SPACE_DEVELOPER: []models.UserFields{user4}, + models.SPACE_AUDITOR: []models.UserFields{user3}, + } + }) + + It("tells you about the space users in the given space", func() { + runCommand("my-org", "my-space") + + Expect(spaceRepo.FindByNameInOrgName).To(Equal("my-space")) + Expect(spaceRepo.FindByNameInOrgOrgGuid).To(Equal("org1-guid")) + Expect(userRepo.ListUsersSpaceGuid).To(Equal("space1-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Getting users in org", "Org1", "Space1", "my-user"}, + []string{"SPACE MANAGER"}, + []string{"user1"}, + []string{"user2"}, + []string{"SPACE DEVELOPER"}, + []string{"user4"}, + []string{"SPACE AUDITOR"}, + []string{"user3"}, + )) + }) + }) +}) diff --git a/cf/commands/user/unset_org_role.go b/cf/commands/user/unset_org_role.go new file mode 100644 index 00000000000..42ad94c90b5 --- /dev/null +++ b/cf/commands/user/unset_org_role.go @@ -0,0 +1,81 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnsetOrgRole struct { + ui terminal.UI + config core_config.Reader + userRepo api.UserRepository + userReq requirements.UserRequirement + orgReq requirements.OrganizationRequirement +} + +func NewUnsetOrgRole(ui terminal.UI, config core_config.Reader, userRepo api.UserRepository) (cmd *UnsetOrgRole) { + cmd = new(UnsetOrgRole) + cmd.ui = ui + cmd.config = config + cmd.userRepo = userRepo + + return +} + +func (cmd *UnsetOrgRole) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unset-org-role", + Description: T("Remove an org role from a user"), + Usage: T("CF_NAME unset-org-role USERNAME ORG ROLE\n\n") + + T("ROLES:\n") + + T(" OrgManager - Invite and manage users, select and change plans, and set spending limits\n") + + T(" BillingManager - Create and manage the billing account and payment info\n") + + T(" OrgAuditor - Read-only access to org info and reports\n"), + } +} + +func (cmd *UnsetOrgRole) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 3 { + cmd.ui.FailWithUsage(c) + } + + cmd.userReq = requirementsFactory.NewUserRequirement(c.Args()[0]) + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[1]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.userReq, + cmd.orgReq, + } + + return +} + +func (cmd *UnsetOrgRole) Run(c *cli.Context) { + role := models.UserInputToOrgRole[c.Args()[2]] + user := cmd.userReq.GetUser() + org := cmd.orgReq.GetOrganization() + + cmd.ui.Say(T("Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + map[string]interface{}{ + "Role": terminal.EntityNameColor(role), + "TargetUser": terminal.EntityNameColor(c.Args()[0]), + "TargetOrg": terminal.EntityNameColor(c.Args()[1]), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr := cmd.userRepo.UnsetOrgRole(user.Guid, org.Guid, role) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/user/unset_org_role_test.go b/cf/commands/user/unset_org_role_test.go new file mode 100644 index 00000000000..923327313dc --- /dev/null +++ b/cf/commands/user/unset_org_role_test.go @@ -0,0 +1,91 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("unset-org-role command", func() { + var ( + ui *testterm.FakeUI + userRepo *testapi.FakeUserRepository + configRepo core_config.ReadWriter + requirementsFactory *testreq.FakeReqFactory + ) + + runCommand := func(args ...string) bool { + cmd := NewUnsetOrgRole(ui, configRepo, userRepo) + return testcmd.RunCommand(cmd, args, requirementsFactory) + } + + BeforeEach(func() { + ui = &testterm.FakeUI{} + userRepo = &testapi.FakeUserRepository{} + requirementsFactory = &testreq.FakeReqFactory{} + configRepo = testconfig.NewRepositoryWithDefaults() + }) + + It("fails with usage when invoked without exactly three args", func() { + runCommand("username", "org") + Expect(ui.FailedWithUsage).To(BeTrue()) + + runCommand("woah", "too", "many", "args") + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + Describe("requirements", func() { + It("fails when not logged in", func() { + requirementsFactory.LoginSuccess = false + + Expect(runCommand("username", "org", "role")).To(BeFalse()) + }) + + It("succeeds when logged in", func() { + requirementsFactory.LoginSuccess = true + passed := runCommand("username", "org", "role") + Expect(passed).To(BeTrue()) + + Expect(requirementsFactory.UserUsername).To(Equal("username")) + Expect(requirementsFactory.OrganizationName).To(Equal("org")) + }) + }) + + Context("when logged in", func() { + BeforeEach(func() { + requirementsFactory.LoginSuccess = true + + user := models.UserFields{} + user.Username = "some-user" + user.Guid = "some-user-guid" + org := models.Organization{} + org.Name = "some-org" + org.Guid = "some-org-guid" + + requirementsFactory.UserFields = user + requirementsFactory.Organization = org + }) + + It("unsets a user's org role", func() { + runCommand("my-username", "my-org", "OrgManager") + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing role", "OrgManager", "my-username", "my-org", "my-user"}, + []string{"OK"}, + )) + + Expect(userRepo.UnsetOrgRoleRole).To(Equal(models.ORG_MANAGER)) + Expect(userRepo.UnsetOrgRoleUserGuid).To(Equal("some-user-guid")) + Expect(userRepo.UnsetOrgRoleOrganizationGuid).To(Equal("some-org-guid")) + }) + }) +}) diff --git a/cf/commands/user/unset_space_role.go b/cf/commands/user/unset_space_role.go new file mode 100644 index 00000000000..9f84dc280f2 --- /dev/null +++ b/cf/commands/user/unset_space_role.go @@ -0,0 +1,91 @@ +package user + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +type UnsetSpaceRole struct { + ui terminal.UI + config core_config.Reader + spaceRepo spaces.SpaceRepository + userRepo api.UserRepository + userReq requirements.UserRequirement + orgReq requirements.OrganizationRequirement +} + +func NewUnsetSpaceRole(ui terminal.UI, config core_config.Reader, spaceRepo spaces.SpaceRepository, userRepo api.UserRepository) (cmd *UnsetSpaceRole) { + cmd = new(UnsetSpaceRole) + cmd.ui = ui + cmd.config = config + cmd.spaceRepo = spaceRepo + cmd.userRepo = userRepo + return +} + +func (cmd *UnsetSpaceRole) Metadata() command_metadata.CommandMetadata { + return command_metadata.CommandMetadata{ + Name: "unset-space-role", + Description: T("Remove a space role from a user"), + Usage: T("CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n") + + T("ROLES:\n") + + T(" SpaceManager - Invite and manage users, and enable features for a given space\n") + + T(" SpaceDeveloper - Create and manage apps and services, and see logs and reports\n") + + T(" SpaceAuditor - View logs, reports, and settings on this space\n"), + } +} + +func (cmd *UnsetSpaceRole) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { + if len(c.Args()) != 4 { + cmd.ui.FailWithUsage(c) + } + + cmd.userReq = requirementsFactory.NewUserRequirement(c.Args()[0]) + cmd.orgReq = requirementsFactory.NewOrganizationRequirement(c.Args()[1]) + + reqs = []requirements.Requirement{ + requirementsFactory.NewLoginRequirement(), + cmd.userReq, + cmd.orgReq, + } + + return +} + +func (cmd *UnsetSpaceRole) Run(c *cli.Context) { + spaceName := c.Args()[2] + role := models.UserInputToSpaceRole[c.Args()[3]] + + user := cmd.userReq.GetUser() + org := cmd.orgReq.GetOrganization() + space, apiErr := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Say(T("Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + map[string]interface{}{ + "Role": terminal.EntityNameColor(role), + "TargetUser": terminal.EntityNameColor(user.Username), + "TargetOrg": terminal.EntityNameColor(org.Name), + "TargetSpace": terminal.EntityNameColor(space.Name), + "CurrentUser": terminal.EntityNameColor(cmd.config.Username()), + })) + + apiErr = cmd.userRepo.UnsetSpaceRole(user.Guid, space.Guid, role) + + if apiErr != nil { + cmd.ui.Failed(apiErr.Error()) + return + } + + cmd.ui.Ok() +} diff --git a/cf/commands/user/unset_space_role_test.go b/cf/commands/user/unset_space_role_test.go new file mode 100644 index 00000000000..05a634d5e4c --- /dev/null +++ b/cf/commands/user/unset_space_role_test.go @@ -0,0 +1,80 @@ +package user_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + . "github.com/cloudfoundry/cli/cf/commands/user" + "github.com/cloudfoundry/cli/cf/models" + testcmd "github.com/cloudfoundry/cli/testhelpers/commands" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testreq "github.com/cloudfoundry/cli/testhelpers/requirements" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("unset-space-role command", func() { + It("fails with usage when not called with exactly four args", func() { + requirementsFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() + + ui, _ := callUnsetSpaceRole([]string{"username", "org", "space"}, spaceRepo, userRepo, requirementsFactory) + Expect(ui.FailedWithUsage).To(BeTrue()) + }) + + It("fails requirements when not logged in", func() { + requirementsFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() + args := []string{"username", "org", "space", "role"} + + requirementsFactory.LoginSuccess = false + _, passed := callUnsetSpaceRole(args, spaceRepo, userRepo, requirementsFactory) + Expect(passed).To(BeFalse()) + }) + + It("unsets the user's space role", func() { + user := models.UserFields{} + user.Username = "some-user" + user.Guid = "some-user-guid" + org := models.Organization{} + org.Name = "some-org" + org.Guid = "some-org-guid" + + requirementsFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() + requirementsFactory.LoginSuccess = true + requirementsFactory.UserFields = user + requirementsFactory.Organization = org + spaceRepo.FindByNameInOrgSpace = models.Space{} + spaceRepo.FindByNameInOrgSpace.Name = "some-space" + spaceRepo.FindByNameInOrgSpace.Guid = "some-space-guid" + + args := []string{"my-username", "my-org", "my-space", "SpaceManager"} + + ui, _ := callUnsetSpaceRole(args, spaceRepo, userRepo, requirementsFactory) + + Expect(spaceRepo.FindByNameInOrgName).To(Equal("my-space")) + Expect(spaceRepo.FindByNameInOrgOrgGuid).To(Equal("some-org-guid")) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"Removing role", "SpaceManager", "some-user", "some-org", "some-space", "my-user"}, + []string{"OK"}, + )) + Expect(userRepo.UnsetSpaceRoleRole).To(Equal(models.SPACE_MANAGER)) + Expect(userRepo.UnsetSpaceRoleUserGuid).To(Equal("some-user-guid")) + Expect(userRepo.UnsetSpaceRoleSpaceGuid).To(Equal("some-space-guid")) + }) +}) + +func getUnsetSpaceRoleDeps() (requirementsFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository) { + requirementsFactory = &testreq.FakeReqFactory{} + spaceRepo = &testapi.FakeSpaceRepository{} + userRepo = &testapi.FakeUserRepository{} + return +} + +func callUnsetSpaceRole(args []string, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository, requirementsFactory *testreq.FakeReqFactory) (*testterm.FakeUI, bool) { + ui := &testterm.FakeUI{} + config := testconfig.NewRepositoryWithDefaults() + cmd := NewUnsetSpaceRole(ui, config, spaceRepo, userRepo) + passed := testcmd.RunCommand(cmd, args, requirementsFactory) + return ui, passed +} diff --git a/cf/commands/user/user_suite_test.go b/cf/commands/user/user_suite_test.go new file mode 100644 index 00000000000..84c8109a6c2 --- /dev/null +++ b/cf/commands/user/user_suite_test.go @@ -0,0 +1,19 @@ +package user_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestUser(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "User Suite") +} diff --git a/cf/configuration/config_disk_persistor.go b/cf/configuration/config_disk_persistor.go new file mode 100644 index 00000000000..818405601d8 --- /dev/null +++ b/cf/configuration/config_disk_persistor.go @@ -0,0 +1,74 @@ +package configuration + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +const ( + filePermissions = 0600 + dirPermissions = 0700 +) + +type Persistor interface { + Delete() + Load(DataInterface) error + Save(DataInterface) error +} + +type DataInterface interface { + JsonMarshalV3() ([]byte, error) + JsonUnmarshalV3([]byte) error +} + +type DiskPersistor struct { + filePath string +} + +func NewDiskPersistor(path string) (dp DiskPersistor) { + return DiskPersistor{ + filePath: path, + } +} + +func (dp DiskPersistor) Delete() { + os.Remove(dp.filePath) +} + +func (dp DiskPersistor) Load(data DataInterface) error { + err := dp.read(data) + if err != nil { + err = dp.write(data) + } + return err +} + +func (dp DiskPersistor) Save(data DataInterface) (err error) { + return dp.write(data) +} + +func (dp DiskPersistor) read(data DataInterface) error { + err := os.MkdirAll(filepath.Dir(dp.filePath), dirPermissions) + if err != nil { + return err + } + + jsonBytes, err := ioutil.ReadFile(dp.filePath) + if err != nil { + return err + } + + err = data.JsonUnmarshalV3(jsonBytes) + return err +} + +func (dp DiskPersistor) write(data DataInterface) error { + bytes, err := data.JsonMarshalV3() + if err != nil { + return err + } + + err = ioutil.WriteFile(dp.filePath, bytes, filePermissions) + return err +} diff --git a/cf/configuration/config_disk_persistor_test.go b/cf/configuration/config_disk_persistor_test.go new file mode 100644 index 00000000000..17d889d8a5a --- /dev/null +++ b/cf/configuration/config_disk_persistor_test.go @@ -0,0 +1,91 @@ +package configuration_test + +import ( + "encoding/json" + "io/ioutil" + "os" + + . "github.com/cloudfoundry/cli/cf/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DiskPersistor", func() { + var ( + tmpDir string + tmpFile *os.File + diskPersistor DiskPersistor + ) + + BeforeEach(func() { + var err error + + tmpDir = os.TempDir() + + tmpFile, err = ioutil.TempFile(tmpDir, "tmp_file") + Expect(err).ToNot(HaveOccurred()) + + diskPersistor = NewDiskPersistor(tmpFile.Name()) + }) + + AfterEach(func() { + os.Remove(tmpFile.Name()) + }) + + Describe(".Delete", func() { + It("Deletes the correct file", func() { + tmpFile.Close() + diskPersistor.Delete() + + file, err := os.Stat(tmpFile.Name()) + Expect(file).To(BeNil()) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Save", func() { + It("Writes the json file to the correct filepath", func() { + d := &data{Info: "save test"} + + err := diskPersistor.Save(d) + Expect(err).ToNot(HaveOccurred()) + + dataBytes, err := ioutil.ReadFile(tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + Expect(string(dataBytes)).To(ContainSubstring(d.Info)) + }) + }) + + Describe(".Load", func() { + It("Will load an empty json file", func() { + d := &data{} + + err := diskPersistor.Load(d) + Expect(err).ToNot(HaveOccurred()) + Expect(d.Info).To(Equal("")) + }) + + It("Will load a json file with specific keys", func() { + d := &data{} + + err := ioutil.WriteFile(tmpFile.Name(), []byte(`{"Info":"test string"}`), 0700) + Expect(err).ToNot(HaveOccurred()) + + err = diskPersistor.Load(d) + Expect(err).ToNot(HaveOccurred()) + Expect(d.Info).To(Equal("test string")) + }) + }) +}) + +type data struct { + Info string +} + +func (d *data) JsonMarshalV3() ([]byte, error) { + return json.MarshalIndent(d, "", " ") +} + +func (d *data) JsonUnmarshalV3(data []byte) error { + return json.Unmarshal(data, d) +} diff --git a/cf/configuration/config_helpers/config_helpers.go b/cf/configuration/config_helpers/config_helpers.go new file mode 100644 index 00000000000..901c4a13149 --- /dev/null +++ b/cf/configuration/config_helpers/config_helpers.go @@ -0,0 +1,43 @@ +package config_helpers + +import ( + "os" + "path/filepath" + "runtime" +) + +func DefaultFilePath() string { + var configDir string + + if os.Getenv("CF_HOME") != "" { + cfHome := os.Getenv("CF_HOME") + configDir = filepath.Join(cfHome, ".cf") + } else { + configDir = filepath.Join(userHomeDir(), ".cf") + } + + return filepath.Join(configDir, "config.json") +} + +// See: http://stackoverflow.com/questions/7922270/obtain-users-home-directory +// we can't cross compile using cgo and use user.Current() +var userHomeDir = func() string { + + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + + return os.Getenv("HOME") +} + +var PluginRepoDir = func() string { + if os.Getenv("CF_PLUGIN_HOME") != "" { + return os.Getenv("CF_PLUGIN_HOME") + } + + return userHomeDir() +} diff --git a/cf/configuration/configuration_suite_test.go b/cf/configuration/configuration_suite_test.go new file mode 100644 index 00000000000..c9832177aa4 --- /dev/null +++ b/cf/configuration/configuration_suite_test.go @@ -0,0 +1,13 @@ +package configuration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfiguration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Configuration Suite") +} diff --git a/cf/configuration/core_config/access_token.go b/cf/configuration/core_config/access_token.go new file mode 100644 index 00000000000..336f6b2df0d --- /dev/null +++ b/cf/configuration/core_config/access_token.go @@ -0,0 +1,56 @@ +package core_config + +import ( + "encoding/base64" + "encoding/json" + "strings" +) + +type TokenInfo struct { + Username string `json:"user_name"` + Email string `json:"email"` + UserGuid string `json:"user_id"` +} + +func NewTokenInfo(accessToken string) (info TokenInfo) { + tokenJson, err := DecodeAccessToken(accessToken) + + if err != nil { + return + } + info = TokenInfo{} + err = json.Unmarshal(tokenJson, &info) + return +} + +func DecodeAccessToken(accessToken string) (tokenJson []byte, err error) { + tokenParts := strings.Split(accessToken, " ") + + if len(tokenParts) < 2 { + return + } + + token := tokenParts[1] + encodedParts := strings.Split(token, ".") + + if len(encodedParts) < 3 { + return + } + + encodedTokenJson := encodedParts[1] + return base64Decode(encodedTokenJson) +} + +func base64Decode(encodedData string) ([]byte, error) { + return base64.StdEncoding.DecodeString(restorePadding(encodedData)) +} + +func restorePadding(seg string) string { + switch len(seg) % 4 { + case 2: + seg = seg + "==" + case 3: + seg = seg + "=" + } + return seg +} diff --git a/cf/configuration/core_config/access_token_test.go b/cf/configuration/core_config/access_token_test.go new file mode 100644 index 00000000000..f83c2d1e062 --- /dev/null +++ b/cf/configuration/core_config/access_token_test.go @@ -0,0 +1,33 @@ +package core_config_test + +import ( + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("NewTokenInfo", func() { + It("decodes a string into TokenInfo when there is no padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("user1@example.com")) + }) + + It("decodes a string into TokenInfo when there is doubling padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwNTg2MjlkNC04NjEwLTQ3NTEtOTg3Ny0yOGMwNzE3YTE5ZTciLCJzdWIiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJ1c2VyX25hbWUiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiZW1haWwiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiaWF0IjoxMzc3MDk1ODM5LCJleHAiOjEzNzcxMzkwMzksImlzcyI6Imh0dHBzOi8vdWFhLnJ1bi5waXZvdGFsLmlvL29hdXRoL3Rva2VuIiwiYXVkIjpbIm9wZW5pZCIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCJdfQ.dcgrGjPvTjYvg8dTSZY5ecZZTNt59IYd442VaEXXvLNB_WQCAdbVOxiJ14ogzQkkzDDw60Q2lbw4z6HrqM1a-BNpYfRmvaIP_79GpIZC6OzQy_PgA1whL27pO7_ABkSJT1CEgJQJMTQlYOiZNHvFTWen3G4O6ey680cxIN5VvbFjmmQHCuwANE9_GqnYYvoI9tS1nERku8DX2H9KH5NAgDa52-p0NhLnZRqYjGss6EyPYkwYN5w2OizfYUmEYVWo8K1Q45_TGMoE-LgZe2mGWwv0euLYBoFTkYhtBMj91dQagLrL1aGcmDKPc6ivkXtfpN4Zv7FJ9OXJ2DPQyHKRpw" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("tlang@gopivotal.com")) + }) + + It("decodes a string into TokenInfo when there is single padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwNTg2MjlkNC04NjEwLTQ3NTEtOTg3Ny0yOGMwNzE3YTE5ZTciLCJzdWIiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJ1c2VyX25hbWUiOiJ0bGFuZzFAZ29waXZvdGFsLmNvbSIsImVtYWlsIjoidGxhbmdAZ29waXZvdGFsLmNvbSIsImlhdCI6MTM3NzA5NTgzOSwiZXhwIjoxMzc3MTM5MDM5LCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.dcgrGjPvTjYvg8dTSZY5ecZZTNt59IYd442VaEXXvLNB_WQCAdbVOxiJ14ogzQkkzDDw60Q2lbw4z6HrqM1a-BNpYfRmvaIP_79GpIZC6OzQy_PgA1whL27pO7_ABkSJT1CEgJQJMTQlYOiZNHvFTWen3G4O6ey680cxIN5VvbFjmmQHCuwANE9_GqnYYvoI9tS1nERku8DX2H9KH5NAgDa52-p0NhLnZRqYjGss6EyPYkwYN5w2OizfYUmEYVWo8K1Q45_TGMoE-LgZe2mGWwv0euLYBoFTkYhtBMj91dQagLrL1aGcmDKPc6ivkXtfpN4Zv7FJ9OXJ2DPQyHKRpw" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("tlang1@gopivotal.com")) + }) +}) diff --git a/cf/configuration/core_config/config_data.go b/cf/configuration/core_config/config_data.go new file mode 100644 index 00000000000..a50237a80bc --- /dev/null +++ b/cf/configuration/core_config/config_data.go @@ -0,0 +1,61 @@ +package core_config + +import ( + "encoding/json" + + "github.com/cloudfoundry/cli/cf/models" +) + +type AuthPromptType string + +const ( + AuthPromptTypeText AuthPromptType = "TEXT" + AuthPromptTypePassword AuthPromptType = "PASSWORD" +) + +type AuthPrompt struct { + Type AuthPromptType + DisplayName string +} + +type Data struct { + ConfigVersion int + Target string + ApiVersion string + AuthorizationEndpoint string + LoggregatorEndPoint string + UaaEndpoint string + AccessToken string + RefreshToken string + OrganizationFields models.OrganizationFields + SpaceFields models.SpaceFields + SSLDisabled bool + AsyncTimeout uint + Trace string + ColorEnabled string + Locale string +} + +func NewData() (data *Data) { + data = new(Data) + return +} + +func (d *Data) JsonMarshalV3() (output []byte, err error) { + d.ConfigVersion = 3 + return json.MarshalIndent(d, "", " ") +} + +func (d *Data) JsonUnmarshalV3(input []byte) (err error) { + err = json.Unmarshal(input, d) + if err != nil { + return + } + + if d.ConfigVersion != 3 { + *d = Data{} + return + } + + return +} diff --git a/cf/configuration/core_config/config_data_test.go b/cf/configuration/core_config/config_data_test.go new file mode 100644 index 00000000000..3302cd9921a --- /dev/null +++ b/cf/configuration/core_config/config_data_test.go @@ -0,0 +1,100 @@ +package core_config_test + +import ( + "regexp" + + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var exampleJSON = ` +{ + "ConfigVersion": 3, + "Target": "api.example.com", + "ApiVersion": "3", + "AuthorizationEndpoint": "auth.example.com", + "LoggregatorEndPoint": "logs.example.com", + "UaaEndpoint": "uaa.example.com", + "AccessToken": "the-access-token", + "RefreshToken": "the-refresh-token", + "OrganizationFields": { + "Guid": "the-org-guid", + "Name": "the-org", + "QuotaDefinition": { + "name":"", + "memory_limit":0, + "instance_memory_limit":0, + "total_routes":0, + "total_services":0, + "non_basic_services_allowed": false + } + }, + "SpaceFields": { + "Guid": "the-space-guid", + "Name": "the-space" + }, + "SSLDisabled": true, + "AsyncTimeout": 1000, + "Trace": "path/to/some/file", + "ColorEnabled": "true", + "Locale": "fr_FR" +}` + +var exampleData = &Data{ + Target: "api.example.com", + ApiVersion: "3", + AuthorizationEndpoint: "auth.example.com", + LoggregatorEndPoint: "logs.example.com", + UaaEndpoint: "uaa.example.com", + AccessToken: "the-access-token", + RefreshToken: "the-refresh-token", + OrganizationFields: models.OrganizationFields{ + Guid: "the-org-guid", + Name: "the-org", + }, + SpaceFields: models.SpaceFields{ + Guid: "the-space-guid", + Name: "the-space", + }, + SSLDisabled: true, + Trace: "path/to/some/file", + AsyncTimeout: 1000, + ColorEnabled: "true", + Locale: "fr_FR", +} + +var _ = Describe("V3 Config files", func() { + Describe("serialization", func() { + It("creates a JSON string from the config object", func() { + jsonData, err := exampleData.JsonMarshalV3() + + Expect(err).NotTo(HaveOccurred()) + Expect(stripWhitespace(string(jsonData))).To(ContainSubstring(stripWhitespace(exampleJSON))) + }) + }) + + Describe("parsing", func() { + It("returns an error when the JSON is invalid", func() { + configData := NewData() + err := configData.JsonUnmarshalV3([]byte(`{ "not_valid": ### }`)) + + Expect(err).To(HaveOccurred()) + }) + + It("creates a config object from valid JSON", func() { + configData := NewData() + err := configData.JsonUnmarshalV3([]byte(exampleJSON)) + + Expect(err).NotTo(HaveOccurred()) + Expect(configData).To(Equal(exampleData)) + }) + }) +}) + +var whiteSpaceRegex = regexp.MustCompile(`\s+`) + +func stripWhitespace(input string) string { + return whiteSpaceRegex.ReplaceAllString(input, "") +} diff --git a/cf/configuration/core_config/config_repository.go b/cf/configuration/core_config/config_repository.go new file mode 100644 index 00000000000..d5e2ab98860 --- /dev/null +++ b/cf/configuration/core_config/config_repository.go @@ -0,0 +1,369 @@ +package core_config + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/configuration" + "github.com/cloudfoundry/cli/cf/models" +) + +type ConfigRepository struct { + data *Data + mutex *sync.RWMutex + initOnce *sync.Once + persistor configuration.Persistor + onError func(error) +} + +func NewRepositoryFromFilepath(filepath string, errorHandler func(error)) Repository { + return NewRepositoryFromPersistor(configuration.NewDiskPersistor(filepath), errorHandler) +} + +func NewRepositoryFromPersistor(persistor configuration.Persistor, errorHandler func(error)) Repository { + return &ConfigRepository{ + data: NewData(), + mutex: new(sync.RWMutex), + initOnce: new(sync.Once), + persistor: persistor, + onError: errorHandler, + } +} + +type Reader interface { + ApiEndpoint() string + ApiVersion() string + HasAPIEndpoint() bool + + AuthenticationEndpoint() string + LoggregatorEndpoint() string + UaaEndpoint() string + AccessToken() string + RefreshToken() string + + OrganizationFields() models.OrganizationFields + HasOrganization() bool + + SpaceFields() models.SpaceFields + HasSpace() bool + + Username() string + UserGuid() string + UserEmail() string + IsLoggedIn() bool + IsSSLDisabled() bool + + AsyncTimeout() uint + Trace() string + + ColorEnabled() string + + Locale() string +} + +type ReadWriter interface { + Reader + ClearSession() + SetApiEndpoint(string) + SetApiVersion(string) + SetAuthenticationEndpoint(string) + SetLoggregatorEndpoint(string) + SetUaaEndpoint(string) + SetAccessToken(string) + SetRefreshToken(string) + SetOrganizationFields(models.OrganizationFields) + SetSpaceFields(models.SpaceFields) + SetSSLDisabled(bool) + SetAsyncTimeout(uint) + SetTrace(string) + SetColorEnabled(string) + SetLocale(string) +} + +type Repository interface { + ReadWriter + Close() +} + +// ACCESS CONTROL + +func (c *ConfigRepository) init() { + c.initOnce.Do(func() { + err := c.persistor.Load(c.data) + if err != nil { + c.onError(err) + } + }) +} + +func (c *ConfigRepository) read(cb func()) { + c.mutex.RLock() + defer c.mutex.RUnlock() + c.init() + + cb() +} + +func (c *ConfigRepository) write(cb func()) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.init() + + cb() + + err := c.persistor.Save(c.data) + if err != nil { + c.onError(err) + } +} + +// CLOSERS + +func (c *ConfigRepository) Close() { + c.read(func() { + // perform a read to ensure write lock has been cleared + }) +} + +// GETTERS + +func (c *ConfigRepository) ApiVersion() (apiVersion string) { + c.read(func() { + apiVersion = c.data.ApiVersion + }) + return +} + +func (c *ConfigRepository) AuthenticationEndpoint() (authEndpoint string) { + c.read(func() { + authEndpoint = c.data.AuthorizationEndpoint + }) + return +} + +func (c *ConfigRepository) LoggregatorEndpoint() (logEndpoint string) { + c.read(func() { + logEndpoint = c.data.LoggregatorEndPoint + }) + return +} + +func (c *ConfigRepository) UaaEndpoint() (uaaEndpoint string) { + c.read(func() { + uaaEndpoint = c.data.UaaEndpoint + }) + return +} + +func (c *ConfigRepository) ApiEndpoint() (apiEndpoint string) { + c.read(func() { + apiEndpoint = c.data.Target + }) + return +} + +func (c *ConfigRepository) HasAPIEndpoint() (hasEndpoint bool) { + c.read(func() { + hasEndpoint = c.data.ApiVersion != "" && c.data.Target != "" + }) + return +} + +func (c *ConfigRepository) AccessToken() (accessToken string) { + c.read(func() { + accessToken = c.data.AccessToken + }) + return +} + +func (c *ConfigRepository) RefreshToken() (refreshToken string) { + c.read(func() { + refreshToken = c.data.RefreshToken + }) + return +} + +func (c *ConfigRepository) OrganizationFields() (org models.OrganizationFields) { + c.read(func() { + org = c.data.OrganizationFields + }) + return +} + +func (c *ConfigRepository) SpaceFields() (space models.SpaceFields) { + c.read(func() { + space = c.data.SpaceFields + }) + return +} + +func (c *ConfigRepository) UserEmail() (email string) { + c.read(func() { + email = NewTokenInfo(c.data.AccessToken).Email + }) + return +} + +func (c *ConfigRepository) UserGuid() (guid string) { + c.read(func() { + guid = NewTokenInfo(c.data.AccessToken).UserGuid + }) + return +} + +func (c *ConfigRepository) Username() (name string) { + c.read(func() { + name = NewTokenInfo(c.data.AccessToken).Username + }) + return +} + +func (c *ConfigRepository) IsLoggedIn() (loggedIn bool) { + c.read(func() { + loggedIn = c.data.AccessToken != "" + }) + return +} + +func (c *ConfigRepository) HasOrganization() (hasOrg bool) { + c.read(func() { + hasOrg = c.data.OrganizationFields.Guid != "" && c.data.OrganizationFields.Name != "" + }) + return +} + +func (c *ConfigRepository) HasSpace() (hasSpace bool) { + c.read(func() { + hasSpace = c.data.SpaceFields.Guid != "" && c.data.SpaceFields.Name != "" + }) + return +} + +func (c *ConfigRepository) IsSSLDisabled() (isSSLDisabled bool) { + c.read(func() { + isSSLDisabled = c.data.SSLDisabled + }) + return +} + +func (c *ConfigRepository) AsyncTimeout() (timeout uint) { + c.read(func() { + timeout = c.data.AsyncTimeout + }) + return +} + +func (c *ConfigRepository) Trace() (trace string) { + c.read(func() { + trace = c.data.Trace + }) + return +} + +func (c *ConfigRepository) ColorEnabled() (enabled string) { + c.read(func() { + enabled = c.data.ColorEnabled + }) + return +} + +func (c *ConfigRepository) Locale() (locale string) { + c.read(func() { + locale = c.data.Locale + }) + return +} + +// SETTERS + +func (c *ConfigRepository) ClearSession() { + c.write(func() { + c.data.AccessToken = "" + c.data.RefreshToken = "" + c.data.OrganizationFields = models.OrganizationFields{} + c.data.SpaceFields = models.SpaceFields{} + }) +} + +func (c *ConfigRepository) SetApiEndpoint(endpoint string) { + c.write(func() { + c.data.Target = endpoint + }) +} + +func (c *ConfigRepository) SetApiVersion(version string) { + c.write(func() { + c.data.ApiVersion = version + }) +} + +func (c *ConfigRepository) SetAuthenticationEndpoint(endpoint string) { + c.write(func() { + c.data.AuthorizationEndpoint = endpoint + }) +} + +func (c *ConfigRepository) SetLoggregatorEndpoint(endpoint string) { + c.write(func() { + c.data.LoggregatorEndPoint = endpoint + }) +} + +func (c *ConfigRepository) SetUaaEndpoint(uaaEndpoint string) { + c.write(func() { + c.data.UaaEndpoint = uaaEndpoint + }) +} + +func (c *ConfigRepository) SetAccessToken(token string) { + c.write(func() { + c.data.AccessToken = token + }) +} + +func (c *ConfigRepository) SetRefreshToken(token string) { + c.write(func() { + c.data.RefreshToken = token + }) +} + +func (c *ConfigRepository) SetOrganizationFields(org models.OrganizationFields) { + c.write(func() { + c.data.OrganizationFields = org + }) +} + +func (c *ConfigRepository) SetSpaceFields(space models.SpaceFields) { + c.write(func() { + c.data.SpaceFields = space + }) +} + +func (c *ConfigRepository) SetSSLDisabled(disabled bool) { + c.write(func() { + c.data.SSLDisabled = disabled + }) +} + +func (c *ConfigRepository) SetAsyncTimeout(timeout uint) { + c.write(func() { + c.data.AsyncTimeout = timeout + }) +} + +func (c *ConfigRepository) SetTrace(value string) { + c.write(func() { + c.data.Trace = value + }) +} + +func (c *ConfigRepository) SetColorEnabled(enabled string) { + c.write(func() { + c.data.ColorEnabled = enabled + }) +} + +func (c *ConfigRepository) SetLocale(locale string) { + c.write(func() { + c.data.Locale = locale + }) +} diff --git a/cf/configuration/core_config/config_repository_test.go b/cf/configuration/core_config/config_repository_test.go new file mode 100644 index 00000000000..980ab1bd865 --- /dev/null +++ b/cf/configuration/core_config/config_repository_test.go @@ -0,0 +1,176 @@ +package core_config_test + +import ( + "fmt" + "os" + "path/filepath" + "time" + + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/fileutils" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Configuration Repository", func() { + var config Repository + var repo *testconfig.FakePersistor + + BeforeEach(func() { + repo = testconfig.NewFakePersistor() + repo.LoadReturns.Data = NewData() + config = testconfig.NewRepository() + }) + + It("is safe for concurrent reading and writing", func() { + swapValLoop := func(config Repository) { + for { + val := config.ApiEndpoint() + + switch val { + case "foo": + config.SetApiEndpoint("bar") + case "bar": + config.SetApiEndpoint("foo") + default: + panic(fmt.Sprintf("WAT: %s", val)) + } + } + } + + config.SetApiEndpoint("foo") + + go swapValLoop(config) + go swapValLoop(config) + go swapValLoop(config) + go swapValLoop(config) + + time.Sleep(10 * time.Millisecond) + }) + + // TODO - test ClearTokens et al + It("has acccessor methods for all config fields", func() { + config.SetApiEndpoint("http://api.the-endpoint") + Expect(config.ApiEndpoint()).To(Equal("http://api.the-endpoint")) + + config.SetApiVersion("3") + Expect(config.ApiVersion()).To(Equal("3")) + + config.SetAuthenticationEndpoint("http://auth.the-endpoint") + Expect(config.AuthenticationEndpoint()).To(Equal("http://auth.the-endpoint")) + + config.SetLoggregatorEndpoint("http://logs.the-endpoint") + Expect(config.LoggregatorEndpoint()).To(Equal("http://logs.the-endpoint")) + + config.SetUaaEndpoint("http://uaa.the-endpoint") + Expect(config.UaaEndpoint()).To(Equal("http://uaa.the-endpoint")) + + config.SetAccessToken("the-token") + Expect(config.AccessToken()).To(Equal("the-token")) + + config.SetRefreshToken("the-token") + Expect(config.RefreshToken()).To(Equal("the-token")) + + organization := maker.NewOrgFields(maker.Overrides{"name": "the-org"}) + config.SetOrganizationFields(organization) + Expect(config.OrganizationFields()).To(Equal(organization)) + + space := maker.NewSpaceFields(maker.Overrides{"name": "the-space"}) + config.SetSpaceFields(space) + Expect(config.SpaceFields()).To(Equal(space)) + + config.SetSSLDisabled(false) + Expect(config.IsSSLDisabled()).To(BeFalse()) + + config.SetLocale("en_US") + Expect(config.Locale()).To(Equal("en_US")) + }) + + Describe("HasAPIEndpoint", func() { + Context("when both endpoint and version are set", func() { + BeforeEach(func() { + config.SetApiEndpoint("http://example.org") + config.SetApiVersion("42.1.2.3") + }) + It("returns true", func() { + Expect(config.HasAPIEndpoint()).To(BeTrue()) + }) + }) + + Context("when endpoint is not set", func() { + BeforeEach(func() { + config.SetApiVersion("42.1.2.3") + }) + It("returns false", func() { + Expect(config.HasAPIEndpoint()).To(BeFalse()) + }) + }) + + Context("when version is not set", func() { + BeforeEach(func() { + config.SetApiEndpoint("http://example.org") + }) + It("returns false", func() { + Expect(config.HasAPIEndpoint()).To(BeFalse()) + }) + }) + }) + + It("User has a valid Access Token", func() { + config.SetAccessToken("bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E") + Expect(config.UserGuid()).To(Equal("772dda3f-669f-4276-b2bd-90486abe1f6f")) + Expect(config.UserEmail()).To(Equal("user1@example.com")) + }) + + It("User has an invalid Access Token", func() { + config.SetAccessToken("bearer") + Expect(config.UserGuid()).To(BeEmpty()) + Expect(config.UserEmail()).To(BeEmpty()) + + config.SetAccessToken("bearer eyJhbGciOiJSUzI1NiJ9") + Expect(config.UserGuid()).To(BeEmpty()) + Expect(config.UserEmail()).To(BeEmpty()) + }) + + It("has sane defaults when there is no config to read", func() { + withFakeHome(func(configPath string) { + config = NewRepositoryFromFilepath(configPath, func(err error) { + panic(err) + }) + + Expect(config.ApiEndpoint()).To(Equal("")) + Expect(config.ApiVersion()).To(Equal("")) + Expect(config.AuthenticationEndpoint()).To(Equal("")) + Expect(config.AccessToken()).To(Equal("")) + }) + }) + + Context("when the configuration version is older than the current version", func() { + It("returns a new empty config", func() { + withConfigFixture("outdated-config", func(configPath string) { + config = NewRepositoryFromFilepath(configPath, func(err error) { + panic(err) + }) + + Expect(config.ApiEndpoint()).To(Equal("")) + }) + }) + }) +}) + +func withFakeHome(callback func(dirPath string)) { + fileutils.TempDir("test-config", func(dir string, err error) { + if err != nil { + Fail("Couldn't create tmp file") + } + callback(filepath.Join(dir, ".cf", "config.json")) + }) +} + +func withConfigFixture(name string, callback func(dirPath string)) { + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + callback(filepath.Join(cwd, "..", "..", "..", "fixtures", "config", name, ".cf", "config.json")) +} diff --git a/cf/configuration/core_config/core_config_suite_test.go b/cf/configuration/core_config/core_config_suite_test.go new file mode 100644 index 00000000000..1a13f055ed4 --- /dev/null +++ b/cf/configuration/core_config/core_config_suite_test.go @@ -0,0 +1,13 @@ +package core_config_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCoreConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CoreConfig Suite") +} diff --git a/cf/configuration/fakes/fake_repository.go b/cf/configuration/fakes/fake_repository.go new file mode 100644 index 00000000000..dc0f7667fd0 --- /dev/null +++ b/cf/configuration/fakes/fake_repository.go @@ -0,0 +1,1139 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeRepository struct { + ApiEndpointStub func() string + apiEndpointMutex sync.RWMutex + apiEndpointArgsForCall []struct{} + apiEndpointReturns struct { + result1 string + } + ApiVersionStub func() string + apiVersionMutex sync.RWMutex + apiVersionArgsForCall []struct{} + apiVersionReturns struct { + result1 string + } + HasAPIEndpointStub func() bool + hasAPIEndpointMutex sync.RWMutex + hasAPIEndpointArgsForCall []struct{} + hasAPIEndpointReturns struct { + result1 bool + } + AuthenticationEndpointStub func() string + authenticationEndpointMutex sync.RWMutex + authenticationEndpointArgsForCall []struct{} + authenticationEndpointReturns struct { + result1 string + } + LoggregatorEndpointStub func() string + loggregatorEndpointMutex sync.RWMutex + loggregatorEndpointArgsForCall []struct{} + loggregatorEndpointReturns struct { + result1 string + } + UaaEndpointStub func() string + uaaEndpointMutex sync.RWMutex + uaaEndpointArgsForCall []struct{} + uaaEndpointReturns struct { + result1 string + } + AccessTokenStub func() string + accessTokenMutex sync.RWMutex + accessTokenArgsForCall []struct{} + accessTokenReturns struct { + result1 string + } + RefreshTokenStub func() string + refreshTokenMutex sync.RWMutex + refreshTokenArgsForCall []struct{} + refreshTokenReturns struct { + result1 string + } + OrganizationFieldsStub func() models.OrganizationFields + organizationFieldsMutex sync.RWMutex + organizationFieldsArgsForCall []struct{} + organizationFieldsReturns struct { + result1 models.OrganizationFields + } + HasOrganizationStub func() bool + hasOrganizationMutex sync.RWMutex + hasOrganizationArgsForCall []struct{} + hasOrganizationReturns struct { + result1 bool + } + SpaceFieldsStub func() models.SpaceFields + spaceFieldsMutex sync.RWMutex + spaceFieldsArgsForCall []struct{} + spaceFieldsReturns struct { + result1 models.SpaceFields + } + HasSpaceStub func() bool + hasSpaceMutex sync.RWMutex + hasSpaceArgsForCall []struct{} + hasSpaceReturns struct { + result1 bool + } + UsernameStub func() string + usernameMutex sync.RWMutex + usernameArgsForCall []struct{} + usernameReturns struct { + result1 string + } + UserGuidStub func() string + userGuidMutex sync.RWMutex + userGuidArgsForCall []struct{} + userGuidReturns struct { + result1 string + } + UserEmailStub func() string + userEmailMutex sync.RWMutex + userEmailArgsForCall []struct{} + userEmailReturns struct { + result1 string + } + IsLoggedInStub func() bool + isLoggedInMutex sync.RWMutex + isLoggedInArgsForCall []struct{} + isLoggedInReturns struct { + result1 bool + } + IsSSLDisabledStub func() bool + isSSLDisabledMutex sync.RWMutex + isSSLDisabledArgsForCall []struct{} + isSSLDisabledReturns struct { + result1 bool + } + AsyncTimeoutStub func() uint + asyncTimeoutMutex sync.RWMutex + asyncTimeoutArgsForCall []struct{} + asyncTimeoutReturns struct { + result1 uint + } + TraceStub func() string + traceMutex sync.RWMutex + traceArgsForCall []struct{} + traceReturns struct { + result1 string + } + ColorEnabledStub func() string + colorEnabledMutex sync.RWMutex + colorEnabledArgsForCall []struct{} + colorEnabledReturns struct { + result1 string + } + LocaleStub func() string + localeMutex sync.RWMutex + localeArgsForCall []struct{} + localeReturns struct { + result1 string + } + PluginsStub func() map[string]string + pluginsMutex sync.RWMutex + pluginsArgsForCall []struct{} + pluginsReturns struct { + result1 map[string]string + } + UserHomePathStub func() string + userHomePathMutex sync.RWMutex + userHomePathArgsForCall []struct{} + userHomePathReturns struct { + result1 string + } + ClearSessionStub func() + clearSessionMutex sync.RWMutex + clearSessionArgsForCall []struct{} + SetApiEndpointStub func(string) + setApiEndpointMutex sync.RWMutex + setApiEndpointArgsForCall []struct { + arg1 string + } + SetApiVersionStub func(string) + setApiVersionMutex sync.RWMutex + setApiVersionArgsForCall []struct { + arg1 string + } + SetAuthenticationEndpointStub func(string) + setAuthenticationEndpointMutex sync.RWMutex + setAuthenticationEndpointArgsForCall []struct { + arg1 string + } + SetLoggregatorEndpointStub func(string) + setLoggregatorEndpointMutex sync.RWMutex + setLoggregatorEndpointArgsForCall []struct { + arg1 string + } + SetUaaEndpointStub func(string) + setUaaEndpointMutex sync.RWMutex + setUaaEndpointArgsForCall []struct { + arg1 string + } + SetAccessTokenStub func(string) + setAccessTokenMutex sync.RWMutex + setAccessTokenArgsForCall []struct { + arg1 string + } + SetRefreshTokenStub func(string) + setRefreshTokenMutex sync.RWMutex + setRefreshTokenArgsForCall []struct { + arg1 string + } + SetOrganizationFieldsStub func(models.OrganizationFields) + setOrganizationFieldsMutex sync.RWMutex + setOrganizationFieldsArgsForCall []struct { + arg1 models.OrganizationFields + } + SetSpaceFieldsStub func(models.SpaceFields) + setSpaceFieldsMutex sync.RWMutex + setSpaceFieldsArgsForCall []struct { + arg1 models.SpaceFields + } + SetSSLDisabledStub func(bool) + setSSLDisabledMutex sync.RWMutex + setSSLDisabledArgsForCall []struct { + arg1 bool + } + SetAsyncTimeoutStub func(uint) + setAsyncTimeoutMutex sync.RWMutex + setAsyncTimeoutArgsForCall []struct { + arg1 uint + } + SetTraceStub func(string) + setTraceMutex sync.RWMutex + setTraceArgsForCall []struct { + arg1 string + } + SetColorEnabledStub func(string) + setColorEnabledMutex sync.RWMutex + setColorEnabledArgsForCall []struct { + arg1 string + } + SetLocaleStub func(string) + setLocaleMutex sync.RWMutex + setLocaleArgsForCall []struct { + arg1 string + } + SetPluginStub func(string, string) + setPluginMutex sync.RWMutex + setPluginArgsForCall []struct { + arg1 string + arg2 string + } + CloseStub func() + closeMutex sync.RWMutex + closeArgsForCall []struct{} +} + +func (fake *FakeRepository) ApiEndpoint() string { + fake.apiEndpointMutex.Lock() + defer fake.apiEndpointMutex.Unlock() + fake.apiEndpointArgsForCall = append(fake.apiEndpointArgsForCall, struct{}{}) + if fake.ApiEndpointStub != nil { + return fake.ApiEndpointStub() + } else { + return fake.apiEndpointReturns.result1 + } +} + +func (fake *FakeRepository) ApiEndpointCallCount() int { + fake.apiEndpointMutex.RLock() + defer fake.apiEndpointMutex.RUnlock() + return len(fake.apiEndpointArgsForCall) +} + +func (fake *FakeRepository) ApiEndpointReturns(result1 string) { + fake.apiEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ApiVersion() string { + fake.apiVersionMutex.Lock() + defer fake.apiVersionMutex.Unlock() + fake.apiVersionArgsForCall = append(fake.apiVersionArgsForCall, struct{}{}) + if fake.ApiVersionStub != nil { + return fake.ApiVersionStub() + } else { + return fake.apiVersionReturns.result1 + } +} + +func (fake *FakeRepository) ApiVersionCallCount() int { + fake.apiVersionMutex.RLock() + defer fake.apiVersionMutex.RUnlock() + return len(fake.apiVersionArgsForCall) +} + +func (fake *FakeRepository) ApiVersionReturns(result1 string) { + fake.apiVersionReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) HasAPIEndpoint() bool { + fake.hasAPIEndpointMutex.Lock() + defer fake.hasAPIEndpointMutex.Unlock() + fake.hasAPIEndpointArgsForCall = append(fake.hasAPIEndpointArgsForCall, struct{}{}) + if fake.HasAPIEndpointStub != nil { + return fake.HasAPIEndpointStub() + } else { + return fake.hasAPIEndpointReturns.result1 + } +} + +func (fake *FakeRepository) HasAPIEndpointCallCount() int { + fake.hasAPIEndpointMutex.RLock() + defer fake.hasAPIEndpointMutex.RUnlock() + return len(fake.hasAPIEndpointArgsForCall) +} + +func (fake *FakeRepository) HasAPIEndpointReturns(result1 bool) { + fake.hasAPIEndpointReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) AuthenticationEndpoint() string { + fake.authenticationEndpointMutex.Lock() + defer fake.authenticationEndpointMutex.Unlock() + fake.authenticationEndpointArgsForCall = append(fake.authenticationEndpointArgsForCall, struct{}{}) + if fake.AuthenticationEndpointStub != nil { + return fake.AuthenticationEndpointStub() + } else { + return fake.authenticationEndpointReturns.result1 + } +} + +func (fake *FakeRepository) AuthenticationEndpointCallCount() int { + fake.authenticationEndpointMutex.RLock() + defer fake.authenticationEndpointMutex.RUnlock() + return len(fake.authenticationEndpointArgsForCall) +} + +func (fake *FakeRepository) AuthenticationEndpointReturns(result1 string) { + fake.authenticationEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) LoggregatorEndpoint() string { + fake.loggregatorEndpointMutex.Lock() + defer fake.loggregatorEndpointMutex.Unlock() + fake.loggregatorEndpointArgsForCall = append(fake.loggregatorEndpointArgsForCall, struct{}{}) + if fake.LoggregatorEndpointStub != nil { + return fake.LoggregatorEndpointStub() + } else { + return fake.loggregatorEndpointReturns.result1 + } +} + +func (fake *FakeRepository) LoggregatorEndpointCallCount() int { + fake.loggregatorEndpointMutex.RLock() + defer fake.loggregatorEndpointMutex.RUnlock() + return len(fake.loggregatorEndpointArgsForCall) +} + +func (fake *FakeRepository) LoggregatorEndpointReturns(result1 string) { + fake.loggregatorEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UaaEndpoint() string { + fake.uaaEndpointMutex.Lock() + defer fake.uaaEndpointMutex.Unlock() + fake.uaaEndpointArgsForCall = append(fake.uaaEndpointArgsForCall, struct{}{}) + if fake.UaaEndpointStub != nil { + return fake.UaaEndpointStub() + } else { + return fake.uaaEndpointReturns.result1 + } +} + +func (fake *FakeRepository) UaaEndpointCallCount() int { + fake.uaaEndpointMutex.RLock() + defer fake.uaaEndpointMutex.RUnlock() + return len(fake.uaaEndpointArgsForCall) +} + +func (fake *FakeRepository) UaaEndpointReturns(result1 string) { + fake.uaaEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) AccessToken() string { + fake.accessTokenMutex.Lock() + defer fake.accessTokenMutex.Unlock() + fake.accessTokenArgsForCall = append(fake.accessTokenArgsForCall, struct{}{}) + if fake.AccessTokenStub != nil { + return fake.AccessTokenStub() + } else { + return fake.accessTokenReturns.result1 + } +} + +func (fake *FakeRepository) AccessTokenCallCount() int { + fake.accessTokenMutex.RLock() + defer fake.accessTokenMutex.RUnlock() + return len(fake.accessTokenArgsForCall) +} + +func (fake *FakeRepository) AccessTokenReturns(result1 string) { + fake.accessTokenReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) RefreshToken() string { + fake.refreshTokenMutex.Lock() + defer fake.refreshTokenMutex.Unlock() + fake.refreshTokenArgsForCall = append(fake.refreshTokenArgsForCall, struct{}{}) + if fake.RefreshTokenStub != nil { + return fake.RefreshTokenStub() + } else { + return fake.refreshTokenReturns.result1 + } +} + +func (fake *FakeRepository) RefreshTokenCallCount() int { + fake.refreshTokenMutex.RLock() + defer fake.refreshTokenMutex.RUnlock() + return len(fake.refreshTokenArgsForCall) +} + +func (fake *FakeRepository) RefreshTokenReturns(result1 string) { + fake.refreshTokenReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) OrganizationFields() models.OrganizationFields { + fake.organizationFieldsMutex.Lock() + defer fake.organizationFieldsMutex.Unlock() + fake.organizationFieldsArgsForCall = append(fake.organizationFieldsArgsForCall, struct{}{}) + if fake.OrganizationFieldsStub != nil { + return fake.OrganizationFieldsStub() + } else { + return fake.organizationFieldsReturns.result1 + } +} + +func (fake *FakeRepository) OrganizationFieldsCallCount() int { + fake.organizationFieldsMutex.RLock() + defer fake.organizationFieldsMutex.RUnlock() + return len(fake.organizationFieldsArgsForCall) +} + +func (fake *FakeRepository) OrganizationFieldsReturns(result1 models.OrganizationFields) { + fake.organizationFieldsReturns = struct { + result1 models.OrganizationFields + }{result1} +} + +func (fake *FakeRepository) HasOrganization() bool { + fake.hasOrganizationMutex.Lock() + defer fake.hasOrganizationMutex.Unlock() + fake.hasOrganizationArgsForCall = append(fake.hasOrganizationArgsForCall, struct{}{}) + if fake.HasOrganizationStub != nil { + return fake.HasOrganizationStub() + } else { + return fake.hasOrganizationReturns.result1 + } +} + +func (fake *FakeRepository) HasOrganizationCallCount() int { + fake.hasOrganizationMutex.RLock() + defer fake.hasOrganizationMutex.RUnlock() + return len(fake.hasOrganizationArgsForCall) +} + +func (fake *FakeRepository) HasOrganizationReturns(result1 bool) { + fake.hasOrganizationReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) SpaceFields() models.SpaceFields { + fake.spaceFieldsMutex.Lock() + defer fake.spaceFieldsMutex.Unlock() + fake.spaceFieldsArgsForCall = append(fake.spaceFieldsArgsForCall, struct{}{}) + if fake.SpaceFieldsStub != nil { + return fake.SpaceFieldsStub() + } else { + return fake.spaceFieldsReturns.result1 + } +} + +func (fake *FakeRepository) SpaceFieldsCallCount() int { + fake.spaceFieldsMutex.RLock() + defer fake.spaceFieldsMutex.RUnlock() + return len(fake.spaceFieldsArgsForCall) +} + +func (fake *FakeRepository) SpaceFieldsReturns(result1 models.SpaceFields) { + fake.spaceFieldsReturns = struct { + result1 models.SpaceFields + }{result1} +} + +func (fake *FakeRepository) HasSpace() bool { + fake.hasSpaceMutex.Lock() + defer fake.hasSpaceMutex.Unlock() + fake.hasSpaceArgsForCall = append(fake.hasSpaceArgsForCall, struct{}{}) + if fake.HasSpaceStub != nil { + return fake.HasSpaceStub() + } else { + return fake.hasSpaceReturns.result1 + } +} + +func (fake *FakeRepository) HasSpaceCallCount() int { + fake.hasSpaceMutex.RLock() + defer fake.hasSpaceMutex.RUnlock() + return len(fake.hasSpaceArgsForCall) +} + +func (fake *FakeRepository) HasSpaceReturns(result1 bool) { + fake.hasSpaceReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) Username() string { + fake.usernameMutex.Lock() + defer fake.usernameMutex.Unlock() + fake.usernameArgsForCall = append(fake.usernameArgsForCall, struct{}{}) + if fake.UsernameStub != nil { + return fake.UsernameStub() + } else { + return fake.usernameReturns.result1 + } +} + +func (fake *FakeRepository) UsernameCallCount() int { + fake.usernameMutex.RLock() + defer fake.usernameMutex.RUnlock() + return len(fake.usernameArgsForCall) +} + +func (fake *FakeRepository) UsernameReturns(result1 string) { + fake.usernameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UserGuid() string { + fake.userGuidMutex.Lock() + defer fake.userGuidMutex.Unlock() + fake.userGuidArgsForCall = append(fake.userGuidArgsForCall, struct{}{}) + if fake.UserGuidStub != nil { + return fake.UserGuidStub() + } else { + return fake.userGuidReturns.result1 + } +} + +func (fake *FakeRepository) UserGuidCallCount() int { + fake.userGuidMutex.RLock() + defer fake.userGuidMutex.RUnlock() + return len(fake.userGuidArgsForCall) +} + +func (fake *FakeRepository) UserGuidReturns(result1 string) { + fake.userGuidReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UserEmail() string { + fake.userEmailMutex.Lock() + defer fake.userEmailMutex.Unlock() + fake.userEmailArgsForCall = append(fake.userEmailArgsForCall, struct{}{}) + if fake.UserEmailStub != nil { + return fake.UserEmailStub() + } else { + return fake.userEmailReturns.result1 + } +} + +func (fake *FakeRepository) UserEmailCallCount() int { + fake.userEmailMutex.RLock() + defer fake.userEmailMutex.RUnlock() + return len(fake.userEmailArgsForCall) +} + +func (fake *FakeRepository) UserEmailReturns(result1 string) { + fake.userEmailReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) IsLoggedIn() bool { + fake.isLoggedInMutex.Lock() + defer fake.isLoggedInMutex.Unlock() + fake.isLoggedInArgsForCall = append(fake.isLoggedInArgsForCall, struct{}{}) + if fake.IsLoggedInStub != nil { + return fake.IsLoggedInStub() + } else { + return fake.isLoggedInReturns.result1 + } +} + +func (fake *FakeRepository) IsLoggedInCallCount() int { + fake.isLoggedInMutex.RLock() + defer fake.isLoggedInMutex.RUnlock() + return len(fake.isLoggedInArgsForCall) +} + +func (fake *FakeRepository) IsLoggedInReturns(result1 bool) { + fake.isLoggedInReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) IsSSLDisabled() bool { + fake.isSSLDisabledMutex.Lock() + defer fake.isSSLDisabledMutex.Unlock() + fake.isSSLDisabledArgsForCall = append(fake.isSSLDisabledArgsForCall, struct{}{}) + if fake.IsSSLDisabledStub != nil { + return fake.IsSSLDisabledStub() + } else { + return fake.isSSLDisabledReturns.result1 + } +} + +func (fake *FakeRepository) IsSSLDisabledCallCount() int { + fake.isSSLDisabledMutex.RLock() + defer fake.isSSLDisabledMutex.RUnlock() + return len(fake.isSSLDisabledArgsForCall) +} + +func (fake *FakeRepository) IsSSLDisabledReturns(result1 bool) { + fake.isSSLDisabledReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) AsyncTimeout() uint { + fake.asyncTimeoutMutex.Lock() + defer fake.asyncTimeoutMutex.Unlock() + fake.asyncTimeoutArgsForCall = append(fake.asyncTimeoutArgsForCall, struct{}{}) + if fake.AsyncTimeoutStub != nil { + return fake.AsyncTimeoutStub() + } else { + return fake.asyncTimeoutReturns.result1 + } +} + +func (fake *FakeRepository) AsyncTimeoutCallCount() int { + fake.asyncTimeoutMutex.RLock() + defer fake.asyncTimeoutMutex.RUnlock() + return len(fake.asyncTimeoutArgsForCall) +} + +func (fake *FakeRepository) AsyncTimeoutReturns(result1 uint) { + fake.asyncTimeoutReturns = struct { + result1 uint + }{result1} +} + +func (fake *FakeRepository) Trace() string { + fake.traceMutex.Lock() + defer fake.traceMutex.Unlock() + fake.traceArgsForCall = append(fake.traceArgsForCall, struct{}{}) + if fake.TraceStub != nil { + return fake.TraceStub() + } else { + return fake.traceReturns.result1 + } +} + +func (fake *FakeRepository) TraceCallCount() int { + fake.traceMutex.RLock() + defer fake.traceMutex.RUnlock() + return len(fake.traceArgsForCall) +} + +func (fake *FakeRepository) TraceReturns(result1 string) { + fake.traceReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ColorEnabled() string { + fake.colorEnabledMutex.Lock() + defer fake.colorEnabledMutex.Unlock() + fake.colorEnabledArgsForCall = append(fake.colorEnabledArgsForCall, struct{}{}) + if fake.ColorEnabledStub != nil { + return fake.ColorEnabledStub() + } else { + return fake.colorEnabledReturns.result1 + } +} + +func (fake *FakeRepository) ColorEnabledCallCount() int { + fake.colorEnabledMutex.RLock() + defer fake.colorEnabledMutex.RUnlock() + return len(fake.colorEnabledArgsForCall) +} + +func (fake *FakeRepository) ColorEnabledReturns(result1 string) { + fake.colorEnabledReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) Locale() string { + fake.localeMutex.Lock() + defer fake.localeMutex.Unlock() + fake.localeArgsForCall = append(fake.localeArgsForCall, struct{}{}) + if fake.LocaleStub != nil { + return fake.LocaleStub() + } else { + return fake.localeReturns.result1 + } +} + +func (fake *FakeRepository) LocaleCallCount() int { + fake.localeMutex.RLock() + defer fake.localeMutex.RUnlock() + return len(fake.localeArgsForCall) +} + +func (fake *FakeRepository) LocaleReturns(result1 string) { + fake.localeReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) Plugins() map[string]string { + fake.pluginsMutex.Lock() + defer fake.pluginsMutex.Unlock() + fake.pluginsArgsForCall = append(fake.pluginsArgsForCall, struct{}{}) + if fake.PluginsStub != nil { + return fake.PluginsStub() + } else { + return fake.pluginsReturns.result1 + } +} + +func (fake *FakeRepository) PluginsCallCount() int { + fake.pluginsMutex.RLock() + defer fake.pluginsMutex.RUnlock() + return len(fake.pluginsArgsForCall) +} + +func (fake *FakeRepository) PluginsReturns(result1 map[string]string) { + fake.pluginsReturns = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakeRepository) UserHomePath() string { + fake.userHomePathMutex.Lock() + defer fake.userHomePathMutex.Unlock() + fake.userHomePathArgsForCall = append(fake.userHomePathArgsForCall, struct{}{}) + if fake.UserHomePathStub != nil { + return fake.UserHomePathStub() + } else { + return fake.userHomePathReturns.result1 + } +} + +func (fake *FakeRepository) UserHomePathCallCount() int { + fake.userHomePathMutex.RLock() + defer fake.userHomePathMutex.RUnlock() + return len(fake.userHomePathArgsForCall) +} + +func (fake *FakeRepository) UserHomePathReturns(result1 string) { + fake.userHomePathReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ClearSession() { + fake.clearSessionMutex.Lock() + defer fake.clearSessionMutex.Unlock() + fake.clearSessionArgsForCall = append(fake.clearSessionArgsForCall, struct{}{}) + if fake.ClearSessionStub != nil { + fake.ClearSessionStub() + } +} + +func (fake *FakeRepository) ClearSessionCallCount() int { + fake.clearSessionMutex.RLock() + defer fake.clearSessionMutex.RUnlock() + return len(fake.clearSessionArgsForCall) +} + +func (fake *FakeRepository) SetApiEndpoint(arg1 string) { + fake.setApiEndpointMutex.Lock() + defer fake.setApiEndpointMutex.Unlock() + fake.setApiEndpointArgsForCall = append(fake.setApiEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetApiEndpointStub != nil { + fake.SetApiEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetApiEndpointCallCount() int { + fake.setApiEndpointMutex.RLock() + defer fake.setApiEndpointMutex.RUnlock() + return len(fake.setApiEndpointArgsForCall) +} + +func (fake *FakeRepository) SetApiEndpointArgsForCall(i int) string { + fake.setApiEndpointMutex.RLock() + defer fake.setApiEndpointMutex.RUnlock() + return fake.setApiEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetApiVersion(arg1 string) { + fake.setApiVersionMutex.Lock() + defer fake.setApiVersionMutex.Unlock() + fake.setApiVersionArgsForCall = append(fake.setApiVersionArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetApiVersionStub != nil { + fake.SetApiVersionStub(arg1) + } +} + +func (fake *FakeRepository) SetApiVersionCallCount() int { + fake.setApiVersionMutex.RLock() + defer fake.setApiVersionMutex.RUnlock() + return len(fake.setApiVersionArgsForCall) +} + +func (fake *FakeRepository) SetApiVersionArgsForCall(i int) string { + fake.setApiVersionMutex.RLock() + defer fake.setApiVersionMutex.RUnlock() + return fake.setApiVersionArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAuthenticationEndpoint(arg1 string) { + fake.setAuthenticationEndpointMutex.Lock() + defer fake.setAuthenticationEndpointMutex.Unlock() + fake.setAuthenticationEndpointArgsForCall = append(fake.setAuthenticationEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetAuthenticationEndpointStub != nil { + fake.SetAuthenticationEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetAuthenticationEndpointCallCount() int { + fake.setAuthenticationEndpointMutex.RLock() + defer fake.setAuthenticationEndpointMutex.RUnlock() + return len(fake.setAuthenticationEndpointArgsForCall) +} + +func (fake *FakeRepository) SetAuthenticationEndpointArgsForCall(i int) string { + fake.setAuthenticationEndpointMutex.RLock() + defer fake.setAuthenticationEndpointMutex.RUnlock() + return fake.setAuthenticationEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetLoggregatorEndpoint(arg1 string) { + fake.setLoggregatorEndpointMutex.Lock() + defer fake.setLoggregatorEndpointMutex.Unlock() + fake.setLoggregatorEndpointArgsForCall = append(fake.setLoggregatorEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetLoggregatorEndpointStub != nil { + fake.SetLoggregatorEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetLoggregatorEndpointCallCount() int { + fake.setLoggregatorEndpointMutex.RLock() + defer fake.setLoggregatorEndpointMutex.RUnlock() + return len(fake.setLoggregatorEndpointArgsForCall) +} + +func (fake *FakeRepository) SetLoggregatorEndpointArgsForCall(i int) string { + fake.setLoggregatorEndpointMutex.RLock() + defer fake.setLoggregatorEndpointMutex.RUnlock() + return fake.setLoggregatorEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetUaaEndpoint(arg1 string) { + fake.setUaaEndpointMutex.Lock() + defer fake.setUaaEndpointMutex.Unlock() + fake.setUaaEndpointArgsForCall = append(fake.setUaaEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetUaaEndpointStub != nil { + fake.SetUaaEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetUaaEndpointCallCount() int { + fake.setUaaEndpointMutex.RLock() + defer fake.setUaaEndpointMutex.RUnlock() + return len(fake.setUaaEndpointArgsForCall) +} + +func (fake *FakeRepository) SetUaaEndpointArgsForCall(i int) string { + fake.setUaaEndpointMutex.RLock() + defer fake.setUaaEndpointMutex.RUnlock() + return fake.setUaaEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAccessToken(arg1 string) { + fake.setAccessTokenMutex.Lock() + defer fake.setAccessTokenMutex.Unlock() + fake.setAccessTokenArgsForCall = append(fake.setAccessTokenArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetAccessTokenStub != nil { + fake.SetAccessTokenStub(arg1) + } +} + +func (fake *FakeRepository) SetAccessTokenCallCount() int { + fake.setAccessTokenMutex.RLock() + defer fake.setAccessTokenMutex.RUnlock() + return len(fake.setAccessTokenArgsForCall) +} + +func (fake *FakeRepository) SetAccessTokenArgsForCall(i int) string { + fake.setAccessTokenMutex.RLock() + defer fake.setAccessTokenMutex.RUnlock() + return fake.setAccessTokenArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetRefreshToken(arg1 string) { + fake.setRefreshTokenMutex.Lock() + defer fake.setRefreshTokenMutex.Unlock() + fake.setRefreshTokenArgsForCall = append(fake.setRefreshTokenArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetRefreshTokenStub != nil { + fake.SetRefreshTokenStub(arg1) + } +} + +func (fake *FakeRepository) SetRefreshTokenCallCount() int { + fake.setRefreshTokenMutex.RLock() + defer fake.setRefreshTokenMutex.RUnlock() + return len(fake.setRefreshTokenArgsForCall) +} + +func (fake *FakeRepository) SetRefreshTokenArgsForCall(i int) string { + fake.setRefreshTokenMutex.RLock() + defer fake.setRefreshTokenMutex.RUnlock() + return fake.setRefreshTokenArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetOrganizationFields(arg1 models.OrganizationFields) { + fake.setOrganizationFieldsMutex.Lock() + defer fake.setOrganizationFieldsMutex.Unlock() + fake.setOrganizationFieldsArgsForCall = append(fake.setOrganizationFieldsArgsForCall, struct { + arg1 models.OrganizationFields + }{arg1}) + if fake.SetOrganizationFieldsStub != nil { + fake.SetOrganizationFieldsStub(arg1) + } +} + +func (fake *FakeRepository) SetOrganizationFieldsCallCount() int { + fake.setOrganizationFieldsMutex.RLock() + defer fake.setOrganizationFieldsMutex.RUnlock() + return len(fake.setOrganizationFieldsArgsForCall) +} + +func (fake *FakeRepository) SetOrganizationFieldsArgsForCall(i int) models.OrganizationFields { + fake.setOrganizationFieldsMutex.RLock() + defer fake.setOrganizationFieldsMutex.RUnlock() + return fake.setOrganizationFieldsArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetSpaceFields(arg1 models.SpaceFields) { + fake.setSpaceFieldsMutex.Lock() + defer fake.setSpaceFieldsMutex.Unlock() + fake.setSpaceFieldsArgsForCall = append(fake.setSpaceFieldsArgsForCall, struct { + arg1 models.SpaceFields + }{arg1}) + if fake.SetSpaceFieldsStub != nil { + fake.SetSpaceFieldsStub(arg1) + } +} + +func (fake *FakeRepository) SetSpaceFieldsCallCount() int { + fake.setSpaceFieldsMutex.RLock() + defer fake.setSpaceFieldsMutex.RUnlock() + return len(fake.setSpaceFieldsArgsForCall) +} + +func (fake *FakeRepository) SetSpaceFieldsArgsForCall(i int) models.SpaceFields { + fake.setSpaceFieldsMutex.RLock() + defer fake.setSpaceFieldsMutex.RUnlock() + return fake.setSpaceFieldsArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetSSLDisabled(arg1 bool) { + fake.setSSLDisabledMutex.Lock() + defer fake.setSSLDisabledMutex.Unlock() + fake.setSSLDisabledArgsForCall = append(fake.setSSLDisabledArgsForCall, struct { + arg1 bool + }{arg1}) + if fake.SetSSLDisabledStub != nil { + fake.SetSSLDisabledStub(arg1) + } +} + +func (fake *FakeRepository) SetSSLDisabledCallCount() int { + fake.setSSLDisabledMutex.RLock() + defer fake.setSSLDisabledMutex.RUnlock() + return len(fake.setSSLDisabledArgsForCall) +} + +func (fake *FakeRepository) SetSSLDisabledArgsForCall(i int) bool { + fake.setSSLDisabledMutex.RLock() + defer fake.setSSLDisabledMutex.RUnlock() + return fake.setSSLDisabledArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAsyncTimeout(arg1 uint) { + fake.setAsyncTimeoutMutex.Lock() + defer fake.setAsyncTimeoutMutex.Unlock() + fake.setAsyncTimeoutArgsForCall = append(fake.setAsyncTimeoutArgsForCall, struct { + arg1 uint + }{arg1}) + if fake.SetAsyncTimeoutStub != nil { + fake.SetAsyncTimeoutStub(arg1) + } +} + +func (fake *FakeRepository) SetAsyncTimeoutCallCount() int { + fake.setAsyncTimeoutMutex.RLock() + defer fake.setAsyncTimeoutMutex.RUnlock() + return len(fake.setAsyncTimeoutArgsForCall) +} + +func (fake *FakeRepository) SetAsyncTimeoutArgsForCall(i int) uint { + fake.setAsyncTimeoutMutex.RLock() + defer fake.setAsyncTimeoutMutex.RUnlock() + return fake.setAsyncTimeoutArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetTrace(arg1 string) { + fake.setTraceMutex.Lock() + defer fake.setTraceMutex.Unlock() + fake.setTraceArgsForCall = append(fake.setTraceArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetTraceStub != nil { + fake.SetTraceStub(arg1) + } +} + +func (fake *FakeRepository) SetTraceCallCount() int { + fake.setTraceMutex.RLock() + defer fake.setTraceMutex.RUnlock() + return len(fake.setTraceArgsForCall) +} + +func (fake *FakeRepository) SetTraceArgsForCall(i int) string { + fake.setTraceMutex.RLock() + defer fake.setTraceMutex.RUnlock() + return fake.setTraceArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetColorEnabled(arg1 string) { + fake.setColorEnabledMutex.Lock() + defer fake.setColorEnabledMutex.Unlock() + fake.setColorEnabledArgsForCall = append(fake.setColorEnabledArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetColorEnabledStub != nil { + fake.SetColorEnabledStub(arg1) + } +} + +func (fake *FakeRepository) SetColorEnabledCallCount() int { + fake.setColorEnabledMutex.RLock() + defer fake.setColorEnabledMutex.RUnlock() + return len(fake.setColorEnabledArgsForCall) +} + +func (fake *FakeRepository) SetColorEnabledArgsForCall(i int) string { + fake.setColorEnabledMutex.RLock() + defer fake.setColorEnabledMutex.RUnlock() + return fake.setColorEnabledArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetLocale(arg1 string) { + fake.setLocaleMutex.Lock() + defer fake.setLocaleMutex.Unlock() + fake.setLocaleArgsForCall = append(fake.setLocaleArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetLocaleStub != nil { + fake.SetLocaleStub(arg1) + } +} + +func (fake *FakeRepository) SetLocaleCallCount() int { + fake.setLocaleMutex.RLock() + defer fake.setLocaleMutex.RUnlock() + return len(fake.setLocaleArgsForCall) +} + +func (fake *FakeRepository) SetLocaleArgsForCall(i int) string { + fake.setLocaleMutex.RLock() + defer fake.setLocaleMutex.RUnlock() + return fake.setLocaleArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetPlugin(arg1 string, arg2 string) { + fake.setPluginMutex.Lock() + defer fake.setPluginMutex.Unlock() + fake.setPluginArgsForCall = append(fake.setPluginArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.SetPluginStub != nil { + fake.SetPluginStub(arg1, arg2) + } +} + +func (fake *FakeRepository) SetPluginCallCount() int { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return len(fake.setPluginArgsForCall) +} + +func (fake *FakeRepository) SetPluginArgsForCall(i int) (string, string) { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return fake.setPluginArgsForCall[i].arg1, fake.setPluginArgsForCall[i].arg2 +} + +func (fake *FakeRepository) Close() { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) + if fake.CloseStub != nil { + fake.CloseStub() + } +} + +func (fake *FakeRepository) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +var _ Repository = new(FakeRepository) diff --git a/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go b/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go new file mode 100644 index 00000000000..822ad26af16 --- /dev/null +++ b/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go @@ -0,0 +1,131 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" +) + +type FakePluginConfiguration struct { + PluginsStub func() map[string]plugin_config.PluginMetadata + pluginsMutex sync.RWMutex + pluginsArgsForCall []struct{} + pluginsReturns struct { + result1 map[string]plugin_config.PluginMetadata + } + SetPluginStub func(string, plugin_config.PluginMetadata) + setPluginMutex sync.RWMutex + setPluginArgsForCall []struct { + arg1 string + arg2 plugin_config.PluginMetadata + } + GetPluginPathStub func() string + getPluginPathMutex sync.RWMutex + getPluginPathArgsForCall []struct{} + getPluginPathReturns struct { + result1 string + } + RemovePluginStub func(string) + removePluginMutex sync.RWMutex + removePluginArgsForCall []struct { + arg1 string + } +} + +func (fake *FakePluginConfiguration) Plugins() map[string]plugin_config.PluginMetadata { + fake.pluginsMutex.Lock() + fake.pluginsArgsForCall = append(fake.pluginsArgsForCall, struct{}{}) + fake.pluginsMutex.Unlock() + if fake.PluginsStub != nil { + return fake.PluginsStub() + } else { + return fake.pluginsReturns.result1 + } +} + +func (fake *FakePluginConfiguration) PluginsCallCount() int { + fake.pluginsMutex.RLock() + defer fake.pluginsMutex.RUnlock() + return len(fake.pluginsArgsForCall) +} + +func (fake *FakePluginConfiguration) PluginsReturns(result1 map[string]plugin_config.PluginMetadata) { + fake.PluginsStub = nil + fake.pluginsReturns = struct { + result1 map[string]plugin_config.PluginMetadata + }{result1} +} + +func (fake *FakePluginConfiguration) SetPlugin(arg1 string, arg2 plugin_config.PluginMetadata) { + fake.setPluginMutex.Lock() + fake.setPluginArgsForCall = append(fake.setPluginArgsForCall, struct { + arg1 string + arg2 plugin_config.PluginMetadata + }{arg1, arg2}) + fake.setPluginMutex.Unlock() + if fake.SetPluginStub != nil { + fake.SetPluginStub(arg1, arg2) + } +} + +func (fake *FakePluginConfiguration) SetPluginCallCount() int { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return len(fake.setPluginArgsForCall) +} + +func (fake *FakePluginConfiguration) SetPluginArgsForCall(i int) (string, plugin_config.PluginMetadata) { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return fake.setPluginArgsForCall[i].arg1, fake.setPluginArgsForCall[i].arg2 +} + +func (fake *FakePluginConfiguration) GetPluginPath() string { + fake.getPluginPathMutex.Lock() + fake.getPluginPathArgsForCall = append(fake.getPluginPathArgsForCall, struct{}{}) + fake.getPluginPathMutex.Unlock() + if fake.GetPluginPathStub != nil { + return fake.GetPluginPathStub() + } else { + return fake.getPluginPathReturns.result1 + } +} + +func (fake *FakePluginConfiguration) GetPluginPathCallCount() int { + fake.getPluginPathMutex.RLock() + defer fake.getPluginPathMutex.RUnlock() + return len(fake.getPluginPathArgsForCall) +} + +func (fake *FakePluginConfiguration) GetPluginPathReturns(result1 string) { + fake.GetPluginPathStub = nil + fake.getPluginPathReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePluginConfiguration) RemovePlugin(arg1 string) { + fake.removePluginMutex.Lock() + fake.removePluginArgsForCall = append(fake.removePluginArgsForCall, struct { + arg1 string + }{arg1}) + fake.removePluginMutex.Unlock() + if fake.RemovePluginStub != nil { + fake.RemovePluginStub(arg1) + } +} + +func (fake *FakePluginConfiguration) RemovePluginCallCount() int { + fake.removePluginMutex.RLock() + defer fake.removePluginMutex.RUnlock() + return len(fake.removePluginArgsForCall) +} + +func (fake *FakePluginConfiguration) RemovePluginArgsForCall(i int) string { + fake.removePluginMutex.RLock() + defer fake.removePluginMutex.RUnlock() + return fake.removePluginArgsForCall[i].arg1 +} + +var _ plugin_config.PluginConfiguration = new(FakePluginConfiguration) diff --git a/cf/configuration/plugin_config/plugin_config.go b/cf/configuration/plugin_config/plugin_config.go new file mode 100644 index 00000000000..bc0120bd0fb --- /dev/null +++ b/cf/configuration/plugin_config/plugin_config.go @@ -0,0 +1,99 @@ +package plugin_config + +import ( + "path/filepath" + "sync" + + "github.com/cloudfoundry/cli/cf/configuration" + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" +) + +type PluginConfiguration interface { + Plugins() map[string]PluginMetadata + SetPlugin(string, PluginMetadata) + GetPluginPath() string + RemovePlugin(string) +} + +type PluginConfig struct { + mutex *sync.RWMutex + initOnce *sync.Once + persistor configuration.Persistor + onError func(error) + data *PluginData + pluginPath string +} + +func NewPluginConfig(errorHandler func(error)) *PluginConfig { + pluginPath := filepath.Join(config_helpers.PluginRepoDir(), ".cf", "plugins") + return &PluginConfig{ + data: NewData(), + mutex: new(sync.RWMutex), + initOnce: new(sync.Once), + persistor: configuration.NewDiskPersistor(filepath.Join(pluginPath, "config.json")), + onError: errorHandler, + pluginPath: pluginPath, + } +} + +/* getter methods */ +func (c *PluginConfig) GetPluginPath() string { + return c.pluginPath +} + +func (c *PluginConfig) Plugins() map[string]PluginMetadata { + c.read() + return c.data.Plugins +} + +/* setter methods */ +func (c *PluginConfig) SetPlugin(name string, metadata PluginMetadata) { + if c.data.Plugins == nil { + c.data.Plugins = make(map[string]PluginMetadata) + } + c.write(func() { + c.data.Plugins[name] = metadata + }) +} + +func (c *PluginConfig) RemovePlugin(name string) { + c.write(func() { + delete(c.data.Plugins, name) + }) +} + +/* Functions that handel locking */ +func (c *PluginConfig) init() { + //only read from disk if it was never read + c.initOnce.Do(func() { + err := c.persistor.Load(c.data) + if err != nil { + c.onError(err) + } + }) +} + +func (c *PluginConfig) read() { + c.mutex.RLock() + defer c.mutex.RUnlock() + c.init() +} + +func (c *PluginConfig) write(cb func()) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.init() + + cb() + + err := c.persistor.Save(c.data) + if err != nil { + c.onError(err) + } +} + +// CLOSERS +func (c *PluginConfig) Close() { + c.read() + // perform a read to ensure write lock has been cleared +} diff --git a/cf/configuration/plugin_config/plugin_config_test.go b/cf/configuration/plugin_config/plugin_config_test.go new file mode 100644 index 00000000000..df144c3fa41 --- /dev/null +++ b/cf/configuration/plugin_config/plugin_config_test.go @@ -0,0 +1,134 @@ +package plugin_config_test + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + . "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + "github.com/cloudfoundry/cli/plugin" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("PluginConfig", func() { + var ( + metadata PluginMetadata + commands1 []plugin.Command + commands2 []plugin.Command + ) + + BeforeEach(func() { + commands1 = []plugin.Command{ + { + Name: "test_1_cmd1", + HelpText: "help text for test1 cmd1", + }, + { + Name: "test_1_cmd2", + HelpText: "help text for test1 cmd2", + }, + } + + commands2 = []plugin.Command{ + { + Name: "test_2_cmd1", + HelpText: "help text for test2 cmd1", + }, + { + Name: "test_2_cmd2", + HelpText: "help text for test2 cmd2", + }, + } + + metadata = PluginMetadata{ + Location: "../../../fixtures/plugins/test_1.exe", + Commands: commands1, + } + }) + + Describe("Reading configuration data", func() { + BeforeEach(func() { + + config_helpers.PluginRepoDir = func() string { + return filepath.Join("..", "..", "..", "fixtures", "config", "plugin-config") + } + }) + + It("returns a list of plugin executables and their location", func() { + pluginConfig := NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + plugins := pluginConfig.Plugins() + + Expect(plugins["Test1"].Location).To(Equal("../../../fixtures/plugins/test_1.exe")) + Expect(plugins["Test1"].Commands).To(Equal(commands1)) + Expect(plugins["Test2"].Location).To(Equal("../../../fixtures/plugins/test_2.exe")) + Expect(plugins["Test2"].Commands).To(Equal(commands2)) + }) + }) + + Describe("Writing configuration data", func() { + BeforeEach(func() { + config_helpers.PluginRepoDir = func() string { return os.TempDir() } + }) + + AfterEach(func() { + os.Remove(filepath.Join(os.TempDir(), ".cf", "plugins", "config.json")) + }) + + It("saves plugin location and executable information", func() { + pluginConfig := NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + + pluginConfig.SetPlugin("foo", metadata) + plugins := pluginConfig.Plugins() + Expect(plugins["foo"].Commands).To(Equal(commands1)) + }) + }) + + Describe("Removing configuration data", func() { + var ( + pluginConfig *PluginConfig + ) + + BeforeEach(func() { + config_helpers.PluginRepoDir = func() string { return os.TempDir() } + pluginConfig = NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + }) + + AfterEach(func() { + os.Remove(filepath.Join(os.TempDir())) + }) + + It("removes plugin location and executable information", func() { + pluginConfig.SetPlugin("foo", metadata) + + plugins := pluginConfig.Plugins() + Expect(plugins).To(HaveKey("foo")) + + pluginConfig.RemovePlugin("foo") + + plugins = pluginConfig.Plugins() + Expect(plugins).NotTo(HaveKey("foo")) + }) + + It("handles when the config is not yet initialized", func() { + pluginConfig.RemovePlugin("foo") + + plugins := pluginConfig.Plugins() + Expect(plugins).NotTo(HaveKey("foo")) + }) + }) +}) diff --git a/cf/configuration/plugin_config/plugin_data.go b/cf/configuration/plugin_config/plugin_data.go new file mode 100644 index 00000000000..ace68262e04 --- /dev/null +++ b/cf/configuration/plugin_config/plugin_data.go @@ -0,0 +1,30 @@ +package plugin_config + +import ( + "encoding/json" + + "github.com/cloudfoundry/cli/plugin" +) + +type PluginData struct { + Plugins map[string]PluginMetadata +} + +type PluginMetadata struct { + Location string + Commands []plugin.Command +} + +func NewData() *PluginData { + return &PluginData{ + Plugins: make(map[string]PluginMetadata), + } +} + +func (pd *PluginData) JsonMarshalV3() (output []byte, err error) { + return json.MarshalIndent(pd, "", " ") +} + +func (pd *PluginData) JsonUnmarshalV3(input []byte) (err error) { + return json.Unmarshal(input, pd) +} diff --git a/cf/configuration/plugin_config/plugin_suite_test.go b/cf/configuration/plugin_config/plugin_suite_test.go new file mode 100644 index 00000000000..288f24d03c0 --- /dev/null +++ b/cf/configuration/plugin_config/plugin_suite_test.go @@ -0,0 +1,13 @@ +package plugin_config_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlugins(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugins Suite") +} diff --git a/src/cf/endpoints.go b/cf/endpoints.go similarity index 100% rename from src/cf/endpoints.go rename to cf/endpoints.go diff --git a/cf/errors/access_denied_error.go b/cf/errors/access_denied_error.go new file mode 100644 index 00000000000..f3d693e1dd2 --- /dev/null +++ b/cf/errors/access_denied_error.go @@ -0,0 +1,14 @@ +package errors + +import . "github.com/cloudfoundry/cli/cf/i18n" + +type AccessDeniedError struct { +} + +func NewAccessDeniedError() *AccessDeniedError { + return &AccessDeniedError{} +} + +func (err *AccessDeniedError) Error() string { + return T("Server error, status code: 403: Access is denied. You do not have privileges to execute this command.") +} diff --git a/cf/errors/empty_dir_error.go b/cf/errors/empty_dir_error.go new file mode 100644 index 00000000000..8be2530ea6b --- /dev/null +++ b/cf/errors/empty_dir_error.go @@ -0,0 +1,17 @@ +package errors + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type EmptyDirError struct { + dir string +} + +func NewEmptyDirError(dir string) error { + return &EmptyDirError{dir: dir} +} + +func (err *EmptyDirError) Error() string { + return err.dir + T(" is empty") +} diff --git a/cf/errors/error.go b/cf/errors/error.go new file mode 100644 index 00000000000..c33fb576e67 --- /dev/null +++ b/cf/errors/error.go @@ -0,0 +1,27 @@ +package errors + +import ( + original "errors" + "fmt" +) + +func New(message string) error { + return original.New(message) +} + +func NewWithFmt(message string, args ...interface{}) error { + return original.New(fmt.Sprintf(message, args...)) +} + +func NewWithError(message string, err error) error { + return NewWithFmt("%s: %s", message, err.Error()) +} + +func NewWithSlice(errs []error) error { + message := "" + + for _, err := range errs { + message = fmt.Sprintf("%s%s\n", message, err.Error()) + } + return New(message) +} diff --git a/cf/errors/exception.go b/cf/errors/exception.go new file mode 100644 index 00000000000..aff9cd99f2d --- /dev/null +++ b/cf/errors/exception.go @@ -0,0 +1,6 @@ +package errors + +type Exception struct { + Message string + DisplayCrashDialog bool +} diff --git a/cf/errors/gateway_error.go b/cf/errors/gateway_error.go new file mode 100644 index 00000000000..6eb6b482ac1 --- /dev/null +++ b/cf/errors/gateway_error.go @@ -0,0 +1,19 @@ +package errors + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type AsyncTimeoutError struct { + url string +} + +func NewAsyncTimeoutError(url string) error { + return &AsyncTimeoutError{url: url} +} + +func (err *AsyncTimeoutError) Error() string { + return fmt.Sprintf(T("Error: timed out waiting for async job '{{.ErrURL}}' to finish", + map[string]interface{}{"ErrURL": err.url})) +} diff --git a/cf/errors/http_error.go b/cf/errors/http_error.go new file mode 100644 index 00000000000..ab35e4d9ec9 --- /dev/null +++ b/cf/errors/http_error.go @@ -0,0 +1,53 @@ +package errors + +import ( + "fmt" + + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type HttpError interface { + error + StatusCode() int // actual HTTP status code + ErrorCode() string // error code returned in response body from CC or UAA +} + +type baseHttpError struct { + statusCode int + apiErrorCode string + description string +} + +type HttpNotFoundError struct { + baseHttpError +} + +func NewHttpError(statusCode int, code string, description string) error { + err := baseHttpError{ + statusCode: statusCode, + apiErrorCode: code, + description: description, + } + switch statusCode { + case 404: + return &HttpNotFoundError{err} + default: + return &err + } +} + +func (err *baseHttpError) StatusCode() int { + return err.statusCode +} + +func (err *baseHttpError) Error() string { + return fmt.Sprintf(T("Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + map[string]interface{}{"ErrStatusCode": err.statusCode, + "ErrApiErrorCode": err.apiErrorCode, + "ErrDescription": err.description}), + ) +} + +func (err *baseHttpError) ErrorCode() string { + return err.apiErrorCode +} diff --git a/cf/errors/invalid_ssl_cert_error.go b/cf/errors/invalid_ssl_cert_error.go new file mode 100644 index 00000000000..154f3985ca1 --- /dev/null +++ b/cf/errors/invalid_ssl_cert_error.go @@ -0,0 +1,25 @@ +package errors + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type InvalidSSLCert struct { + URL string + Reason string +} + +func NewInvalidSSLCert(url, reason string) *InvalidSSLCert { + return &InvalidSSLCert{ + URL: url, + Reason: reason, + } +} + +func (err *InvalidSSLCert) Error() string { + message := T("Received invalid SSL certificate from ") + err.URL + if err.Reason != "" { + message += " - " + err.Reason + } + return message +} diff --git a/cf/errors/invalid_token_error.go b/cf/errors/invalid_token_error.go new file mode 100644 index 00000000000..13a2f5e9199 --- /dev/null +++ b/cf/errors/invalid_token_error.go @@ -0,0 +1,17 @@ +package errors + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type InvalidTokenError struct { + description string +} + +func NewInvalidTokenError(description string) error { + return &InvalidTokenError{description: description} +} + +func (err *InvalidTokenError) Error() string { + return T("Invalid auth token: ") + err.description +} diff --git a/cf/errors/known_error_codes.go b/cf/errors/known_error_codes.go new file mode 100644 index 00000000000..78a713f6e10 --- /dev/null +++ b/cf/errors/known_error_codes.go @@ -0,0 +1,18 @@ +package errors + +const ( + PARSE_ERROR = "1001" + INVALID_RELATION = "1002" + BAD_QUERY_PARAM = "10005" + USER_EXISTS = "20002" + USER_NOT_FOUND = "20003" + ORG_EXISTS = "30002" + SPACE_EXISTS = "40002" + QUOTA_EXISTS = "240002" + SERVICE_INSTANCE_NAME_TAKEN = "60002" + APP_NOT_STAGED = "170002" + APP_STOPPED = "220001" + BUILDPACK_EXISTS = "290001" + SECURITY_GROUP_EXISTS = "300005" + APP_ALREADY_BOUND = "90003" +) diff --git a/cf/errors/model_already_exists_error.go b/cf/errors/model_already_exists_error.go new file mode 100644 index 00000000000..7dfadeeb04d --- /dev/null +++ b/cf/errors/model_already_exists_error.go @@ -0,0 +1,23 @@ +package errors + +import ( + "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type ModelAlreadyExistsError struct { + ModelType string + ModelName string +} + +func NewModelAlreadyExistsError(modelType, name string) *ModelAlreadyExistsError { + return &ModelAlreadyExistsError{ + ModelType: modelType, + ModelName: name, + } +} + +func (err *ModelAlreadyExistsError) Error() string { + return fmt.Sprintf(T("{{.ModelType}} {{.ModelName}} already exists", + map[string]interface{}{"ModelType": err.ModelType, "ModelName": err.ModelName})) +} diff --git a/cf/errors/model_not_found_error.go b/cf/errors/model_not_found_error.go new file mode 100644 index 00000000000..b860bff9c04 --- /dev/null +++ b/cf/errors/model_not_found_error.go @@ -0,0 +1,21 @@ +package errors + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" +) + +type ModelNotFoundError struct { + ModelType string + ModelName string +} + +func NewModelNotFoundError(modelType, name string) error { + return &ModelNotFoundError{ + ModelType: modelType, + ModelName: name, + } +} + +func (err *ModelNotFoundError) Error() string { + return err.ModelType + " " + err.ModelName + T(" not found") +} diff --git a/cf/flag_helpers/cli_flags.go b/cf/flag_helpers/cli_flags.go new file mode 100644 index 00000000000..4f1f55569c6 --- /dev/null +++ b/cf/flag_helpers/cli_flags.go @@ -0,0 +1,61 @@ +package flag_helpers + +import ( + "fmt" + "strings" + "unicode/utf8" + + "github.com/codegangsta/cli" +) + +func NewIntFlag(name, usage string) IntFlagWithNoDefault { + return IntFlagWithNoDefault{cli.IntFlag{Name: name, Usage: usage}} +} + +func NewIntFlagWithValue(name, usage string, value int) IntFlagWithNoDefault { + return IntFlagWithNoDefault{cli.IntFlag{Name: name, Value: value, Usage: usage}} +} + +func NewStringFlag(name, usage string) StringFlagWithNoDefault { + return StringFlagWithNoDefault{cli.StringFlag{Name: name, Usage: usage}} +} + +func NewStringSliceFlag(name, usage string) StringSliceFlagWithNoDefault { + return StringSliceFlagWithNoDefault{cli.StringSliceFlag{Name: name, Usage: usage, Value: &cli.StringSlice{}}} +} + +type IntFlagWithNoDefault struct { + cli.IntFlag +} + +type StringFlagWithNoDefault struct { + cli.StringFlag +} + +type StringSliceFlagWithNoDefault struct { + cli.StringSliceFlag +} + +func (f IntFlagWithNoDefault) String() string { + defaultVal := fmt.Sprintf("'%v'", f.Value) + return strings.Replace(f.IntFlag.String(), defaultVal, "", 1) +} + +func (f StringFlagWithNoDefault) String() string { + defaultVal := fmt.Sprintf("'%v'", f.Value) + return strings.Replace(f.StringFlag.String(), defaultVal, "", 1) +} + +func (f StringSliceFlagWithNoDefault) String() string { + return fmt.Sprintf("%s%s \t%s", prefixFor(f.Name), f.Name, f.Usage) +} + +func prefixFor(name string) (prefix string) { + if utf8.RuneCountInString(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} diff --git a/cf/formatters/bools.go b/cf/formatters/bools.go new file mode 100644 index 00000000000..b4ce4634a66 --- /dev/null +++ b/cf/formatters/bools.go @@ -0,0 +1,13 @@ +package formatters + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" +) + +func Allowed(allowed bool) string { + if allowed { + return T("allowed") + } else { + return T("disallowed") + } +} diff --git a/cf/formatters/bools_test.go b/cf/formatters/bools_test.go new file mode 100644 index 00000000000..0b5d2bbea43 --- /dev/null +++ b/cf/formatters/bools_test.go @@ -0,0 +1,19 @@ +package formatters_test + +import ( + . "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("bool formatting", func() { + Describe("Allowed", func() { + It("is 'allowed' when true", func() { + Expect(Allowed(true)).To(Equal("allowed")) + }) + + It("is 'disallowed' when false", func() { + Expect(Allowed(false)).To(Equal("disallowed")) + }) + }) +}) diff --git a/cf/formatters/bytes.go b/cf/formatters/bytes.go new file mode 100644 index 00000000000..4957fa333c9 --- /dev/null +++ b/cf/formatters/bytes.go @@ -0,0 +1,80 @@ +package formatters + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" +) + +const ( + BYTE = 1.0 + KILOBYTE = 1024 * BYTE + MEGABYTE = 1024 * KILOBYTE + GIGABYTE = 1024 * MEGABYTE + TERABYTE = 1024 * GIGABYTE +) + +func ByteSize(bytes int64) string { + unit := "" + value := float32(bytes) + + switch { + case bytes >= TERABYTE: + unit = "T" + value = value / TERABYTE + case bytes >= GIGABYTE: + unit = "G" + value = value / GIGABYTE + case bytes >= MEGABYTE: + unit = "M" + value = value / MEGABYTE + case bytes >= KILOBYTE: + unit = "K" + value = value / KILOBYTE + case bytes == 0: + return "0" + } + + stringValue := fmt.Sprintf("%.1f", value) + stringValue = strings.TrimSuffix(stringValue, ".0") + return fmt.Sprintf("%s%s", stringValue, unit) +} + +func ToMegabytes(s string) (int64, error) { + parts := bytesPattern.FindStringSubmatch(strings.TrimSpace(s)) + if len(parts) < 3 { + return 0, invalidByteQuantityError() + } + + value, err := strconv.ParseInt(parts[1], 10, 0) + if err != nil { + return 0, invalidByteQuantityError() + } + + var bytes int64 + unit := strings.ToUpper(parts[2]) + switch unit { + case "T": + bytes = value * TERABYTE + case "G": + bytes = value * GIGABYTE + case "M": + bytes = value * MEGABYTE + case "K": + bytes = value * KILOBYTE + } + + return bytes / MEGABYTE, nil +} + +var ( + bytesPattern *regexp.Regexp = regexp.MustCompile(`(?i)^(-?\d+)([KMGT])B?$`) +) + +func invalidByteQuantityError() error { + return errors.New(T("Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB")) +} diff --git a/cf/formatters/bytes_test.go b/cf/formatters/bytes_test.go new file mode 100644 index 00000000000..1cd528cbe46 --- /dev/null +++ b/cf/formatters/bytes_test.go @@ -0,0 +1,78 @@ +package formatters_test + +import ( + . "github.com/cloudfoundry/cli/cf/formatters" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("formatting bytes to / from strings", func() { + It("converts megabytes to a human readable description", func() { + Expect(ByteSize(100 * MEGABYTE)).To(Equal("100M")) + Expect(ByteSize(int64(100.5 * MEGABYTE))).To(Equal("100.5M")) + }) + + It("parses byte amounts with short units (e.g. M, G)", func() { + var ( + megabytes int64 + err error + ) + + megabytes, err = ToMegabytes("5M") + Expect(megabytes).To(Equal(int64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5m") + Expect(megabytes).To(Equal(int64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("2G") + Expect(megabytes).To(Equal(int64(2 * 1024))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("3T") + Expect(megabytes).To(Equal(int64(3 * 1024 * 1024))) + Expect(err).NotTo(HaveOccurred()) + }) + + It("parses byte amounts with long units (e.g MB, GB)", func() { + var ( + megabytes int64 + err error + ) + + megabytes, err = ToMegabytes("5MB") + Expect(megabytes).To(Equal(int64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5mb") + Expect(megabytes).To(Equal(int64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("2GB") + Expect(megabytes).To(Equal(int64(2 * 1024))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("3TB") + Expect(megabytes).To(Equal(int64(3 * 1024 * 1024))) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error when the unit is missing", func() { + _, err := ToMegabytes("5") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + }) + + It("returns an error when the unit is unrecognized", func() { + _, err := ToMegabytes("5MBB") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + }) + + It("allows whitespace before and after the value", func() { + megabytes, err := ToMegabytes("\t\n\r 5MB ") + Expect(megabytes).To(Equal(int64(5))) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/cf/formatters/formatters_suite_test.go b/cf/formatters/formatters_suite_test.go new file mode 100644 index 00000000000..48242d32f65 --- /dev/null +++ b/cf/formatters/formatters_suite_test.go @@ -0,0 +1,19 @@ +package formatters_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFormatters(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Formatters Suite") +} diff --git a/src/cf/formatters/string.go b/cf/formatters/string.go similarity index 100% rename from src/cf/formatters/string.go rename to cf/formatters/string.go diff --git a/cf/i18n/README-i18n.md b/cf/i18n/README-i18n.md new file mode 100644 index 00000000000..0dd506987d7 --- /dev/null +++ b/cf/i18n/README-i18n.md @@ -0,0 +1,84 @@ +# README for CF CLI Localization + +__td;lr The Cloud Foundry cli is ready to be translated to one of the supported languages. See "How you can contribute" to help in this effort.__ + +The CloudFoundry (CF) Command Line Interface (CLI) has been internationalized (i18n) and is now ready for localization (l10n). + +This README details what features are available, what are not, how you can contribute, when, as well as the overarching goals we had in mind as we entered this massive update of the CLI. + +## What? + +The CF CLI is perhaps the most user-facing component of any CF environment. Every user of CF at some point will use the CLI to interact with a specific CF PaaS that they are targeting, be it private or public. + +As CF is a global operating system for clouds, it only made sense for it to be accessible to the many countries all over the world, where English, may not be the best language of communication. + +The CF CLI i18n effort enabled the CLI so that all strings that are used to communicate with the end-user are ready to be translated in the user's native tongue or in some other language that is close to that native tongue. So a French Canadian user can use the CLI in French (fr_FR) and Portuguese user can use the CLI in Brazilian Portuguese (pt_BR). + +Note: Translations only affect messages generated by the CLI. Responses from server side components (Cloud Controller, etc.) will be internationalized seperately and are likely to be English only at the time of this writing. + +User locale is set using the cf config --locale option + +## When? + +Available today are: + +1. A version of the CLI that is enabled for translation. Going forward, all other versions of the CLI will maintain that i18n enablement. This means that any new strings added to the system will follow i18n enablement guideline, e.g., use Go-style templates and use `T()` functions call to load translated strings. + +2. Default language is English in the the en_US locale. This means that any user for any locale will either have strings for their locale loaded, if they exist, otherwise will default to English, specifically the en_US locale. So for instance, a Great Britain user with locale en_GB will default to en_US since there are no en_GB translations. + +3. Complete versions of French (fr_FR), Chinese-Simplified (zh_Hans), Spanish (es_ES), and Portugese-Brazil (pt_BR). Many thanks to all the hard work from the people that contributed! We welcome fixes to any of these translations. See next sections on how to contribute. + +4. Defaulting of language and territory when a specific translation for a territory does not exist. So for instance, a French Canadian speaker with fr_CA locale will have the fr_FR translation strings loaded instead of en_US since that is the closest translation strings to their language and locale. + +## How can you contribute? + +1. __Give it a test drive.__ Download the latest CLI and try it in your locale. You should always at least see no difference since English is the default locale if your current locale does not have any translations yet. If your locale is any of the translated locales, then you should see that the CLI strings are in that language. If you run into error, please submit an issue on Github. + +2. __Submit pull requests for languages files with translations/improvements.__ If you see typos, grammar errors, ambiguous strings or lingering English then please find the appropriate string in the `cf/i18n/resources/_.all.json` and submit a PR with the fix(es). It is much better to submit small pull requests as you complete translations, rather than submitting one giant pull request after you finish everything. This makes it much easier to rapidly merge your changes in. You can also report translations needing fixes as an issue in Github, but if you do understand the language, we would really appreciate the fix. + +### Note on contributing translations + +It is very important that you DO NOT change the `id` sections of the JSON files---simply said, do not change any of the `id` strings, but only the `translation` strings. See the French locale translations as examples. + +``` +[ + { + "id":"Create an org", + "translation":"Créez un org" + }, + ... +] +``` + +Finally, it is also important not to translate the argument names in templated strings. Templated strings are the ones which contain arguments, e.g., `{{.Name}}` or `{{.Username}}` and so on. The arguments can move to a different location on the translated string, however, the arguments cannot change, should not be translated, and should not be removed or new ones added. So for instance, the following string is translated in French as follows: + +``` +[ + ..., + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Créez quota {{.QuotaName}} étant {{.Username}}..." + }, + ... +] +``` + +## Goals + +Our goal is to have translations for the following languages first: + +| Language | Locale | Status | +|-----------------------|--------|-------------------------| +| English | en_US | Complete | +| French | fr_FR | Complete | +| Spanish | es_ES | Complete | +| German | de_DE | English files ready(*) | +| Italian | it_IT | English files ready | +| Japanese | ja_JA | English files ready | +| Portuguese (Brazil) | pt_BR | Complete | +| Chinese (simplified) | zh_Hans | Complete | +| Chinese (traditional) | zh_Hant | English files ready | + +If you are interested in submitting translations for another language/locale, then please communicate with us via VCAP-DEV mailing list first. + +(*) We note "English files ready" to mean that our github project contains English versions for the translation files. We are ready for PRs on these files with actual translated strings. diff --git a/cf/i18n/detection/detection.go b/cf/i18n/detection/detection.go new file mode 100644 index 00000000000..6d8e16ff1f2 --- /dev/null +++ b/cf/i18n/detection/detection.go @@ -0,0 +1,18 @@ +package detection + +import "github.com/pivotal-cf-experimental/jibber_jabber" + +type Detector interface { + DetectIETF() (string, error) + DetectLanguage() (string, error) +} + +type JibberJabberDetector struct{} + +func (detector *JibberJabberDetector) DetectIETF() (string, error) { + return jibber_jabber.DetectIETF() +} + +func (detector *JibberJabberDetector) DetectLanguage() (string, error) { + return jibber_jabber.DetectLanguage() +} diff --git a/cf/i18n/detection/detection_suite_test.go b/cf/i18n/detection/detection_suite_test.go new file mode 100644 index 00000000000..e95d99267f9 --- /dev/null +++ b/cf/i18n/detection/detection_suite_test.go @@ -0,0 +1,13 @@ +package detection_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestDetection(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Detection Suite") +} diff --git a/cf/i18n/detection/fakes/fake_detector.go b/cf/i18n/detection/fakes/fake_detector.go new file mode 100644 index 00000000000..6ac0b37f9e0 --- /dev/null +++ b/cf/i18n/detection/fakes/fake_detector.go @@ -0,0 +1,75 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + . "github.com/cloudfoundry/cli/cf/i18n/detection" +) + +type FakeDetector struct { + DetectIETFStub func() (string, error) + detectIETFMutex sync.RWMutex + detectIETFArgsForCall []struct{} + detectIETFReturns struct { + result1 string + result2 error + } + DetectLanguageStub func() (string, error) + detectLanguageMutex sync.RWMutex + detectLanguageArgsForCall []struct{} + detectLanguageReturns struct { + result1 string + result2 error + } +} + +func (fake *FakeDetector) DetectIETF() (string, error) { + fake.detectIETFMutex.Lock() + defer fake.detectIETFMutex.Unlock() + fake.detectIETFArgsForCall = append(fake.detectIETFArgsForCall, struct{}{}) + if fake.DetectIETFStub != nil { + return fake.DetectIETFStub() + } else { + return fake.detectIETFReturns.result1, fake.detectIETFReturns.result2 + } +} + +func (fake *FakeDetector) DetectIETFCallCount() int { + fake.detectIETFMutex.RLock() + defer fake.detectIETFMutex.RUnlock() + return len(fake.detectIETFArgsForCall) +} + +func (fake *FakeDetector) DetectIETFReturns(result1 string, result2 error) { + fake.detectIETFReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeDetector) DetectLanguage() (string, error) { + fake.detectLanguageMutex.Lock() + defer fake.detectLanguageMutex.Unlock() + fake.detectLanguageArgsForCall = append(fake.detectLanguageArgsForCall, struct{}{}) + if fake.DetectLanguageStub != nil { + return fake.DetectLanguageStub() + } else { + return fake.detectLanguageReturns.result1, fake.detectLanguageReturns.result2 + } +} + +func (fake *FakeDetector) DetectLanguageCallCount() int { + fake.detectLanguageMutex.RLock() + defer fake.detectLanguageMutex.RUnlock() + return len(fake.detectLanguageArgsForCall) +} + +func (fake *FakeDetector) DetectLanguageReturns(result1 string, result2 error) { + fake.detectLanguageReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +var _ Detector = new(FakeDetector) diff --git a/cf/i18n/excluded.json b/cf/i18n/excluded.json new file mode 100644 index 00000000000..efaa0323ac2 --- /dev/null +++ b/cf/i18n/excluded.json @@ -0,0 +1,41 @@ +{ + "excludedStrings" : [ + "", + " ", + "\n", + "\t", + "\n\t", + "extract_strings", + "excluded.json", + "gi18n", + ".en.json", + ".extracted.json", + "recursive:", + ".json", + ".po", + ", column: ", + ", line: ", + ", offset: ", + "msgid ", + "msgstr ", + "# filename: ", + ".", + "\\", + "help", + ".go", + "", + "/", + "false", + "true", + + "allow-paid-service-plans" + ], + "excludedRegexps" : [ + "^\\d+$", + "^[-%]?\\w$", + "^\\w$", + "^json:", + "^\\w*[-]?quota[-]?\\w*$", + "^\\w+-paid-service-plans$" + ] +} diff --git a/cf/i18n/i18n_suite_test.go b/cf/i18n/i18n_suite_test.go new file mode 100644 index 00000000000..758d8d718cd --- /dev/null +++ b/cf/i18n/i18n_suite_test.go @@ -0,0 +1,13 @@ +package i18n_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestI18n(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "i18n Suite") +} diff --git a/cf/i18n/init.go b/cf/i18n/init.go new file mode 100644 index 00000000000..dbd312e8547 --- /dev/null +++ b/cf/i18n/init.go @@ -0,0 +1,158 @@ +package i18n + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/i18n/detection" + resources "github.com/cloudfoundry/cli/cf/resources" + go_i18n "github.com/nicksnyder/go-i18n/i18n" +) + +const ( + DEFAULT_LOCALE = "en_US" + DEFAULT_LANGUAGE = "en" +) + +var T go_i18n.TranslateFunc + +var SUPPORTED_LOCALES = map[string]string{ + "de": "de_DE", + "en": "en_US", + "es": "es_ES", + "fr": "fr_FR", + "it": "it_IT", + "ja": "ja_JA", + //"ko": "ko_KO", - Will add support for Korean when nicksnyder/go-i18n supports Korean + "pt": "pt_BR", + //"ru": "ru_RU", - Will add support for Russian when nicksnyder/go-i18n supports Russian + "zh": "zh_Hans", +} +var Resources_path = filepath.Join("cf", "i18n", "resources") + +func GetResourcesPath() string { + return Resources_path +} + +func Init(config core_config.ReadWriter, detector detection.Detector) go_i18n.TranslateFunc { + var T go_i18n.TranslateFunc + var err error + + locale := config.Locale() + if locale != "" { + err = loadFromAsset(locale) + if err == nil { + T, err = go_i18n.Tfunc(config.Locale(), DEFAULT_LOCALE) + } + } else { + var userLocale string + userLocale, err = initWithUserLocale(detector) + if err != nil { + userLocale = mustLoadDefaultLocale() + } + + T, err = go_i18n.Tfunc(userLocale, DEFAULT_LOCALE) + } + + if err != nil { + panic(err) + } + + return T +} + +func initWithUserLocale(detector detection.Detector) (string, error) { + userLocale, err := detector.DetectIETF() + if err != nil { + userLocale = DEFAULT_LOCALE + } + + language, err := detector.DetectLanguage() + if err != nil { + language = DEFAULT_LANGUAGE + } + + userLocale = strings.Replace(userLocale, "-", "_", 1) + if strings.HasPrefix(userLocale, "zh_TW") || strings.HasPrefix(userLocale, "zh_HK") { + userLocale = "zh_Hant" + language = "zh" + } + + err = loadFromAsset(userLocale) + if err != nil { + locale := SUPPORTED_LOCALES[language] + if locale == "" { + userLocale = DEFAULT_LOCALE + } else { + userLocale = locale + } + err = loadFromAsset(userLocale) + } + + return userLocale, err +} + +func mustLoadDefaultLocale() string { + userLocale := DEFAULT_LOCALE + + err := loadFromAsset(DEFAULT_LOCALE) + if err != nil { + panic("Could not load en_US language files. God save the queen. " + err.Error()) + } + + return userLocale +} + +func loadFromAsset(locale string) error { + assetName := locale + ".all.json" + assetKey := filepath.Join(GetResourcesPath(), assetName) + + byteArray, err := resources.Asset(assetKey) + if err != nil { + return err + } + + if len(byteArray) == 0 { + return errors.New(fmt.Sprintf("Could not load i18n asset: %v", assetKey)) + } + + tmpDir, err := ioutil.TempDir("", "cloudfoundry_cli_i18n_res") + if err != nil { + return err + } + defer func() { + os.RemoveAll(tmpDir) + }() + + fileName, err := saveLanguageFileToDisk(tmpDir, assetName, byteArray) + if err != nil { + return err + } + + go_i18n.MustLoadTranslationFile(fileName) + + os.RemoveAll(fileName) + + return nil +} + +func saveLanguageFileToDisk(tmpDir, assetName string, byteArray []byte) (fileName string, err error) { + fileName = filepath.Join(tmpDir, assetName) + file, err := os.Create(fileName) + if err != nil { + return + } + defer file.Close() + + _, err = file.Write(byteArray) + if err != nil { + return + } + + return +} diff --git a/cf/i18n/init_unix_test.go b/cf/i18n/init_unix_test.go new file mode 100644 index 00000000000..620cf7c6ddc --- /dev/null +++ b/cf/i18n/init_unix_test.go @@ -0,0 +1,229 @@ +// +build darwin freebsd linux netbsd openbsd + +package i18n_test + +import ( + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + go_i18n "github.com/nicksnyder/go-i18n/i18n" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("i18n.Init() function", func() { + var ( + oldResourcesPath string + configRepo core_config.ReadWriter + detector detection.Detector + + T go_i18n.TranslateFunc + ) + + BeforeEach(func() { + configRepo = testconfig.NewRepositoryWithDefaults() + oldResourcesPath = i18n.GetResourcesPath() + i18n.Resources_path = filepath.Join("cf", "i18n", "test_fixtures") + detector = &detection.JibberJabberDetector{} + }) + + JustBeforeEach(func() { + T = i18n.Init(configRepo, detector) + }) + + Describe("When a user has a locale configuration set", func() { + It("panics when the translation files cannot be loaded", func() { + i18n.Resources_path = filepath.Join("should", "not", "be_valid") + configRepo.SetLocale("en_us") + + init := func() { i18n.Init(configRepo, detector) } + Ω(init).Should(Panic(), "loading translations from an invalid path should panic") + }) + + It("Panics if the locale is not valid", func() { + configRepo.SetLocale("abc_def") + + init := func() { i18n.Init(configRepo, detector) } + Ω(init).Should(Panic(), "loading translations from an invalid path should panic") + }) + + Context("when the locale is set to french", func() { + BeforeEach(func() { + configRepo.SetLocale("fr_FR") + }) + + It("translates into french correctly", func() { + translation := T("No buildpacks found") + Ω(translation).Should(Equal("Pas buildpacks trouvés")) + }) + }) + + Context("creates a valid T function", func() { + BeforeEach(func() { + configRepo.SetLocale("en_US") + }) + + It("returns a usable T function for simple strings", func() { + Ω(T).ShouldNot(BeNil()) + + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + }) + + It("returns a usable T function for complex strings (interpolated)", func() { + Ω(T).ShouldNot(BeNil()) + + translation := T("Deleting domain {{.DomainName}} as {{.Username}}...", map[string]interface{}{"DomainName": "foo.com", "Username": "Anand"}) + Ω("Deleting domain foo.com as Anand...").Should(Equal(translation)) + }) + }) + }) + + Describe("When the user does not have a locale configuration set", func() { + AfterEach(func() { + i18n.Resources_path = oldResourcesPath + os.Setenv("LC_ALL", "") + os.Setenv("LANG", "en_US.UTF-8") + }) + + It("panics when the translation files cannot be loaded", func() { + os.Setenv("LANG", "en") + i18n.Resources_path = filepath.Join("should", "not", "be_valid") + + init := func() { i18n.Init(configRepo, detector) } + Ω(init).Should(Panic(), "loading translations from an invalid path should panic") + }) + + Context("loads correct locale", func() { + It("defaults to en_US when LC_ALL and LANG not set", func() { + os.Setenv("LC_ALL", "") + os.Setenv("LANG", "") + + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + }) + + Context("when there is no territory set", func() { + BeforeEach(func() { + os.Setenv("LANG", "en") + }) + + It("still loads the english translation", func() { + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + }) + }) + + Context("when the desired language is not supported", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "zz_FF.UTF-8") + }) + + It("defaults to en_US when langauge is not supported", func() { + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + + translation = T("No buildpacks found") + Ω("No buildpacks found").Should(Equal(translation)) + }) + + Context("because we don't have the territory", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "fr_CA.UTF-8") + }) + + It("defaults to same language in supported territory", func() { + translation := T("No buildpacks found") + Ω("Pas buildpacks trouvés").Should(Equal(translation)) + }) + }) + }) + + Context("translates correctly", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "fr_FR.UTF-8") + }) + + It("T function should return translation if string key exists", func() { + translation := T("No buildpacks found") + Ω("Pas buildpacks trouvés").Should(Equal(translation)) + }) + }) + + Context("matches zh_CN to simplified Chinese", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "zh_CN.UTF-8") + }) + + It("matches to zh_Hans", func() { + translation := T("No buildpacks found") + Ω("buildpack未找到").Should(Equal(translation)) + }) + }) + + Context("matches zh_TW locale to traditional Chinese", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "zh_TW.UTF-8") + }) + + It("matches to zh_Hant", func() { + translation := T("No buildpacks found") + Ω("(Hant)No buildpacks found").Should(Equal(translation)) + }) + }) + + Context("matches zh_HK locale to traditional Chinese", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "zh_HK.UTF-8") + }) + + It("matches to zh_Hant", func() { + translation := T("No buildpacks found") + Ω("(Hant)No buildpacks found").Should(Equal(translation)) + }) + }) + }) + + Context("creates a valid T function", func() { + BeforeEach(func() { + os.Setenv("LC_ALL", "en_US.UTF-8") + }) + + It("returns a usable T function for simple strings", func() { + Ω(T).ShouldNot(BeNil()) + + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + }) + + It("returns a usable T function for complex strings (interpolated)", func() { + Ω(T).ShouldNot(BeNil()) + + translation := T("Deleting domain {{.DomainName}} as {{.Username}}...", map[string]interface{}{"DomainName": "foo.com", "Username": "Anand"}) + Ω("Deleting domain foo.com as Anand...").Should(Equal(translation)) + }) + }) + }) + + Describe("when the config is set to a non-english language and the LANG environamnt variable is en_US", func() { + BeforeEach(func() { + configRepo.SetLocale("fr_FR") + os.Setenv("LANG", "en_US") + }) + + AfterEach(func() { + i18n.Resources_path = oldResourcesPath + os.Setenv("LANG", "en_US.UTF-8") + }) + + It("ignores the english LANG enviornmant variable", func() { + translation := T("No buildpacks found") + Ω(translation).Should(Equal("Pas buildpacks trouvés")) + }) + }) +}) diff --git a/cf/i18n/init_windows_test.go b/cf/i18n/init_windows_test.go new file mode 100644 index 00000000000..15d603d6d59 --- /dev/null +++ b/cf/i18n/init_windows_test.go @@ -0,0 +1,107 @@ +// +build windows + +package i18n_test + +import ( + "path/filepath" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection/fakes" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("i18n.Init() function", func() { + var ( + configRepo core_config.ReadWriter + detector *fakes.FakeDetector + ) + + BeforeEach(func() { + i18n.Resources_path = filepath.Join("cf", "i18n", "test_fixtures") + configRepo = testconfig.NewRepositoryWithDefaults() + detector = &fakes.FakeDetector{} + }) + + Describe("When a user has a locale configuration set", func() { + Context("creates a valid T function", func() { + BeforeEach(func() { + configRepo.SetLocale("en_US") + }) + + It("returns a usable T function for simple strings", func() { + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("Hello world!") + Ω("Hello world!").Should(Equal(translation)) + }) + + It("returns a usable T function for complex strings (interpolated)", func() { + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("Deleting domain {{.DomainName}} as {{.Username}}...", map[string]interface{}{"DomainName": "foo.com", "Username": "Anand"}) + Ω("Deleting domain foo.com as Anand...").Should(Equal(translation)) + }) + }) + }) + + Describe("When a user does not have a locale configuration set", func() { + BeforeEach(func() { + detector.DetectIETFReturns("en-US", nil) + }) + + Context("creates a valid T function", func() { + It("returns a usable T function for simple strings", func() { + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("Change user password") + Ω("Change user password").Should(Equal(translation)) + }) + + It("returns a usable T function for complex strings (interpolated)", func() { + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("Deleting domain {{.DomainName}} as {{.Username}}...", map[string]interface{}{"DomainName": "foo", "Username": "Anand"}) + Ω("Deleting domain foo as Anand...").Should(Equal(translation)) + }) + }) + + }) + + Describe("When locale is HK/TW", func() { + It("matches zh_CN to zh_Hans", func() { + detector.DetectIETFReturns("zh-CN.UTF-8", nil) + detector.DetectLanguageReturns("zh", nil) + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("No buildpacks found") + Ω("buildpack未找到").Should(Equal(translation)) + }) + + It("matches zh_TW to zh_Hant", func() { + detector.DetectIETFReturns("zh-TW.UTF-8", nil) + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("No buildpacks found") + Ω("(Hant)No buildpacks found").Should(Equal(translation)) + }) + + It("matches zh_HK to zh_Hant", func() { + detector.DetectIETFReturns("zh-HK.UTF-8", nil) + T := i18n.Init(configRepo, detector) + Ω(T).ShouldNot(BeNil()) + + translation := T("No buildpacks found") + Ω("(Hant)No buildpacks found").Should(Equal(translation)) + }) + }) +}) diff --git a/cf/i18n/resources/de_DE.all.json b/cf/i18n/resources/de_DE.all.json new file mode 100644 index 00000000000..0feddbd48c3 --- /dev/null +++ b/cf/i18n/resources/de_DE.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nApp started\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Use '{{.Command}}' to target new org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Read-only access to org info and reports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Path should be a zip file, a url to a zip file, or a local directory. Position is an integer, sets priority, and is sorted from lowest to highest.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - View logs, reports, and settings on this space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " View allowable quotas with 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " is already started", + "modified": false + }, + { + "id": " is already stopped", + "translation": " is already stopped", + "modified": false + }, + { + "id": " is empty", + "translation": " is empty", + "modified": false + }, + { + "id": " not found", + "translation": " not found", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "A command line tool to interact with Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API endpoint:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Also delete any mapped routes", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "An org must be targeted before targeting a space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "App name is a required field", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} does not exist.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} is a worker, skipping route creation", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Append API request diagnostics to a log file", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assign a quota to an org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assign a space role to a user", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assign an org role to a user", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authenticate user non-interactively", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authenticating...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "BUILD TIME:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Binding {{.URL}} to {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} already exists", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} does not exist.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be a positive integer with a unit of measurement like M, MB, G, or GB", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USERNAME PASSWORD\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USERNAME PASSWORD", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USERNAME [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NAME VALUE", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NAME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Cannot specify both lock and unlock options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Cannot specify buildpack bits and lock/unlock.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Change or view the instance count, disk space limit, and memory limit for an app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changing password...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Command not found", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Could not determine the current working directory!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Could not find a default domain", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Could not find app named '{{.AppName}}' in manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Could not parse version number: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Could not serialize information", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Could not serialize updates.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Could not target org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Couldn't create temp file for upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Couldn't open buildpack file", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Couldn't write zip file", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Create a buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Create a new user", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Create a random route for this app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "Create an org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creating org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creating route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credentials were rejected, please try again.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Current Password", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Current password did not match", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Custom headers to include in the request, flag can be specified multiple times", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Delete a buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Delete a user", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Delete an app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Delete an org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Delete cancelled", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Deleting buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Deleting org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Disable the buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Disk limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Display health and status for app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Do not colorize output", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do not map a route to this app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Do not start an app after pushing", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump recent logs instead of tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXAMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Enable CF_TRACE output for all requests and responses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Enable HTTP proxying for API requests", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Enable or disable color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Enable the buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Env variable {{.VarName}} was not set.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error building request", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creating request:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error creating tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creating upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error dumping request\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error finding available orgs\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error finding available spaces\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error finding command {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error finding manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error finding org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error finding space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error in requirement", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error opening buildpack file", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error parsing JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error performing request", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error reading manifest file:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error reading response", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error updating buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error uploading application.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error writing to tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error zipping application", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: No name found for app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executes a raw request, content-type set to application/json by default", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Expected applications to be a list", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Expected {{.PropertyName}} to be a boolean.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Expected {{.PropertyName}} to be a list of strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FAILED", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Failed fetching buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Failed fetching events.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Failed fetching orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Failed to create json for resource_match request", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Failed to marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Failed to start oauth request", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Force deletion without confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Force migration without confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force restart of app without prompt", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "GLOBAL OPTIONS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Getting all services from marketplace...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Getting buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Getting info for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Getting orgs as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "HTTP data to include in the request body", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignore manifest file", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Include response headers in the output", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Incorrect Usage.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Incorrect number of arguments", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Incorrect usage", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Invalid JSON response from server", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Invalid Role {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Invalid async response from server", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Invalid auth token: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Invalid manifest. Expected a map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Invalid usage", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Unexpected value for {{.PropertyName}} :\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "List all apps in the target space", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "List all buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "List all orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "List all users in the org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Lock the buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Log user in", + "modified": false + }, + { + "id": "Log user out", + "translation": "Log user out", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Logging out...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint missing from config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Make a user-provided service instance available to cf apps", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Map the root domain to this app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Max wait time for app instance startup, in minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Max wait time for buildpack staging, in minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Start timeout in seconds", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Memory limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrate service instances from one service plan to another", + "modified": false + }, + { + "id": "NAME:", + "translation": "NAME:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "New Password", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "No apps found", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No events for app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No org and space targeted, use '{{.Command}}' to target an org and space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No org or space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No org targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No org targeted, use '{{.Command}}' to target an org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No orgs found", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No service offerings found", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "No space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "No space targeted, use '{{.Command}}' to target a space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "No system-provided env variables have been set", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "No user-defined env variables have been set", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "None of your application files have changed. Nothing will be uploaded.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Number of instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITOR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG MANAGER", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} already exists", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} does not exist.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Override path to default config directory", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Password", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Password verification does not match", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Path of app directory or zip file", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Path to directory or zip file", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Path to manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan {{.ServicePlanName}} cannot be found", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Please don't", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Please log in again", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Print API request diagnostics to stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Print out a list of files in a directory or the contents of a specific file", + "modified": false + }, + { + "id": "Print the version", + "translation": "Print the version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Push a new app or sync changes to an existing app", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Push a single app (with or without a manifest):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "REQUEST:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Received invalid SSL certificate from ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remove a space role from a user", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remove an env variable", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remove an org role from a user", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removing route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Rename a buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Rename an app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Rename an org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Restage an app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Restart an app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "SPACE AUDITOR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "SPACE DEVELOPER", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE MANAGER", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Select a space (or press enter to skip):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Select an org (or press enter to skip):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service {{.ServiceName}} does not exist.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Set an env variable for an app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Set or view target api url", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Set or view the targeted org or space", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Setting api endpoint to {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Show all env variables for an app", + "modified": false + }, + { + "id": "Show help", + "translation": "Show help", + "modified": false + }, + { + "id": "Show org info", + "translation": "Show org info", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Show org users by role", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Show recent app events", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Show space users by role", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Start an app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Startup command, set to null to reset to default start command", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Stop an app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Drain Url", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "System-Provided:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail or show recent logs for an app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Targeted org {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Targeted space {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Buildpack position among other buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "There are no running instances of this app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "There are too many options to display, please type in the name.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Timeout for async HTTP requests", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memory", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace HTTP requests", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint missing from config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USAGE:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Unable to authenticate.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Unlock the buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Update a buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Updating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Uploading app files from: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Uploading buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Uploading {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Use '{{.Name}}' to view or set your target org and space", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Use a one-time password to login", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "User {{.TargetUser}} does not exist.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "User-Provided:", + "modified": false + }, + { + "id": "User:", + "translation": "User:", + "modified": false + }, + { + "id": "Username", + "translation": "Username", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Using manifest file {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Using route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Using stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verify Password", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Warning: error tailing logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive does not contain a buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[environment variables]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[global options] command [arguments...] [command options]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "allowed", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app crashed", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "bound apps", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "crashing", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "disallowed", + "modified": false + }, + { + "id": "disk", + "translation": "disk", + "modified": false + }, + { + "id": "disk:", + "translation": "disk:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "domains:", + "modified": true + }, + { + "id": "down", + "translation": "down", + "modified": false + }, + { + "id": "enabled", + "translation": "enabled", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "env var '{{.PropertyName}}' should not be null", + "modified": false + }, + { + "id": "event", + "translation": "event", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "incorrect usage", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "invalid inherit path in manifest", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "locked", + "modified": false + }, + { + "id": "memory", + "translation": "memory", + "modified": false + }, + { + "id": "memory:", + "translation": "memory:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "not valid for the requested host", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organization", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "plans", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": true + }, + { + "id": "requested state", + "translation": "requested state", + "modified": false + }, + { + "id": "requested state:", + "translation": "requested state:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "running", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "since", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": true + }, + { + "id": "starting", + "translation": "starting", + "modified": false + }, + { + "id": "state", + "translation": "state", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "stopped", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "stopped after 1 redirect", + "modified": false + }, + { + "id": "time", + "translation": "time", + "modified": false + }, + { + "id": "total memory limit", + "translation": "memory limit", + "modified": true + }, + { + "id": "unknown authority", + "translation": "unknown authority", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "usage:", + "modified": false + }, + { + "id": "user", + "translation": "user", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "write default values to the config", + "modified": false + }, + { + "id": "yes", + "translation": "yes", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} of {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} down", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} failing", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} of {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} already exists", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} must be a string or null value", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} must be a string value", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} should not be null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} of {{.TotalCount}} instances running", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} starting", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/en_US.all.json b/cf/i18n/resources/en_US.all.json new file mode 100644 index 00000000000..f9ca84091a0 --- /dev/null +++ b/cf/i18n/resources/en_US.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": false + }, + { + "id": "\nApp started\n", + "translation": "\nApp started\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Use '{{.Command}}' to target new org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Read-only access to org info and reports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "modified": false + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - View logs, reports, and settings on this space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " View allowable quotas with 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " is already started", + "modified": false + }, + { + "id": " is already stopped", + "translation": " is already stopped", + "modified": false + }, + { + "id": " is empty", + "translation": " is empty", + "modified": false + }, + { + "id": " not found", + "translation": " not found", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "A command line tool to interact with Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API endpoint:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for this org", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for this org", + "modified": false + }, + { + "id": "Also delete any mapped routes", + "translation": "Also delete any mapped routes", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "An org must be targeted before targeting a space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "App name is a required field", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} does not exist.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} is a worker, skipping route creation", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Append API request diagnostics to a log file", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assign a quota to an org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assign a space role to a user", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assign an org role to a user", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authenticate user non-interactively", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authenticating...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "BUILD TIME:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Binding {{.URL}} to {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} already exists", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} does not exist.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "modified": false + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USERNAME PASSWORD\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "modified": false + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "modified": false + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USERNAME PASSWORD", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": false + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USERNAME [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [-i INSTANCE] [PATH]", + "modified": false + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "CF_NAME running-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NAME VALUE", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NAME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Cannot specify both lock and unlock options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Cannot specify buildpack bits and lock/unlock.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Change or view the instance count, disk space limit, and memory limit for an app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changing password...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command Name", + "modified": false + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Command not found", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Could not determine the current working directory!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Could not find a default domain", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Could not find app named '{{.AppName}}' in manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Could not parse version number: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Could not serialize information", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Could not serialize updates.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Could not target org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Couldn't create temp file for upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Couldn't open buildpack file", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Couldn't write zip file", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Create a buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Create a new user", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Create a random route for this app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "Create an org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creating org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creating route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credentials were rejected, please try again.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Current Password", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Current password did not match", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Custom headers to include in the request, flag can be specified multiple times", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Delete a buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": "Delete a space quota definition and unassign the space quota from all spaces", + "modified": false + }, + { + "id": "Delete a user", + "translation": "Delete a user", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Delete an app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Delete an org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Delete cancelled", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Deleting buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Deleting org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Disable the buildpack from being used for staging", + "modified": false + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Disk limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Display health and status for app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Do not colorize output", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do not map a route to this app and remove routes from previous pushes of this app.", + "modified": false + }, + { + "id": "Do not start an app after pushing", + "translation": "Do not start an app after pushing", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump recent logs instead of tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXAMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Enable CF_TRACE output for all requests and responses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Enable HTTP proxying for API requests", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Enable or disable color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Enable the buildpack to be used for staging", + "modified": false + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Env variable {{.VarName}} was not set.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error building request", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creating request:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error creating tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creating upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error dumping request\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error finding available orgs\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error finding available spaces\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error finding command {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error finding manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error finding org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error finding space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error in requirement", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error opening buildpack file", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error parsing JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error performing request", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error reading manifest file:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error reading response", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error updating buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error uploading application.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error writing to tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error zipping application", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: No name found for app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executes a raw request, content-type set to application/json by default", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Expected applications to be a list", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Expected {{.PropertyName}} to be a boolean.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Expected {{.PropertyName}} to be a list of strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FAILED", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Failed fetching buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Failed fetching events.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Failed fetching orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Failed to create json for resource_match request", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Failed to marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Failed to start oauth request", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Force deletion without confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Force migration without confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force restart of app without prompt", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "GLOBAL OPTIONS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Getting all services from marketplace...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Getting buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Getting info for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Getting orgs as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "Getting service access as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "Getting service access for service {{.Service}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "HTTP data to include in the request body", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignore manifest file", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Include response headers in the output", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Incorrect Usage.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Incorrect number of arguments", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Incorrect usage", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "Install the plugin defined in command argument", + "modified": false + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Invalid JSON response from server", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Invalid Role {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Invalid async response from server", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Invalid auth token: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Invalid manifest. Expected a map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Invalid usage", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "modified": false + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "List all apps in the target space", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "List all buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "List all orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "List all users in the org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Lock the buildpack to prevent updates", + "modified": false + }, + { + "id": "Log user in", + "translation": "Log user in", + "modified": false + }, + { + "id": "Log user out", + "translation": "Log user out", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Logging out...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint missing from config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Make a user-provided service instance available to cf apps", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Map the root domain to this app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Max wait time for app instance startup, in minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Max wait time for buildpack staging, in minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": false + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "modified": false + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Memory limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrate service instances from one service plan to another", + "modified": false + }, + { + "id": "NAME:", + "translation": "NAME:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "New Password", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "modified": false + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "modified": false + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "modified": false + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "No apps found", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No events for app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No org and space targeted, use '{{.Command}}' to target an org and space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No org or space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No org targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No org targeted, use '{{.Command}}' to target an org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No orgs found", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No service offerings found", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "No space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "No space targeted, use '{{.Command}}' to target a space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "No system-provided env variables have been set", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "No user-defined env variables have been set", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "None of your application files have changed. Nothing will be uploaded.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Number of instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITOR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG MANAGER", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} already exists", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} does not exist.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Override path to default config directory", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Password", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Password verification does not match", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Path to app directory or file", + "modified": false + }, + { + "id": "Path to directory or zip file", + "translation": "Path to directory or zip file", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Path to manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan {{.ServicePlanName}} cannot be found", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Please don't", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Please log in again", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin Name", + "modified": false + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": false + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin {{.PluginName}} successfully uninstalled.", + "modified": false + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Print API request diagnostics to stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Print out a list of files in a directory or the contents of a specific file", + "modified": false + }, + { + "id": "Print the version", + "translation": "Print the version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Push a new app or sync changes to an existing app", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Push a single app (with or without a manifest):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "REQUEST:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Received invalid SSL certificate from ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remove a space role from a user", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remove an env variable", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remove an org role from a user", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removing route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Rename a buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Rename an app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Rename an org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Restage an app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Restart an app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "SPACE AUDITOR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "SPACE DEVELOPER", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE MANAGER", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Select a space (or press enter to skip):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Select an org (or press enter to skip):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service {{.ServiceName}} does not exist.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Set an env variable for an app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Set or view target api url", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Set or view the targeted org or space", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Setting api endpoint to {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Show all env variables for an app", + "modified": false + }, + { + "id": "Show help", + "translation": "Show help", + "modified": false + }, + { + "id": "Show org info", + "translation": "Show org info", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Show org users by role", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Show recent app events", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Show space users by role", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Start an app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Startup command, set to null to reset to default start command", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Stop an app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Drain Url", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "System-Provided:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail or show recent logs for an app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Targeted org {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Targeted space {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "The order in which the buildpacks are checked during buildpack auto-detection", + "modified": false + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan is already inaccessible for all orgs", + "modified": false + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan is already inaccessible for this org", + "modified": false + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "There are no running instances of this app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "There are too many options to display, please type in the name.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Timeout for async HTTP requests", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Total Memory", + "modified": false + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace HTTP requests", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint missing from config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USAGE:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Unable to authenticate.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "Uninstall the plugin defined in command argument", + "modified": false + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Unlock the buildpack to enable updates", + "modified": false + }, + { + "id": "Update a buildpack", + "translation": "Update a buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Updating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Uploading app files from: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Uploading buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Uploading {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Use '{{.Name}}' to view or set your target org and space", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Use a one-time password to login", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "User {{.TargetUser}} does not exist.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "User-Provided:", + "modified": false + }, + { + "id": "User:", + "translation": "User:", + "modified": false + }, + { + "id": "Username", + "translation": "Username", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Using manifest file {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Using route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Using stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verify Password", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Warning: error tailing logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive does not contain a buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[environment variables]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[global options] command [arguments...] [command options]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "access for plans of a particular broker", + "modified": false + }, + { + "id": "access for plans of a particular service offering", + "translation": "access for plans of a particular service offering", + "modified": false + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "allowed", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app crashed", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "bound apps", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "crashing", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "disallowed", + "modified": false + }, + { + "id": "disk", + "translation": "disk", + "modified": false + }, + { + "id": "disk:", + "translation": "disk:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "domains:", + "modified": false + }, + { + "id": "down", + "translation": "down", + "modified": false + }, + { + "id": "enabled", + "translation": "enabled", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "env var '{{.PropertyName}}' should not be null", + "modified": false + }, + { + "id": "event", + "translation": "event", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "incorrect usage", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "invalid inherit path in manifest", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "last uploaded:", + "modified": false + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "locked", + "modified": false + }, + { + "id": "memory", + "translation": "memory", + "modified": false + }, + { + "id": "memory:", + "translation": "memory:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "not valid for the requested host", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organization", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "plans", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": false + }, + { + "id": "requested state", + "translation": "requested state", + "modified": false + }, + { + "id": "requested state:", + "translation": "requested state:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "running", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "since", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": false + }, + { + "id": "starting", + "translation": "starting", + "modified": false + }, + { + "id": "state", + "translation": "state", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "stopped", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "stopped after 1 redirect", + "modified": false + }, + { + "id": "time", + "translation": "time", + "modified": false + }, + { + "id": "total memory limit", + "translation": "total memory limit", + "modified": false + }, + { + "id": "unknown authority", + "translation": "unknown authority", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "usage:", + "modified": false + }, + { + "id": "user", + "translation": "user", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "write default values to the config", + "modified": false + }, + { + "id": "yes", + "translation": "yes", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} of {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} down", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} failing", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} of {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} already exists", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} must be a string or null value", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} must be a string value", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} should not be null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} of {{.TotalCount}} instances running", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} starting", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/es_ES.all.json b/cf/i18n/resources/es_ES.all.json new file mode 100644 index 00000000000..8316a826952 --- /dev/null +++ b/cf/i18n/resources/es_ES.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nLa App empezo\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Asignar rol con '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Usar '{{.Command}}' para seleccionar una nueva org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: usar 'cf login -a API --skip-ssl-validation' o 'cf api API --skip-ssl-validation' para evitar este error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (Escapar comillas de ser usadas en la password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (Usar comillas para passwords con espacios)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omitir nombre de usuario y clave para loguearse interactivamente -- CF_NAME preguntará por ambas)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME provee un password que se podrá usar una vez para el login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (Escapar comillas de ser usadas en la clave)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (Usar comillas para claves con espacios)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (especifica nombre de usuario y password como argumentos)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Acceso de solo lectura a información y reportes de una org\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invita y maneja usuarios, selecciona y cambia planes, y ajusta los límites de gasto\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " La ruta debe ser un archivo zip, una url a un archivo zip, o in directorio local. La posicion es un entero, setea prioridad, y es ordenado de menor a mayor.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - Examina logs, reportes, y ajustes en este space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Crea y maneja apps y servicios, y examina logs y reportes\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invita y manja usuarios, y habilita funcionalidades para un space dado\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " Muestra cuotas permitidas con 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUMERO_DE_INSTANCIAS] [-k DISCO] [-m MEMORIA] [-n HOST] [-p RUTA] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " Ya ha comenzado", + "modified": false + }, + { + "id": " is already stopped", + "translation": " ya ha parado", + "modified": false + }, + { + "id": " is empty", + "translation": " esta vacio", + "modified": false + }, + { + "id": " not found", + "translation": " no encontrado", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "Una aplicacion de línea de comando para interactuar con Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (ej. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "Endpoint API:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Agrega una ruta url a la app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Agregando ruta {{.URL}} a app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "También borra cualquier ruta mapeada", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "una org debe estar seleccionada antes de seleccionar el space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "El nombre de la App también es un campo requerido", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "La app {{.AppName}} no existe.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "La app {{.AppName}} es un worker, saltando la ruta de creación", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "La app {{.AppName}} ya esta ligada a {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Anade diagnósticos de solicitud API al archivo de log", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Asigna una cuota a una org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Asigna un rol al spaces para el usuario", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Asigna un rol a la org para el usuario", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Asignando rol {{.Role}} a usuario {{.TargetUser}} en org {{.TargetOrg}} / space {{.TargetSpace}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Asignando rol {{.Role}} a usuario {{.TargetUser}} en org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Auténtica usuario no interactivamente", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Autenticando...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "Tiempo de construcción:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding servicio {{.ServiceName}} a la app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Vinculando {{.URL}} a {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "El buildpack {{.BuildpackName}} ya existe", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "El buildpack {{.BuildpackName}} no existe.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "La cantidad de bytes debe ser un número entero positivo con la unidad medida en M, MB, G, o GB", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USUARIO CLAVE\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIEMPO DE ESPERA EN MINUTOS] [--trace true | false | path/to/file] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMINIO", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME crea-cuota CUOTA [-m MEMORIA] [-r RUTAS] [-s INSTANCIAS_DE_SERVICIOS] [--permitir-planes-de-servicios-pagos]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMINIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USUARIO CLAVE URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMINIO", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USUARIO CLAVE", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METODO] [-H CABECERA] [-d DATA] [--output ARCHIVO]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMINIO [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME borrar-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME borra-cuota Cuota [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMINIO [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMINIO [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME borra-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USUARIO [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USUARIO] [-p CLAVE] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME salio", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMINIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME cuota CUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME cuotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME renombrar-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCIAS] [-k DISCO] [-m MEMORIA] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NOMBRE VALOR", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USUARIO ORG ROL\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME establecer-quota ORG CUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USUARIO ORG SPACE ROL\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMINIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NOMBRE", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USUARIO ORG ROL\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USUARIO ORG SPACE ROL\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USUARIO CLAVE URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREANDO ARCHIVO DE LOG {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Puede proveer instancias de planes de servicios pagos", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "no se puede proveer instancias de planes de servicios pagos", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "No se puede especificar ambos con la opción de lock y unlock", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "No se pueden especificar ambos {{.Enabled}} y {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "No se puede especificar los bits del buildpack y bloquear/desbloquear", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Cambia o muestra el contador de instancias, y límites de memoria de una app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Cambia clave de usuario", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Cambiando clave...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Comando no encontrado", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Conectando, dumping logs recientes de la app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Conectando, tailing logs para la app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "No se pudo asociar el servicio {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "No se pudo determinar el directorio de trabajo actual!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "No se pudo encontrar el dominio por defecto", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "No se pudo encontrar la app llamada Could not find app named '{{.AppName}}' en el manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "No se pudo encontrar el servicio {{.ServiceName}} para asociar a {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "No se pudo parsear el numero de version: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "No se pudo serializar la información", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "No se pudo serializar la actualización.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "No se pudo seleccionar la org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "No se pudo crear el archivo temporal de subida.", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "No se pudo abrir el archivo de buildpack", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "No se pudo escribir el archivo zip", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Crea un buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Crea un dominio en una org para usar posteriormente", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Crea un dominio que puede ser usado por todas las orgs(solo admin)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Crea un nuevo usuario", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Crea una ruta aleatoria para esta app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Crea un corredor de servicio", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Crea una ruta url en el space para su posterior uso", + "modified": false + }, + { + "id": "Create an org", + "translation": "Crea una org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creando app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creando buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creando dominio {{.DomainName}} para la org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creando org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creando cuota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creando ruta {{.Hostname}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creando ruta {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creando servicio {{.ServiceName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "creando dominio compartido {{.DomainName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creando servicio provisto por el usuario {{.ServiceName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creando usuario {{.TargetUser}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "las credenciales fueron rechazadas, por favor intente nuevamente.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Clave Actual", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "La clave actual no coincide", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "buildpack personalizado por nombre (ej. my-buildpack) o por GIT URL (ej. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Cabeceras personalizadas a ser incluidas en la solicitud, La bandera puede ser especificada múltiples veces", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMINIOS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define un nuevo recurso de cuota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Borra un buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Borra un dominio", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Borra una cuota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Borra una ruta", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Borra un corredor de servicio", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Borra un dominio compartido", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Borra un usuario", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Borra todas las rutas huérfanas (ej.: aquellas que no estan mapeadas a una app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Borra una app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Borra una org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Borrado cancelado", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Borrando app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Borrando buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Borrando dominio{{.DomainName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Borrando org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Borrando cuota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Borrando ruta {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Borrando ruta {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token como {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} en la org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Borrando usuario {{.TargetUser}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Descripcion: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Desactiva el buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Limite de Disco (ej. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Muestra salud y estado de una app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "No coloriza la salida", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do mapea ruta a esta app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "No empieza una app después de subirse", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Url de documentacion: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (ej. ejemplo.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Arroja logs recientes en vez de tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EJEMPLO:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Habilita la salida de CF_TRACE para todas las solicitudes y respuestas", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Habilita HTTP proxying para solicitudes API", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Habilitar o desabilitar color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Habilita el buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Variable de Env {{.VarName}} no fue establecido.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error construyendo solicitud", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creando solicitud:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error al crear el archivo temporal: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creando subida", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creando usuario {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error borrando buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error arrojando respuesta\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error arrojando respuesta\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error encontrando orgs disponibles\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error encontrando spaces disponibles\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error al encontrar comando {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error encontrando manifesto", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error encontrando org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error encontrando space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error en el requerimiento", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error calculando las referencias del JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error al abrir el archivo de buildpack", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error analizando JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error analizando las cabeceras", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error realizando solicitud", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error leyendo el archivo de manifiesto:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error al leer la respuesta", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renombrando buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolviendo ruta:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error actualizando buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error subiendo aplicacion.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error al subir el buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error escribiendo el archivo temporal: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error al comprimir la aplicacion", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: ningun nombre encontrado para la app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: se acabo el tiempo de espera esperando el trabajo asincrono '{{.ErrURL}}' para terminar", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Ejecuta una solicitud cruda, el content-type está configurado por defecto a application/json", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Se espera que la aplicacion sea una lista de pares clave/valor\nHubo un error en el manifesto cerca de:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Se espera que las aplicaciones sean una lista", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Se espera {{.Name}} que sea un set de clave =\u003e valor, pero fue un {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Se espera {{.PropertyName}} que sea un booleano.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Se espera {{.PropertyName}} que sea una lista de cadenas.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Se espera que {{.PropertyName}} sea un número, pero fue un {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FALLO", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Fallo trayendo buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Fallo al obtener dominios.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Fallo al recuperar eventos.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Fallo trayendo usuarios-de-org para rol {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Fallo al obtener orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Fallo trayendo rutas.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Fallo trayendo usuarios-de-space para rol {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Error al crear json para la solicitud de resource_match", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Fallo en calcular las referencias del JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Fallo al comenzar la solicitud oauth", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Fuerza borrado sin confirmacion", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Forza migracion sin confirmacion", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Fuerza reinicio de la app sin sugerencias.", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "OPCIONES GLOBALES", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Obteniendo todos los servicios del mercado...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obteniendo apps en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Obteniendo buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Obteniendo dominios en la org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obteniendo variables de env para app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Obteniendo eventos para app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obteniendo archivos para app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Obteniendo info para la org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Obteniendo info del space {{.TargetSpace}} en org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Trayendo las orgs como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Obteniendo info de cuota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Obteniendo cuotas como {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Obteniendo rutas como {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} como {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obteniendo stacks en org {{.OrganizationName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Obteniendo usuarios en la org {{.TargetOrg}} / space {{.TargetSpace}} como {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Obteniendo usuarios en la org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "Data HTTP a incluir en el cuerpo de la solicitud", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "Metodo HTTP (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Nombre de host (ej. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignora archivo de manifesto", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Incluye las cabeceras de la respuesta en la salida", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Uso incorrecto", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Uso Incorrecto.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Uso incorrecto. Banderas de línea de comando (excepto -f) no pudieron ser aplicadas subiendo multiples apps de un archivo de manifiesto.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Numero incorrecto de argumentos", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Uso incorrecto", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Respuesta JSON invalida del servidor", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Rol Invalido {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Certificado SSL invalido para {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Respuesta asíncrona inválida del servidor", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Token de autenticacion inválido: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Cuota de disco invalida: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Cuota de disco invalida: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "cantidad de instancias invalido: {{.InstanceCount}}\nEl contador de instancias debe ser un integer positivo", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "cantidad de instancias invalido: {{.InstancesCount}}\nEl contador de instancias deber ser un integer positivo", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Manifesto invalido. Se espera un mapa", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Limite de memoria invalido: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Limite de memoria invalido: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Limite de memoria invalido: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Posicion invalida. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Parametro de timeout invalido: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Uso invalido", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Valor inesperado para {{.PropertyName}} :\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "Lista todas las apps en el space seleccionado", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "Lista todos los buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "Lista todas las orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "Lista todas las rutas del space actual", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Lista todos los stacks(un stack es un sistema de archivos pre construid, Incluyendo un sistema operativo, que puede correr apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "Lista todos los usuarios en la org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "Lista uso de cuota disponible", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "Lista dominios en la org apuntada", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "Lista los tokens de autenticacion de servicios", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List los corredores de servicios", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Bloquea el buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Inicia sesión con usuario", + "modified": false + }, + { + "id": "Log user out", + "translation": "Cierra sesion con usuario", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Cerrando sesión...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "El endpoint de Loggregator no esta presente en el config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Hace que un servicio provisto por el usuario este disponible en las apps de cf", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Mapea el dominio raiz a esta app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Tiempo máximo de espera para comienzo de instancia, en minutos", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Tiempo máximo de espera máximo para buildpack staging, en minutos", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Tiempo de espera en segundos", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Limite de memoria (ej. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migra instancias de servicios un plan a otro", + "modified": false + }, + { + "id": "NAME:", + "translation": "NOMBRE:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "Nueva Clave", + "modified": false + }, + { + "id": "New name", + "translation": "Nuevo nombre", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No hay endpoint API seleccionado. usar '{{.LoginTip}}' o '{{.APITip}}' para seleccionar un endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No hay un edpoint para la api establecido. Usar '{{.Name}}' para establecer un endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "Apps no encontradas", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No se encontraron builpacks", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No se encontraron dominios", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No hay eventos para la app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No hay org o space seleccionado, usar '{{.Command}}' para seleccionar una org y un space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No se ha seleccion org o space, usar '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No se ha seleccionado una org, usar '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No hay org seleccionada, usar '{{.Command}}' para seleccionar una org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No se encontraron orgs", + "modified": false + }, + { + "id": "No routes found", + "translation": "No se encontraron rutas", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No se encontraron ofertas de servicio", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "Sin space seleccionado, usar '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "no se ha seleccionado un space, usar '{{.Command}}' para seleccionar space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "Ninguna variable de entorno provista por el sistema ha sido establecida", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "Ninguna variable de entorno provista por el usuario ha sido establecida", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "Ninguno archivo de la aplicación ha cambiado. Nada sera subido.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "No hay session iniciada. Usar '{{.CFLoginCommand}}' Para iniciar sesion.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Numero de instancias", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "AUDITOR DE ORG", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "MANAGER DE ORG", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "La org {{.OrgName}} todavia existe", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "La org {{.OrgName}} no existe.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Sobreescribe la ruta al directorio de configuración por default", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Planes pagos de servicio", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Clave", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "La verificacion de la Clave no coincide", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Ruta del directorio de la aplicacion o archivo zip", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Ruta al directorio o al archivo zip", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Ruta al manifesto", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "El plan {{.ServicePlanName}} no se pudo encontrar", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Por favor elegir entre permitir o denegar. Ambas banderas no no se pueden pasar al mismo comando.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Por favor no", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Por favor iniciar sesión nuevamente", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Imprime el diagnostico de solicitudes API a stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Imprime una lista de archivos en un directorio o los contenidos de un archivo específico.", + "modified": false + }, + { + "id": "Print the version", + "translation": "Imprime la version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Propiedad '{{.PropertyName}}' encontrada en el manifesto. Esta funcionalidad ya no es soportada. Por favor removerla e intentar nuevamente.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Sube una nueva app o sincroniza cambios a una existente", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Sube una unica app (con o sin un archivo de manifiesto):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "La definicion de quota {{.QuotaName}} todavia existe", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "La cuota {{.QuotaName}} no existe", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "SOLICITUD:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPUESTA:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Realmente borrar rutas huerfanas?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Borrar realmente el {{.ModelType}} {{.ModelName}} y todo lo que esté asociado a él?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "En verdad quiere borrar el {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Purgar realmente la oferta del servicio {{.ServiceName}} de Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Recibio certificado SSL invalido de ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remueve un rik en space del usuario", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remueve una ruta url de una app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remueve una variable de entorno", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remueve un rol en org del usuario", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removiendo variable de entorno {{.VarName}} de la app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removiendo rol {{.Role}} del usuario {{.TargetUser}} en la org {{.TargetOrg}} / space {{.TargetSpace}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removiendo rol {{.Role}} del usuario {{.TargetUser}} en org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removiendo ruta {{.URL}} de app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removiendo ruta {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Renombrar un buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Renombrar una app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Renombra una org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renombrando app {{.AppName}} a {{.NewName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renombrando buildpack {{.OldBuildpackName}} a {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renombrando la org {{.OrgName}} a {{.NewName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} como {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "re-stageing de una app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "re-stagging de app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Reiniciar una app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Routa {{.URL}} todavia existe", + "modified": false + }, + { + "id": "Routes", + "translation": "Rutas", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "GRUPO DE SEGURIDAD", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "AUDITOR DEL SPACE", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "DEVELOPER DEL SPACE", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "MANAGER DEL SPACE", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Escala app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Grupos de seguridad:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Elegir un space (o presionar enter para omitir):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Elegir una org(o presionar enter para omitir):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Error en servidor, codigo de estado: {{.ErrStatusCode}}, codigo de error: {{.ErrApiErrorCode}}, mensaje: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "El servicio {{.ServiceName}} no existe.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Servicios", + "modified": false + }, + { + "id": "Services:", + "translation": "Servicios:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Configura una variable de entorno para una app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Configura o muestra la url de api seleccionada", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Configura o muestra la org y space seleccionada", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Configurando endpoint api a {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Estableciendo variable de entorno '{{.VarName}}' a '{{.VarValue}}' para app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Estableciendo cuota {{.QuotaName}} a la org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Muestra todas las variables de entorno para una app", + "modified": false + }, + { + "id": "Show help", + "translation": "Mostrar ayuda", + "modified": false + }, + { + "id": "Show org info", + "translation": "Muestra info de org", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Muestra los usuarios de una org por rol", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Muestra info de cuota", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Muestra eventos recientes de una app", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Muestra los usuarios de un space por rol", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Mostrando como escala la app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Mostrando saludo y estado de la app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack a usar(un stack es un sistema de archivos ya construido, incluyendo un sistema operativo, que puede correr las apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Inicia una app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Tiempo de espera para comienzo de app\n\nTIP: usar '{{.Command}}' para mas informacion", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Comienzo fracasado\n\nTIP: usar '{{.Command}}' para mas informacion", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Comenzando app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Comando de inicio, establecer a null para resetear al comando por defecto de inicio", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Para una app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Parando app {{.AppName}} en org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Url para drenar Syslog", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "Provisto-por-el-sistema:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Usar '{{.ApiCommand}}' para continuar con un endpoint API inseguro", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Usar '{{.CFCommand}}' para asegurarse que los cambios en tus variables de entorno surtan efecto", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Usar '{{.Command}}' para asegurar que los cambios en las variables de entorno hagan efecto", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: usar '{{.CfUpdateBuildpackCommand}}' para actualizar este buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail o muestra logs recientes de una app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Org seleccionada {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Space seleccionado {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "La posicion entre otros buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "La ruta {{.URL}} todavia esta en uso.\nTIP: Cambiar el nombre de host con -n HOSTNAME o usar --random-route para generar una nueva ruta y luego subirla nuevamente.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "No hay instancias en marcha de esta app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "Hay muchas opciones para mostrar, por favor ingrese el nombre.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "Este dominio es compartido entre todas las orgs.\nBorranlo removerá todas las rutas asociadas, y hará que cualquier app con este dominio no sea direcionable.\nEsta seguro que quiere borrar el dominio {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "Esto causará que la app reinicie. Esta seguro que quiere escalar {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Tiempo de espera para solicitudes HTTP asíncronas", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memoria", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Cantidad total de memoria (ej. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Numero total de rutas", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Numero total de instancias de servicios", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Reastea solicitudes HTTP", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint no esta presente en el config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USO:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "No se pudo acceder al space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "No se pudo autenticar.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "no se puede borrar, la ruta '{{.URL}}' no existe.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Desbloquea el buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Actualiza buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Actualiza un corredor de serivicio", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Actualiza un recurso de cuota existente", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Actualizando app {{.AppName}} en la org {{.OrgName}} / space {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Subiendo buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Actualizando cuota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Subiendo archivos de la app desde: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Subiendo buildpack{{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Subiendo {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Subiendo {{.ZipFileBytes}}, {{.FileCount}} archivos", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Usar '{{.Name}}' para mostrar o establecer la org y space seleccionadas", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Usar un clave por única vez para iniciar sesión", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "El usuario {{.TargetUser}} no existe.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "Provisto-por-el-Usuario:", + "modified": false + }, + { + "id": "User:", + "translation": "Usuario:", + "modified": false + }, + { + "id": "Username", + "translation": "Usuario", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Usando archivo de manifest {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Usando ruta {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Usando stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verificar Clave", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "ADVERTENCIA:\n Proporcionar tu clave como una opción de línea de comando es desaconsejable\n Tu clave puede ser visible por otros y ser grabada de el historial del shell\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "ADVERTENCIA: Esta operacion asume que el corredor de servicio responsable de esta oferta ya no esta disponible, Y todas las instancias del servicio han sido borradas, dejando servicios huérfanos en los registros de la base de datos de Cloud Foundry. Todo los conocimientos del servicio van a ser removidos de Cloud Foundry, incluyendo instancias del servicio y asociaciones al mismo. No se intentará contactar al corredor de servicio; correr este comando sin destruir el corredor de servicio causará instancias del servicio huérfanas. Despues de correr este comando tal vez quiera correr o delete-service-auth-token o delete-service-broker para completar la limpieza.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Advertencia: endpoint inseguro de API http detectado: se recomienda usar https para API\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Advertencia: error al hacer tail a los logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "El archivo Zip no contiene un builpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[variables de entorno]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[opciones globales] comando [argumentos...] [opciones del comando]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "permitido", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "La app rompio", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "la solicitud ouath fallo", + "modified": false + }, + { + "id": "bound apps", + "translation": "apps ligadas", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "rompio", + "modified": false + }, + { + "id": "description", + "translation": "descripcion", + "modified": false + }, + { + "id": "disallowed", + "translation": "denegado", + "modified": false + }, + { + "id": "disk", + "translation": "disco", + "modified": false + }, + { + "id": "disk:", + "translation": "disco:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "dominio", + "modified": false + }, + { + "id": "domains:", + "translation": "dominios:", + "modified": false + }, + { + "id": "down", + "translation": "caida", + "modified": false + }, + { + "id": "enabled", + "translation": "habilitado", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "la variable de entorno '{{.PropertyName}}' no deberia ser null", + "modified": false + }, + { + "id": "event", + "translation": "evento", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "Fallo al apagar el eco de la consola para la entrada de clave:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "uso incorrecto", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instancia: {{.InstanceIndex}}, razon: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instancias", + "modified": false + }, + { + "id": "instances:", + "translation": "instancias:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "la ruta al manifesto heredada es invalida", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "Valor invalido para la variable de entorno CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "Valor invalido para la variable de entorno CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "bloqueado", + "modified": false + }, + { + "id": "memory", + "translation": "memoria", + "modified": false + }, + { + "id": "memory:", + "translation": "memoria:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "No valido para el host solicitado", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organización", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "adueneada", + "modified": false + }, + { + "id": "paid service plans", + "translation": "planes de servicios pagos", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "planes", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quotas:", + "modified": true + }, + { + "id": "requested state", + "translation": "Estado solicitado", + "modified": false + }, + { + "id": "requested state:", + "translation": "Estado solicitado:", + "modified": false + }, + { + "id": "routes", + "translation": "rutas", + "modified": false + }, + { + "id": "running", + "translation": "en marcha", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "instancias de servicio", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "compartida", + "modified": false + }, + { + "id": "since", + "translation": "desde", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": false + }, + { + "id": "starting", + "translation": "iniciando", + "modified": false + }, + { + "id": "state", + "translation": "estado", + "modified": false + }, + { + "id": "status", + "translation": "estado", + "modified": false + }, + { + "id": "stopped", + "translation": "paro", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "Paro despues de 1 redireccion", + "modified": false + }, + { + "id": "time", + "translation": "tiempo", + "modified": false + }, + { + "id": "total memory limit", + "translation": "limite de memoria", + "modified": true + }, + { + "id": "unknown authority", + "translation": "autoridad desconocida", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "uso:", + "modified": false + }, + { + "id": "user", + "translation": "usuario", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "escribe valores por defecto en la configuración", + "modified": false + }, + { + "id": "yes", + "translation": "si", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (version de API: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} de {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} caidas", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: usar '{{.Command}}' para mas informacion", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} fallando", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} de {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} ya existe", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} Debe ser una cadena o null como valor", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} Debe ser una cadena como valor", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} no deberia ser null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} de {{.TotalCount}} instancias en marcha", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} iniciando", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instancias", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/fr_FR.all.json b/cf/i18n/resources/fr_FR.all.json new file mode 100644 index 00000000000..dc34e85dc2e --- /dev/null +++ b/cf/i18n/resources/fr_FR.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "App commencé", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Attribuer des rôles avec '{{.CurrentUser}} set-org-role' '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Utilisez '{{.Command}}' pour cibler nouvelle org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: utiliser 'cf login -a API --skip-ssl-validation' ou 'cf api API --skip-ssl-validation' pour supprimer cette erreur", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": "BillingManager - Créer et gérer le compte de facturation et informations de paiement\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": "CF_NAME auth name@example.com \"\\\"mot de passe\\\"\" (échapper les guillemets s'il est utilisé dans un mot de passe)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": "CF_NAME auth name@example.com \"mon mot de passe\" (utilisez des guillemets pour les mots de passe avec un espace)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": "CF_NAME login (nom de l'utilisateur et mot de passe omettre de se connecter de manière interactive - CF_NAME vous invite à la fois pour les deux)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": "CF_NAME connexion - SSO (CF_NAME fournira une URL pour obtenir un mot de passe unique pour se connecter)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": "CF_NAME login-u name@example.com-p \"\\\"mot de passe\\\"\" (échapper les guillemets s'il est utilisé dans un mot de passe)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": "CF_NAME login-u name@example.com-p \"mot de passe\" (utilisez des guillemets pour les mots de passe avec un espace)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": "CF_NAME login -u name@example.com -p pa55word (préciser nom de l'utilisateur et mot de passe comme arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMANDE] [-d DOMAINE] [-f MANIFEST_PATH]", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": "OrgAuditor - accès en lecture seule à org informations et rapports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": "OrgManager - Inviter et gérer les utilisateurs, sélectionner et modifier les plans, et fixer des limites de dépenses\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Chemin doit être un fichier zip, une URL pour un fichier zip, ou un répertoire local. Position est un nombre entier, définit la priorité, et est triée par ordre croissant.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Appuyez plusieurs applications avec un manifeste:", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": "SpaceAuditor - Voir les journaux, les rapports et les paramètres sur cet espace\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": "SpaceDeveloper - Créer et gérer les applications et les services, et de consulter des journaux et des rapports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": "SpaceManager - Inviter et gérer les utilisateurs, et activer les fonctionnalités pour un espace donné\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": "Voir quotas admissibles avec 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NOMBRES_INSTANCES] [-k DISC] [-m MEMOIRE] [-n HÔTE] [-p PATH] [-s PILE] [-t TEMPS_EXPIRÉE]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " est déjà commencé", + "modified": false + }, + { + "id": " is already stopped", + "translation": " est déjà arrêté", + "modified": false + }, + { + "id": " is empty", + "translation": " est vide", + "modified": false + }, + { + "id": " not found", + "translation": " pas trouvé", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "Un outil de ligne de commande pour interagir avec Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "AVANCÉE", + "modified": false + }, + { + "id": "API endpoint", + "translation": "Endpoint de l'API", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (par exemple https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "Endpoint API:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API critère: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API critère: {{.ApiEndpoint}} (de version de l'API: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "Endpoint de l'API: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Ajouter une route URL pour une application", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Ajout itinéraire {{.URL}} de l'application {{.AppName}} en {{.OrgName}} / {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Supprimer également les itinéraires balisés", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "Un organisation doit être ciblée avant de cibler un espace", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "Nom de l'application est un champ obligatoire", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} n'existe pas.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} est un travailleur, saute la création de routes", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} est déjà lié à {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Annexer demande API diagnostic à un fichier journal", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Un quota à un org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Attribuer un rôle d'espace pour un utilisateur", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Attribuer un rôle org à un utilisateur", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Affectation rôle {{.Role}} de l'utilisateur {{.TargetUser}} en org {{.TargetOrg}} / espace {{.TargetSpace}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Affectation rôle {{.Role}} de l'utilisateur {{.TargetUser}} en org {{.TargetOrg}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Tentative de migration {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authentifier l'utilisateur non-interactive", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authentification en cours ...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "GÉRANT DE FACTURE", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "TEMPS DE CONSTRUIRE:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Liez une instance de service à une application", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Liaison entre {{.InstanceName}} et {{.AppName}} n'existait pas", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Liasant service {{.ServiceInstanceName}} à app {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Service liant {{.ServiceName}} à app {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Liaison {{.URL}} à {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} existe déjà", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} n'existe pas.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Quantité d'octets doit être un entier positif avec une unité de mesure comme M, MB, G ou B", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth NOM MOT_DE_PASSE\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [- async-timeout TIMEOUT_EN_MINUTES] [- tracer true | false | fishier] [-color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAINE", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MÉMOIRE] [-r ROUTES] [-s INSTANCE_DE_SERVICES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route ESPACE DOMAIN [-n NOM_DE_HÔTE]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE RÉGIME INSTANCE_DE_SERVICE\n\nExemple: CF_NAME create-service cleardb spark clear-db-mines\n\n:TIP:\n Utilisez 'CF_NAME create-user-provided-service' pour mettre disponibles aux applications cf, les services fournis par l'utilisateur", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER PSEUDO MOT_DE_PASSE URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAINE", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user PSEUDO MOT_DE_PASSE", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAINE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n NOM_DE_HÔTE] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "SERVICE_INSTANCE CF_NAME supprimer service [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service courtier SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAINE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user NOM_UTILISATEUR [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u UTILISATEUR] [-p MOT_DE_PASSE] [-o ORG] [-s ESPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n NOM_DE_HÔTE]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_FOURNISSEUR v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "SERVICE CF_NAME purge service offrande [-p FOURNISSEUR]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "NOM_CF quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename NOM_APP NOUVEAU_NOM_APP", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack NOM_BUILDPACK NOUVEAU_NOM_BUILDPACK", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NOUVEAU_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service INSTANCE_DE_SERVICE NOUVEAU_INSTANCE_DE_SERVICE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NOUVEAU_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISC] [-m MEMOIRE] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NOM VALEUR", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role NOM_DE_LUTILISATEUR ORG RÔLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-espace-role NOM_DE_LUTILISATEUR ORG ESPACE RÔLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space ESPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG ESPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s ESPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP INSTANCE_DE_SERVICE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n NOM_DE_HÔTE]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NOM", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role NOM_DE_LUTILISATEUR ORG RÔLE", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-espace-rôle NOM_DE_LUTILISATEUR ORG ESPACE RÔLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MÉMOIRE] [-n NOUVEAU_NOM] [-r ROUTES] [-s INSTANCE_DE_SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER PSEUDO MOT_DE_PASSE URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service INSTANCE_DE_SERVICE [-p PARAMÈTRES] [-l syslog-vindage-URL]'\n\nExemple:\n CF_NAME update-user-provided-service oracle-db-mines -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service mon-service-de-vindage -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERREUR EN CRÉANT LE FISHIER LOG {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Peux provisioner des instances de services pour le plan payé", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Vous ne pouvez pas lister des services de marché sans un espace ciblé", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Incapable de provisioner des instance de service à plan payé", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Vous ne pouvez pas spécifier le verrouillage et le déverrouillage des options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Vous ne pouvez pas spécifier à la fois {{.Enabled}} et {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Vous ne pouvez pas spécifier les bits de buildpack et verrouillage / déverrouillage.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Modifier ou afficher le nombre d'instance, la limite de l'espace disque, et la limite de la mémoire pour une application", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Changer mot de passe de l'utilisateur", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changement de mot de passe ...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "La commande n'est pas trouvé", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connecté, le dumping journaux récents pour application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connecté, résidus journaux pour application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Impossible de lier au service {{.ServiceName}}\nErreur: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Impossible de déterminer le répertoire de travail courant!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Impossible de trouver un domaine par défaut", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Impossible de trouver l'application nommée '{{.AppName}}' dans le manifeste", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Impossible de trouver un plan avec le nom {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Impossible de trouver le service {{.ServiceName}} de se lier à {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Impossible d'analyser la version dans: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Impossible de sérialiser l'informatio", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Impossible de sérialiser les mises à jour.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Impossible de cibler org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Impossible de créer le fichier temporaire pour le téléchargement", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Impossible d'ouvrir le fichier de buildpack", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Impossible d'écrire fichier zip", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Créer un buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Créer un domaine dans une org pour une utilisation ultérieure", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Créer un domaine qui peut être utilisé par tous les orgs (admin uniquement)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Créez un nouvel utilisateur", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Créer un itinéraire aléatoire pour cette application", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Créer un service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Créer un courtier de service", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Créer une instance de service", + "modified": false + }, + { + "id": "Create a space", + "translation": "Créer un espace", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Créer un itinéraire url dans un espace pour une utilisation ultérieure", + "modified": false + }, + { + "id": "Create an org", + "translation": "Créer un org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Création d'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Création buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Création domaine {{.DomainName}} pour org {{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Création org {{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Créez quota {{.QuotaName}} étant {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Création d'itinéraire {{.Hostname}} pour org {{.OrgName}} / {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Créez route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Création d'un service de courtier {{.Name}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Création d'un service {{.ServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Création domaine partagé {{.DomainName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Création d'un service fourni par l'utilisateur {{.ServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Création d'utilisateur {{.TargetUser}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Pouvoirs ont été rejetées, s'il vous plaît essayez de nouveau.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Mot de passe actuel", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Mot de passe actuel ne correspond", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Personnalisé buildpack par nom (par exemple mon-buildpack) ou GIT URL (par exemple https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Têtes personnalisés à inclure dans la demande, la fonction peut être spécifiée plusieurs fois", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINES", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Definir un nouvea quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Supprimer un buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Supprimer un domaine", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Suprimmer le quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Suppression d'une route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Supprimer un service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Supprimer un courtier de service", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Supprimer une instance de service", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Supprimer un domaine partagé", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Supprimer un espace", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Supprimer un utilisateur", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Supprimer tous les itinéraires orphelins (par exemple: ceux qui ne sont pas mappées à une application)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Supprimer une application", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Supprimer un org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Suprimassion annulé", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Suppression de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Suppression buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Suppression domaine {{.DomainName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Suppression org {{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Sumprimmer le quota {{.QuotaName}} étant {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Suppression d'itinéraire {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Suppression d'itinéraire {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Suppression courtier de {{.Name}} de {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Suppression service {{.ServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Suppression d'utilisateur {{.TargetUser}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Désactiver le buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Limites de disque (par exemple, 256M, 1024M, 1g)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Afficher la santé et l'état de l'application", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Ne pas coloriser sortie", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Ne pas mapper un itinéraire vers cette application", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Ne pas démarrer une application après avoir appuyé", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domaine (par exemple, example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump journaux récents lieu de résidus", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "VARIABLES D'ENVIRONNEMENT", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXEMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Activer la sortie de CF_TRACE pour toutes les demandes et les réponses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Activer le proxy HTTP pour les requêtes de l'API", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Activer ou désactiver la couleur", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Activer le buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "La variable env {{.VarName}} n'était pas réglée.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Erreur en créant la demande", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "demande Erreur de création:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Erreur de création de fichier temporaire: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Erreur de création de téléchargement", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Erreur en créant l'utilisateur {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Erreur de suppression buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Erreur domaine suppression {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Erreur dumping la demande\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Erreur trouver orgs disponibles\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Erreur trouver des espaces disponibles\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Erreur trouver la commande {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "domaine d'erreur trouver {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Erreur trouver du manifeste", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "constatation d'erreur org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "espace de constatation d'erreur {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Erreur dans l'exigence", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Erreur marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Erreur d'ouverture du fichier buildpack", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Erreur analysé JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Erreur en effectuent la demande", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Erreur de lecture du fichier manifeste:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Erreur d'analyse de la réponse", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Erreur buildpack renommer {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Erreur de résolution itinéraire:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Erreur mise à jour buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "L'application de téléchargement d'erreur.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Erreur ajout buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Erreur d'écriture de fichier tmp: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Erreur application zip", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Erreur: Aucun nom trouvé pour l'application", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Erreur: expiré en attendent un emploi asynchrone de terminer '{{.ErrURL}}'", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Erreur: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Exécute une requête, le type de contenu brut mis à application/json par défaut", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "L'application devrait être une liste de paires clé / valeur\nErreur s'est produite dans le fichier manifeste près:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Applications devrait être une liste", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "{{.Name}} d'un ensemble de clé =\u003e valeur attendue, mais c'était une {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "La valeur {{.PropertyName}} doit d'être un booléen.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "{{.PropertyName}} doit être une liste de string.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "{{.PropertyName}} doit être un nombre, mais c'était une {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "RATÉ", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Échec aller chercher buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Échec aller chercher domaines.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Échec aller chercher des événements.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Échec en cherchant org-users pour rôle {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Échec aller chercher orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Échec aller chercher routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Échec aller chercher les courtiers de services.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Échec en cherchant les space-users pour rôle {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Impossible de créer json de la demande de resource_match", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Impossible de marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Impossible de démarrer demande oauth", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Forcer la suppression sans confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Forcer la migration sans confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force de redémarrage de l'application sans invite", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "MISE EN ROUTE", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "OPTIONS GLOBALES", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Raporter tous les services de marché...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtenir applications dans org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Obtenir buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Obtenir domaines dans org {{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtenir variables d'environnement pour l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Obtenir des événements de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtenir les fichiers de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Obtenir infos org {{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Obtenir orgs comme {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Trouver l'information quota pour {{.QuotaName}} ètant {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Obtenir les quotas étant {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Obtenir des itinéraires que {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Obtenir courtiers de services comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Rapporter des services de marché dans org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Rapporter des services en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtenir des stacks dans org {{.OrganizationName}} / {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Obtenir utilisateurs dans org {{.TargetOrg}} / espace {{.TargetSpace}} comme {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Obtenir utilisateurs dans org {{.TargetOrg}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "données HTTP pour inclure dans le corps de la demande", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "méthode HTTP (GET, POST, PUT, DELETE, etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Nom d'hôte", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Nom d'hôte (par exemple, mon-domaine)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignorer fichier manifeste", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "En-têtes de réponse dans la sortie", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Utilisation Incorrecte", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Utilisation incorrecte.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Utilisation incorrecte. Drapeaux de ligne de commande (sauf -f) ne peuvent pas être appliquées en poussant plusieurs applications à partir d'un fichier manifeste.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Nombre d'arguments incorrect", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Utilisation incorrecte", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Serveur JSON réponse invalide", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Rôle invalide {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid Cert SSL pour {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Réponse asynchrone du serveur est invalide", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Jeton auth invalide: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Quota de disque non valide: {{.DiskQuota}} {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Quota de disque non valide: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Instance non valide compter: {{.InstanceCount}}\nCompte de l'instance doit être un entier positif", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Instance non valide compter: {{.InstancesCount}}\nCompte de l'instance doit être un entier positif", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Manifeste non valide. Prévue une dictionaire", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Limite de mémoire non valide: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Limite de mémoire invalid: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Limite de mémoire non valide: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Position non valide. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid délai param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Utilisation non valide", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Valeur inattendue pour {{.PropertyName}} :\n {{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON est invalide: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "Liste de toutes les applications dans l'espace ciblé", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "Liste de toutes les buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "Liste de toutes les orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "Lister toutes les routes dans l'espace actuel", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "Liste de toutes les instances de service dans l'espace ciblé", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "liste de tous les espaces dans un org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Liste de toutes les stacks (un stacl est un système de fichiers pré-construit, y compris un système d'exploitation, qui peuvent exécuter des applications)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "Liste tous les utilisateurs dans le org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "Liste des offres disponibles sur le marché", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "domaines de la liste dans la org ciblé", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "Liste service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "Liste des courtiers de service", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Verrouiller le buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Connexion utilisateur", + "modified": false + }, + { + "id": "Log user out", + "translation": "Déconnexion utilisateur", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Déconnexion ...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint manquant de fichier de configuration", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Faire un instance de service fourni par l'utilisateur à la disposition des applications cf", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Plan du domaine racine de l'application", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Maximum temps d'attente, pour instance d'application de démarrage, en quelques minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Maximum temps d'attente pour buildpack mise en scène, en quelques minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Lancer attente en secondes", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Limitations de la mémoire (par exemple, 256M, 1024M, 1g)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrez les instances de service d'un plan de service à un autre", + "modified": false + }, + { + "id": "NAME:", + "translation": "NOM:", + "modified": false + }, + { + "id": "Name", + "translation": "Nom", + "modified": false + }, + { + "id": "New Password", + "translation": "Nouveau mot de passe", + "modified": false + }, + { + "id": "New name", + "translation": "Nouveau nom", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "Aucun API endpoint ciblé. Utiliser '{{.LoginTip}}' ou '{{.APITip}}' pour ciblé un endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "Pas api endpoint ensemble. Utilisez '{{.Name}}' pour définir un point de terminaison", + "modified": false + }, + { + "id": "No apps found", + "translation": "Aucune application trouvée", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "Pas buildpacks trouvés", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "Pas domaines trouvés", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "Aucun événement pour l'application {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "Pas de marques spécifiés. Pas de modifications ont été apportées.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "Aucunne org ou espace ciblée, utiliser la '{{.Command}}' pour ciblé l'org et l'espace", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "Aucunne org ou espace ciblée, utiliser '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "Aucunne org ciblée, utiliser '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "Aucunne org ciblée, utiliser la '{{.Command}}' pour ciblé l'org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "Orgs pas trouvés", + "modified": false + }, + { + "id": "No routes found", + "translation": "Pas de routes trouvés", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "Pas de courtiers de services trouvés", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "Aucune offre de services trouvés", + "modified": false + }, + { + "id": "No services found", + "translation": "Pas de services trouvés", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "Aucun espace ciblé, utiliser '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "Aucun espace ciblé, utiliser la '{{.Command}}' pour ciblé l'espace", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "Pas de variables d'environnement fournies par le système", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "Variables d'environnement utilisateur non définis", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "Aucun de vos dossiers de l'application ont changé. Rien ne sera téléchargé.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Pas connecté. Utiliser '{{.CFLoginCommand}}' pour vous connecter.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Nombre d'instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITEUR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG GÉRANT", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} existe déjà", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} n'existe pas.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organisation", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Remplacer par défaut config chemin", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Plan de service payè", + "modified": false + }, + { + "id": "Parameters", + "translation": "Paramètres", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Mot de passe", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Vérification de mot de passe ne correspond pas", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Chemin de répertoire de l'application ou un fichier zip", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Chemin d'accès au répertoire ou un fichier zip", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Chemin du fishier manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan '{{.ServicePlanName}}' ne peut être trouvé", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan de {{.ServicePlanName}} a aucun cas de services à migrer", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Régime: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Choississez allow ou disallow. Les deux options ne sons pas permise dans la même commande.", + "modified": false + }, + { + "id": "Please don't", + "translation": "S'il vous plaît ne pas", + "modified": false + }, + { + "id": "Please log in again", + "translation": "S'il vous plaît, connecter à nouveau", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "API d'impression de diagnostic sur stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Imprimer une liste de fichiers dans un répertoire ou le contenu d'un fichier spécifique", + "modified": false + }, + { + "id": "Print the version", + "translation": "Affiche la version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "La valeur '{{.PropertyName}}' trouvé dans le manifeste. Cette fonctionnalité n'est plus prise en charge. S'il vous plaît enlever et essayer à nouveau.", + "modified": false + }, + { + "id": "Provider", + "translation": "Fournisseur", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purgé le service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Appuyez une nouvelle application ou les modifications à synchroniser à une application existante", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Appuyez une seule application (avec ou sans un manifeste):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "La définition de quota {{.QuotaName}} existe déjà", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} n'éxiste plus", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "DEMANDE:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RÉPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "RÔLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Vraiment supprimer des routes orphelins?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Voulez-vous vraiment effacer cette {{.ModelType}} {{.ModelName}} et tout associé avec?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Voulez-vous vraiment effacer cette {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Vraiment migrer {{.ServiceInstanceDescription}} de régime {{.OldServicePlanName}} à {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Vraiment purger offre de service {{.ServiceName}} de Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Reçu certificat SSL invalide de ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Effacer de façon récursive un service et des objets enfants base de données Cloud Foundry sans faire des demandes à un courtier de service", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Retirer un espace de rôle d'un utilisateur", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Suppression d'un itinéraire de l'URL d'une application", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Supprimer une variable d'environement", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Retirer un org de rôle d'un utilisateur", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Retrait variable d'environnement {{.VarName}} de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Retrait rôle {{.Role}} de l'utilisateur {{.TargetUser}} en org {{.TargetOrg}} / espace {{.TargetSpace}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Retrait rôle {{.Role}} de l'utilisateur {{.TargetUser}} en org {{.TargetOrg}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Retrait itinéraire {{.URL}} de l'application {{.AppName}} en {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Retirer la route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Renommer un buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Renommer un courtier de service", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Renommer une instance de service", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Renommer un espace", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Renommer une application", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Renommer un org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renommer application {{.AppName}} comme {{.NewName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renommer buildpack {{.OldBuildpackName}} pour {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renommer org {{.OrgName}} pour {{.NewName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renommer courtier de service '{{.OldName}}' pour {{.NewName}} comme {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renommer service {{.ServiceName}} pour {{.NewServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}} ...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Relancer une application", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Relancement application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Redémarrer une application", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} existe déjà", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Règlements", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "ADMIN DE SERVICE", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "ESPACE AUDITEUR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "ESPACE DÉVELOPPEUR", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE GÉRANT", + "modified": false + }, + { + "id": "SPACES", + "translation": "ESPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Mise à l'échelle de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Sélectionnez un espace (ou appuyez sur Entrée pour sauter):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Sélectionner un org (ou appuyez sur Entrée pour sauter):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Erreur du serveur, code d'état: {{.ErrStatusCode}}, code erreur: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} n'existe pas.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Instance de service n'est pas fourni par l'utilisateur", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Instance de service: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Offre de service n'existe pas\nTIP: Si vous essayez de purger un offre service v1, vous devez définir l'option -p.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service de {{.ServiceName}} n'existe pas.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Définir une variable d'environnement pour une application", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Ensemble ou vue URL cible api", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Définir ou afficher l'org ou de l'espace ciblé", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Réglage api endpoint de {{.Endpoint}} ...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Réglage variable d'environnement '{{.VarName}}' à '{{.VarValue}}' pour l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Réglage quota {{.QuotaName}} de l'{{.OrgName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Voir toutes les variables d'environnement pour une application", + "modified": false + }, + { + "id": "Show help", + "translation": "Afficher ce message", + "modified": false + }, + { + "id": "Show org info", + "translation": "Afficher les informations org", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Afficher les utilisateurs de l'org par rôle", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Montrez l'information de quota", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Afficher les événements d'applications récentes", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Afficher infos d'instances de service", + "modified": false + }, + { + "id": "Show space info", + "translation": "Indiquer info d'un espace", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Afficher les utilisateurs de l'espace par rôle", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Affichage actuel de l'échelle de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Affichage de la santé et l'état de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Espace", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Espace:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Empilez à utiliser (une pile est un système de fichiers pré-construit, y compris un système d'exploitation, qui peuvent exécuter des applications)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Lancer une application", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Lancer l'application délai\n\nTIP: utiliser '{{.Command}}' pour plus d'informations", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Lancer échoué\n\nTIP: utiliser '{{.Command}}' pour plus d'informations", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "À partir de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Commande de démarrage, utiliser la valeur null pour réinitialiser par défaut la commande de démarrage", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Arrêter une application", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Arrêt de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Vidange URL", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "Fournies par le système:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "ASTUCE: Pour faire ces modifications prennent effet, utilizer '{{.CFUnbindCommand}}' pour dissocier le service, '{{.CFBindComand}}' pour relier, puis '{{.CFRestageCommand}}' pour mettre à jour l'application avec les nouvelles variables d'environnement", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "ASTUCE: Utilisez '{{.ApiCommand}}' pour continuer avec une API critère insécurité", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Utilisez '{{.CFCommand}}' pour vous assurer que vos changements des variables d'environnement prennent effet", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Utilisez '{{.Command}}' pour vous assurer que vos variables d'environnement modifiées prennent effet", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: utiliser '{{.CfUpdateBuildpackCommand}}' Pour mettre à jour cette buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail ou montrer récents journaux pour une application", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Org ciblée {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Espace ciblé {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Buildpack position parmi d'autres buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "La route {{.URL}} est deja en utilisation.\nTIP: Changer le nom d'hôte avec -n HOSTNAME ou utiliser --random-route pour générer une nouvelle route et appuyez à nouveau.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "Il n'y a pas des instances qui fonctionne pour cette application.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "Il ya trop d'options à afficher, s'il vous plaît entrez le nom.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "Ce domaine est partagé par toutes les orgs.\nSuppression il va supprimer tous les itinéraires associés, et fera n'importe quelle application à ce domaine inaccessible.\nEtes-vous sûr de vouloir effacer le domaine {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "Cela entraînera l'application à redémarrer. Etes-vous sûr que vous voulez écheller {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Délai d'attente pour les demandes HTTP asynchrone", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memoire", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Quantité totale de mémoire (e.g. 1024Mo, 1Go, 10Go)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Quantité totale de routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Nombres total d'instance de services", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace des requêtes HTTP", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint manquant de fichier de configuration", + "modified": false + }, + { + "id": "USAGE:", + "translation": "UTILISATION:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "UTILISATEUR ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "UTILISATEURS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Impossible d'accéder à l'espace {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Impossible de vous authentifier.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Impossible de supprimer, route '{{.URL}}' n'existe pas.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Délier une instance de service d'une application", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Libération App {{.AppName}} du service {{.ServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Déverrouillez le buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Mettre à jour un buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Mettre à jour un service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Mettre à jour un courtier de service", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Reactualize un quota éxistant", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Mettre à jour les noms et valeurs d'un instance de service fournie par l'utilisateur", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Mise à jour de l'application {{.AppName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Mise à jour buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Réactualisation quota {{.QuotaName}} étant {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Mise à jour courtier de {{.Name}} comme {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Mise à jour de l'utilisateur fournie service {{.ServiceName}} en org {{.OrgName}} / espace {{.SpaceName}} comme {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Téléchargement de fichiers d'applications à partir de: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "L'ajout buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "L'ajout {{.AppName}} ...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Téléchargement de {{.ZipFileBytes}}, {{.FileCount}} fichiers", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Utilisez '{{.Name}}' pour afficher ou définir votre organisation cible et l'espace", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Utilisez un mot de passe unique pour se connecter", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "Utilisateur {{.TargetUser}} n'existe pas.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "Fournies par l'utilisateur:", + "modified": false + }, + { + "id": "User:", + "translation": "Utilisateur:", + "modified": false + }, + { + "id": "Username", + "translation": "Nom d'utilisateur", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "En utilisant le fichier manifeste {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Utilisant la route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Utilisant de pile {{.StackName}} ...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Vérifiez Mot de passe", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "AVERTISSEMENT:\n Fournir votre mot de passe comme une option de ligne de commande est fortement déconseillée.\n Votre mot de passe peut être visible par les autres et peut être enregistré dans votre historique du shell\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "ATTENTION: Cette opération suppose que le courtier de service responsable de cette offre de service n'est plus disponible, et toutes les instances de services ont été supprimés, laissant les enregistrements orphelins dans la base de données Cloud Foundry. Toutes les connaissances du service seront supprimé de Cloud Foundry, y compris les instances de service et les liaisons de service. Aucune tentative ne sera faite pour communiquer avec le courtier de service; exécution de cette commande sans détruire le courtier de service fera instances de service orphelins. Après l'exécution de cette commande, vous pouvez exécuter delete-service-auth-token ou delete-service-auth-broker de terminer le nettoyage.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "ATTENTION: Cette opération est interne à Cloud Foundry; courtiers de services ne seront pas contactés et des ressources pour les instances de service ne seront pas modifiés. Le cas d'utilisation principal de cette opération est de remplacer un courtier de service qui implémente l'API de Service Broker v1 avec un courtier qui implémente l'API v2 par remappage instances de service de regime v1 à v2. Nous recommandons l'élaboration du plan de v1 privé ou arrêter le courtier de v1 à prévenir les cas supplémentaires d'être créé. Une fois les instances de service ont été migrés, les services de v1 et plans peuvent être retirés de Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Attention: l'insécurité API de point de terminaison HTTP détectée: sécurisés paramètres de l'API https sont recommandés\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Avertissement: journaux en erreurs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive ne contient pas de buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/DONNÉES DE FORMULAIRE CONTENUE CACHÉ]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[DONNÉES PRIVÉE CACHÉ]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[variables d'environnement]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[options globales] commande [arguments...] [options de commande]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "acteur", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "permis", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app percuter", + "modified": false + }, + { + "id": "apps", + "translation": "applications", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "applications liées", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "en panne", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "refusé", + "modified": false + }, + { + "id": "disk", + "translation": "disque", + "modified": false + }, + { + "id": "disk:", + "translation": "disque:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domaine", + "modified": false + }, + { + "id": "domains:", + "translation": "domaines:", + "modified": true + }, + { + "id": "down", + "translation": "inexistante", + "modified": false + }, + { + "id": "enabled", + "translation": "permis", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "variable d'environnment '{{.PropertyName}}' ne doit pas être null", + "modified": false + }, + { + "id": "event", + "translation": "événement", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "la console écho d'entrée de mot de passe n'a pas pu être déconnectée:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "nom de fichier", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "hôte", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "utilisation incorrecte", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, raison: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "chemin hérité non valide dans le manifeste", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "valeur non valide pour variable d'environnement CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "valeur non valide pour variable d'environnement CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "fermé", + "modified": false + }, + { + "id": "memory", + "translation": "mémoire", + "modified": false + }, + { + "id": "memory:", + "translation": "mémoire:", + "modified": false + }, + { + "id": "name", + "translation": "nom", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "pas valable pour l'hôte demandé", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organisation", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "en propriété", + "modified": false + }, + { + "id": "paid service plans", + "translation": "plans de service payé", + "modified": false + }, + { + "id": "plan", + "translation": "régime", + "modified": false + }, + { + "id": "plans", + "translation": "régimes", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "fournisseur", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": false + }, + { + "id": "requested state", + "translation": "État intentionné", + "modified": false + }, + { + "id": "requested state:", + "translation": "État intentionné:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "fonctionne", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "instance de service", + "modified": false + }, + { + "id": "service instances", + "translation": "instance de services", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service courtier", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "commun", + "modified": false + }, + { + "id": "since", + "translation": "depuis", + "modified": false + }, + { + "id": "space", + "translation": "espace", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "espaces:", + "modified": false + }, + { + "id": "starting", + "translation": "commence", + "modified": false + }, + { + "id": "state", + "translation": "état", + "modified": false + }, + { + "id": "status", + "translation": "statut", + "modified": false + }, + { + "id": "stopped", + "translation": "arrêté", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "arrêté après une redirection", + "modified": false + }, + { + "id": "time", + "translation": "temps", + "modified": false + }, + { + "id": "total memory limit", + "translation": "limite de memoire", + "modified": true + }, + { + "id": "unknown authority", + "translation": "autorité inconnue", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "URL", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "utilisation:", + "modified": false + }, + { + "id": "user", + "translation": "utilisateur", + "modified": false + }, + { + "id": "user-provided", + "translation": "fourni par l'utilisateur", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "écrire des valeurs par défaut pour la configuration", + "modified": false + }, + { + "id": "yes", + "translation": "oui", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (Version API: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMANDE]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migré.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} de {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} bas", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Utilisez '{{.CFServicesCommand}}' pour afficher tous les services dans ce org et de l'espace.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: utilisation '{{.Command}}' pour plus d'informations", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} en défaut", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} de {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} existe déjà", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} doit être une string ou une valeur null", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} doit être une valeur de string", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} ne doit pas être null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M de limite de mémoire, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, services payants {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} de {{.TotalCount}} d'instances en cours", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} départ", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} d'instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/it_IT.all.json b/cf/i18n/resources/it_IT.all.json new file mode 100644 index 00000000000..5aaad5a6868 --- /dev/null +++ b/cf/i18n/resources/it_IT.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nApp started\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Use '{{.Command}}' to target new org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Read-only access to org info and reports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Path should be a zip file, a url to a zip file, or a local directory. Position is an integer, sets priority, and is sorted from lowest to highest.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - View logs, reports, and settings on this space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " View allowable quotas with 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " is already started", + "modified": false + }, + { + "id": " is already stopped", + "translation": " is already stopped", + "modified": false + }, + { + "id": " is empty", + "translation": " is empty", + "modified": false + }, + { + "id": " not found", + "translation": " not found", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "A command line tool to interact with Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API endpoint:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Also delete any mapped routes", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "An org must be targeted before targeting a space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "App name is a required field", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} does not exist.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} is a worker, skipping route creation", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Append API request diagnostics to a log file", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assign a quota to an org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assign a space role to a user", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assign an org role to a user", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authenticate user non-interactively", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authenticating...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "BUILD TIME:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Binding {{.URL}} to {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} already exists", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} does not exist.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be a positive integer with a unit of measurement like M, MB, G, or GB", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USERNAME PASSWORD\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USERNAME PASSWORD", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USERNAME [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NAME VALUE", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NAME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Cannot specify both lock and unlock options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Cannot specify buildpack bits and lock/unlock.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Change or view the instance count, disk space limit, and memory limit for an app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changing password...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Command not found", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Could not determine the current working directory!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Could not find a default domain", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Could not find app named '{{.AppName}}' in manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Could not parse version number: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Could not serialize information", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Could not serialize updates.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Could not target org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Couldn't create temp file for upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Couldn't open buildpack file", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Couldn't write zip file", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Create a buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Create a new user", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Create a random route for this app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "Create an org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creating org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creating route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credentials were rejected, please try again.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Current Password", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Current password did not match", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Custom headers to include in the request, flag can be specified multiple times", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Delete a buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Delete a user", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Delete an app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Delete an org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Delete cancelled", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Deleting buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Deleting org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Disable the buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Disk limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Display health and status for app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Do not colorize output", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do not map a route to this app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Do not start an app after pushing", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump recent logs instead of tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXAMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Enable CF_TRACE output for all requests and responses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Enable HTTP proxying for API requests", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Enable or disable color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Enable the buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Env variable {{.VarName}} was not set.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error building request", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creating request:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error creating tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creating upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error dumping request\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error finding available orgs\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error finding available spaces\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error finding command {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error finding manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error finding org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error finding space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error in requirement", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error opening buildpack file", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error parsing JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error performing request", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error reading manifest file:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error reading response", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error updating buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error uploading application.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error writing to tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error zipping application", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: No name found for app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executes a raw request, content-type set to application/json by default", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Expected applications to be a list", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Expected {{.PropertyName}} to be a boolean.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Expected {{.PropertyName}} to be a list of strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FAILED", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Failed fetching buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Failed fetching events.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Failed fetching orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Failed to create json for resource_match request", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Failed to marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Failed to start oauth request", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Force deletion without confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Force migration without confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force restart of app without prompt", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "GLOBAL OPTIONS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Getting all services from marketplace...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Getting buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Getting info for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Getting orgs as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "HTTP data to include in the request body", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignore manifest file", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Include response headers in the output", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Incorrect Usage.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Incorrect number of arguments", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Incorrect usage", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Invalid JSON response from server", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Invalid Role {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Invalid async response from server", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Invalid auth token: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Invalid manifest. Expected a map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Invalid usage", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Unexpected value for {{.PropertyName}} :\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "List all apps in the target space", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "List all buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "List all orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "List all users in the org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Lock the buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Log user in", + "modified": false + }, + { + "id": "Log user out", + "translation": "Log user out", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Logging out...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint missing from config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Make a user-provided service instance available to cf apps", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Map the root domain to this app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Max wait time for app instance startup, in minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Max wait time for buildpack staging, in minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Start timeout in seconds", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Memory limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrate service instances from one service plan to another", + "modified": false + }, + { + "id": "NAME:", + "translation": "NAME:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "New Password", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "No apps found", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No events for app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No org and space targeted, use '{{.Command}}' to target an org and space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No org or space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No org targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No org targeted, use '{{.Command}}' to target an org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No orgs found", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No service offerings found", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "No space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "No space targeted, use '{{.Command}}' to target a space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "No system-provided env variables have been set", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "No user-defined env variables have been set", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "None of your application files have changed. Nothing will be uploaded.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Number of instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITOR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG MANAGER", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} already exists", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} does not exist.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Override path to default config directory", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Password", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Password verification does not match", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Path of app directory or zip file", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Path to directory or zip file", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Path to manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan {{.ServicePlanName}} cannot be found", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Please don't", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Please log in again", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Print API request diagnostics to stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Print out a list of files in a directory or the contents of a specific file", + "modified": false + }, + { + "id": "Print the version", + "translation": "Print the version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Push a new app or sync changes to an existing app", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Push a single app (with or without a manifest):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "REQUEST:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Received invalid SSL certificate from ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remove a space role from a user", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remove an env variable", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remove an org role from a user", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removing route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Rename a buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Rename an app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Rename an org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Restage an app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Restart an app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "SPACE AUDITOR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "SPACE DEVELOPER", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE MANAGER", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Select a space (or press enter to skip):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Select an org (or press enter to skip):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service {{.ServiceName}} does not exist.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Set an env variable for an app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Set or view target api url", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Set or view the targeted org or space", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Setting api endpoint to {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Show all env variables for an app", + "modified": false + }, + { + "id": "Show help", + "translation": "Show help", + "modified": false + }, + { + "id": "Show org info", + "translation": "Show org info", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Show org users by role", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Show recent app events", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Show space users by role", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Start an app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Startup command, set to null to reset to default start command", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Stop an app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Drain Url", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "System-Provided:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail or show recent logs for an app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Targeted org {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Targeted space {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Buildpack position among other buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "There are no running instances of this app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "There are too many options to display, please type in the name.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Timeout for async HTTP requests", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memory", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace HTTP requests", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint missing from config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USAGE:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Unable to authenticate.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Unlock the buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Update a buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Updating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Uploading app files from: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Uploading buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Uploading {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Use '{{.Name}}' to view or set your target org and space", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Use a one-time password to login", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "User {{.TargetUser}} does not exist.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "User-Provided:", + "modified": false + }, + { + "id": "User:", + "translation": "User:", + "modified": false + }, + { + "id": "Username", + "translation": "Username", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Using manifest file {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Using route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Using stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verify Password", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Warning: error tailing logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive does not contain a buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[environment variables]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[global options] command [arguments...] [command options]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "allowed", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app crashed", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "bound apps", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "crashing", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "disallowed", + "modified": false + }, + { + "id": "disk", + "translation": "disk", + "modified": false + }, + { + "id": "disk:", + "translation": "disk:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "domains:", + "modified": false + }, + { + "id": "down", + "translation": "down", + "modified": false + }, + { + "id": "enabled", + "translation": "enabled", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "env var '{{.PropertyName}}' should not be null", + "modified": false + }, + { + "id": "event", + "translation": "event", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "incorrect usage", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "invalid inherit path in manifest", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "locked", + "modified": false + }, + { + "id": "memory", + "translation": "memory", + "modified": false + }, + { + "id": "memory:", + "translation": "memory:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "not valid for the requested host", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organization", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "plans", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": false + }, + { + "id": "requested state", + "translation": "requested state", + "modified": false + }, + { + "id": "requested state:", + "translation": "requested state:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "running", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "since", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": true + }, + { + "id": "starting", + "translation": "starting", + "modified": false + }, + { + "id": "state", + "translation": "state", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "stopped", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "stopped after 1 redirect", + "modified": false + }, + { + "id": "time", + "translation": "time", + "modified": false + }, + { + "id": "total memory limit", + "translation": "memory limit", + "modified": true + }, + { + "id": "unknown authority", + "translation": "unknown authority", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "usage:", + "modified": false + }, + { + "id": "user", + "translation": "user", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "write default values to the config", + "modified": false + }, + { + "id": "yes", + "translation": "yes", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} of {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} down", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} failing", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} of {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} already exists", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} must be a string or null value", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} must be a string value", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} should not be null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} of {{.TotalCount}} instances running", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} starting", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/ja_JA.all.json b/cf/i18n/resources/ja_JA.all.json new file mode 100644 index 00000000000..5aaad5a6868 --- /dev/null +++ b/cf/i18n/resources/ja_JA.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nApp started\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Use '{{.Command}}' to target new org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Read-only access to org info and reports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Path should be a zip file, a url to a zip file, or a local directory. Position is an integer, sets priority, and is sorted from lowest to highest.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - View logs, reports, and settings on this space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " View allowable quotas with 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " is already started", + "modified": false + }, + { + "id": " is already stopped", + "translation": " is already stopped", + "modified": false + }, + { + "id": " is empty", + "translation": " is empty", + "modified": false + }, + { + "id": " not found", + "translation": " not found", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "A command line tool to interact with Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API endpoint:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Also delete any mapped routes", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "An org must be targeted before targeting a space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "App name is a required field", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} does not exist.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} is a worker, skipping route creation", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Append API request diagnostics to a log file", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assign a quota to an org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assign a space role to a user", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assign an org role to a user", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authenticate user non-interactively", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authenticating...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "BUILD TIME:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Binding {{.URL}} to {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} already exists", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} does not exist.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be a positive integer with a unit of measurement like M, MB, G, or GB", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USERNAME PASSWORD\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USERNAME PASSWORD", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USERNAME [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NAME VALUE", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NAME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Cannot specify both lock and unlock options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Cannot specify buildpack bits and lock/unlock.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Change or view the instance count, disk space limit, and memory limit for an app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changing password...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Command not found", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Could not determine the current working directory!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Could not find a default domain", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Could not find app named '{{.AppName}}' in manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Could not parse version number: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Could not serialize information", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Could not serialize updates.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Could not target org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Couldn't create temp file for upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Couldn't open buildpack file", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Couldn't write zip file", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Create a buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Create a new user", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Create a random route for this app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "Create an org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creating org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creating route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credentials were rejected, please try again.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Current Password", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Current password did not match", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Custom headers to include in the request, flag can be specified multiple times", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Delete a buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Delete a user", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Delete an app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Delete an org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Delete cancelled", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Deleting buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Deleting org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Disable the buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Disk limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Display health and status for app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Do not colorize output", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do not map a route to this app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Do not start an app after pushing", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump recent logs instead of tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXAMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Enable CF_TRACE output for all requests and responses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Enable HTTP proxying for API requests", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Enable or disable color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Enable the buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Env variable {{.VarName}} was not set.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error building request", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creating request:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error creating tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creating upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error dumping request\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error finding available orgs\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error finding available spaces\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error finding command {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error finding manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error finding org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error finding space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error in requirement", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error opening buildpack file", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error parsing JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error performing request", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error reading manifest file:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error reading response", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error updating buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error uploading application.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error writing to tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error zipping application", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: No name found for app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executes a raw request, content-type set to application/json by default", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Expected applications to be a list", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Expected {{.PropertyName}} to be a boolean.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Expected {{.PropertyName}} to be a list of strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FAILED", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Failed fetching buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Failed fetching events.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Failed fetching orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Failed to create json for resource_match request", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Failed to marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Failed to start oauth request", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Force deletion without confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Force migration without confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force restart of app without prompt", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "GLOBAL OPTIONS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Getting all services from marketplace...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Getting buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Getting info for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Getting orgs as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "HTTP data to include in the request body", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignore manifest file", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Include response headers in the output", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Incorrect Usage.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Incorrect number of arguments", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Incorrect usage", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Invalid JSON response from server", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Invalid Role {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Invalid async response from server", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Invalid auth token: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Invalid manifest. Expected a map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Invalid usage", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Unexpected value for {{.PropertyName}} :\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "List all apps in the target space", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "List all buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "List all orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "List all users in the org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Lock the buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Log user in", + "modified": false + }, + { + "id": "Log user out", + "translation": "Log user out", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Logging out...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint missing from config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Make a user-provided service instance available to cf apps", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Map the root domain to this app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Max wait time for app instance startup, in minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Max wait time for buildpack staging, in minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Start timeout in seconds", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Memory limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrate service instances from one service plan to another", + "modified": false + }, + { + "id": "NAME:", + "translation": "NAME:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "New Password", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "No apps found", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No events for app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No org and space targeted, use '{{.Command}}' to target an org and space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No org or space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No org targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No org targeted, use '{{.Command}}' to target an org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No orgs found", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No service offerings found", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "No space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "No space targeted, use '{{.Command}}' to target a space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "No system-provided env variables have been set", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "No user-defined env variables have been set", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "None of your application files have changed. Nothing will be uploaded.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Number of instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITOR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG MANAGER", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} already exists", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} does not exist.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Override path to default config directory", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Password", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Password verification does not match", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Path of app directory or zip file", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Path to directory or zip file", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Path to manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan {{.ServicePlanName}} cannot be found", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Please don't", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Please log in again", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Print API request diagnostics to stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Print out a list of files in a directory or the contents of a specific file", + "modified": false + }, + { + "id": "Print the version", + "translation": "Print the version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Push a new app or sync changes to an existing app", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Push a single app (with or without a manifest):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "REQUEST:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Received invalid SSL certificate from ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remove a space role from a user", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remove an env variable", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remove an org role from a user", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removing route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Rename a buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Rename an app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Rename an org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Restage an app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Restart an app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "SPACE AUDITOR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "SPACE DEVELOPER", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE MANAGER", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Select a space (or press enter to skip):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Select an org (or press enter to skip):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service {{.ServiceName}} does not exist.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Set an env variable for an app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Set or view target api url", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Set or view the targeted org or space", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Setting api endpoint to {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Show all env variables for an app", + "modified": false + }, + { + "id": "Show help", + "translation": "Show help", + "modified": false + }, + { + "id": "Show org info", + "translation": "Show org info", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Show org users by role", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Show recent app events", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Show space users by role", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Start an app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Startup command, set to null to reset to default start command", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Stop an app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Drain Url", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "System-Provided:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail or show recent logs for an app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Targeted org {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Targeted space {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Buildpack position among other buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "There are no running instances of this app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "There are too many options to display, please type in the name.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Timeout for async HTTP requests", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memory", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace HTTP requests", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint missing from config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USAGE:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Unable to authenticate.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Unlock the buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Update a buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Updating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Uploading app files from: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Uploading buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Uploading {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Use '{{.Name}}' to view or set your target org and space", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Use a one-time password to login", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "User {{.TargetUser}} does not exist.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "User-Provided:", + "modified": false + }, + { + "id": "User:", + "translation": "User:", + "modified": false + }, + { + "id": "Username", + "translation": "Username", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Using manifest file {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Using route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Using stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verify Password", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Warning: error tailing logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive does not contain a buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[environment variables]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[global options] command [arguments...] [command options]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "allowed", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app crashed", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "bound apps", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "crashing", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "disallowed", + "modified": false + }, + { + "id": "disk", + "translation": "disk", + "modified": false + }, + { + "id": "disk:", + "translation": "disk:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "domains:", + "modified": false + }, + { + "id": "down", + "translation": "down", + "modified": false + }, + { + "id": "enabled", + "translation": "enabled", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "env var '{{.PropertyName}}' should not be null", + "modified": false + }, + { + "id": "event", + "translation": "event", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "incorrect usage", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "invalid inherit path in manifest", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "locked", + "modified": false + }, + { + "id": "memory", + "translation": "memory", + "modified": false + }, + { + "id": "memory:", + "translation": "memory:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "not valid for the requested host", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organization", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "plans", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": false + }, + { + "id": "requested state", + "translation": "requested state", + "modified": false + }, + { + "id": "requested state:", + "translation": "requested state:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "running", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "since", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": true + }, + { + "id": "starting", + "translation": "starting", + "modified": false + }, + { + "id": "state", + "translation": "state", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "stopped", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "stopped after 1 redirect", + "modified": false + }, + { + "id": "time", + "translation": "time", + "modified": false + }, + { + "id": "total memory limit", + "translation": "memory limit", + "modified": true + }, + { + "id": "unknown authority", + "translation": "unknown authority", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "usage:", + "modified": false + }, + { + "id": "user", + "translation": "user", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "write default values to the config", + "modified": false + }, + { + "id": "yes", + "translation": "yes", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} of {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} down", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} failing", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} of {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} already exists", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} must be a string or null value", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} must be a string value", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} should not be null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} of {{.TotalCount}} instances running", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} starting", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/pt_BR.all.json b/cf/i18n/resources/pt_BR.all.json new file mode 100644 index 00000000000..5a2e32859a6 --- /dev/null +++ b/cf/i18n/resources/pt_BR.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nDICA:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nAplicativo iniciado\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nDICA: Assinale funções com '{{.CurrentUser}} set-org-role' e '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nDICA: Utilize '{{.CFTargetCommand}}' para definir espaço alvo", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nDICA: Use '{{.Command}}' para modificar a organização alvo", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nDICA: utilize 'cf login -a API --skip-ssl-validation' ou 'cf api API --skip-ssl-validation' para suprimir este erro", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Criar e gerenciar a conta de faturamento e informações de pagamento\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"senha\\\"\" (escapar aspas se usado na senha)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"minha senha\" (usar aspas para senhas com espaço)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omita usuário e senha para efetuar o login interativamente -- CF_NAME irá solicitá-los)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME irá fornecer uma URL onde você poderá obter uma senha de uso único)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"senha\\\"\" (escapar aspas se usado na senha)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"minha senha\" (usar aspas para senhas com espaço)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p 53nh4 (especificar usuário e senha como argumentos)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMANDO] [-d DOMÍNIO] [-f CAMINHO-DO-MANIFESTO]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f CAMINHO-DO-MANIFESTO]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Acesso somente leitura à informações e relatórios da Organização\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Convidar e gerenciar usuários, selecionar e alterar planos e definir limites de gastos\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Caminho deverá ser um arquivo zip, uma url para um arquivo zip, ou um diretório local. Posição é um número inteiro que define prioridades e é classificada do menor para o maior valor.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Envie múltiplos aplicativos com um manifesto:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - Inspecionar logs, relatórios e configurações neste espaço\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Criar e gerenciar aplicativos e serviços, e inspecionar logs e relatórios\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Convidar e gerenciar usuários, e ativar recursos para um determinado espaço\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " O caminho especificado pode ser um caminho absoluto ou relativo para um arquivo.\n O arquivo deve conter uma única matriz com objetos JSON descrevendo as regras.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " Mostrar cotas disponíveis com o comando 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i QTD-DE-INSTÂNCIAS] [-k HDD] [-m MEMÓRIA] [-n HOSTNAME] [-p CAMINHO] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " já está iniciada", + "modified": false + }, + { + "id": " is already stopped", + "translation": " já está parada", + "modified": false + }, + { + "id": " is empty", + "translation": " está vazio", + "modified": false + }, + { + "id": " not found", + "translation": " não encontrado", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "Uma ferramenta de linha de comando para interagir com Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "AVANÇADO", + "modified": false + }, + { + "id": "API endpoint", + "translation": "Terminal API", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "Terminal API (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "Terminal da API:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "Terminal API: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "Terminal API: {{.ApiEndpoint}} (Versão da API: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "Terminal API: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APLICATIVOS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Obtendo grupos de segurança para execução como '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Obtendo grupos de segurança para encenação como {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Adicionar uma rota URL para um aplicativo", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adicionando rota {{.URL}} para app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "Todos os planos deste serviço já estão acessíveis a todas as organizações", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "Todos os planos pertencentes à este serviço já está acessível para esta organização", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "Todos os planos deste serviço já estão inacessíveis para todas as organizações", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "Todos os planos pertencentes à este serviço já estão inacessíveis para esta organização", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Também remova rotas mapeadas", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "Uma organização deverá estar definida como alvo antes de definir um espaço", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "Nome do aplicativo é um campo obrigatório", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "Aplicativo {{.AppName}} não existe.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} é um trabalhador, ignorando criação de rotas", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} já está vinculada com {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Anexar informações de diagnóstico para pedidos API em arquivo de log", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assinalar cota para uma organização", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assinale uma função do espaço para um usuário", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assinale uma função da org para um usuário", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assinalando função {{.Role}} para usuário {{.TargetUser}} na org {{.TargetOrg}} / espaço {{.TargetSpace}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assinalando função {{.Role}} para usuário {{.TargetUser}} na org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assinalando grupo de segurança {{.security_group}} para espaço {{.space}} na org {{.organization}} como {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Tentando migrar {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Autenticar usuário não interativamente", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Autenticando...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "GERENTE DE FATURAMENTO", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "COMPILADO EM:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Vincular um grupo de segurança à um espaço", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Vincule um grupo de segurança à lista de grupos para serem usados durante execução de aplicativos", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Vincule um grupo de segurança à lista de grupos para serem usados durante encenação de aplicativos", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Vincular instância de serviço a um aplicativo", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Vínculo entre {{.InstanceName}} e {{.AppName}} não existe", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Vinculando grupo de segurança {{.security_group}} com padrões de execução como {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Vinculando grupo de segurança {{.security_group}} com padrões de encenação como {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Vinculando serviço {{.ServiceInstanceName}} com app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Vinculando serviço {{.ServiceName}} com app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Vinculando {{.URL}} com {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} já existe", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} não existe.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "modified": false + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USUÁRIO SENHA\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group GRUPO-DE-SEGURANÇA", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group GRUPO-DE-SEGURANÇA ORG ESPAÇO", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP INSTÂNCIA_DE_SERVIÇO", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group GRUPO-DE-SEGURANÇA", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TEMPO-LIMITE-EM-MINUTOS] [--trace true | false | caminho/para/arquivo/log] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK CAMINHO POSIÇÃO [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMÍNIO", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route ESPAÇO DOMÍNIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group GRUPO-DE-SEGURANÇA ARQUIVO-DE-REGRAS-JSON", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVIÇO PLANO SERVICE_INSTANCE\n\nEXEMPLO:\n CF_NAME create-service cleardb spark clear-db-mine\n\nDICA:\n Utilize 'CF_NAME create-user-provided-service' para fazer com que serviços fornecidos pelo usuário sejam disponíveis para apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LEGENDA PROVEDOR TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker CORRETOR-DE-SERVIÇO USUÁRIO SENHA URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMÍNIO", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space ESPAÇO [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USUÁRIO SENHA", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X MÉTODO] [-H CABEÇALHO] [-d DATA] [--output ARQUIVO]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMÍNIO [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota COTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMÍNIO [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group GRUPO-DE-SEGURANÇA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LEGENDA PROVEDOR [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker CORRETOR_DE_SERVIÇOS [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMÍNIO [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space ESPAÇO [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USUÁRIO [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [-i INSTÂNCIA] [CAMINHO]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USUÁRIO] [-p SENHA] [-o ORG] [-s ESPAÇO]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMÍNIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances SERVIÇO_v1 PROVEDOR_v1 PLANO_v1 SERVIÇO_v2 PLANO_v2\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVEDOR]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota COTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename NOME-DO-APP NOVO-NOME-DO-APP", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service NOME_DA_INSTÂNCIA_DE_SERVIÇO NOVO_NOME_DA_INSTÂNCIA_DE_SERVIÇO", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker NOME_DO_CORRETOR_DE_SERVIÇOS NOVO_NOME_DO_CORRETOR_DE_SERVIÇOS", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space ESPAÇO NOVO-ESPAÇO", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i QTD-DE-INSTÂNCIAS] [-k HDD] [-m MEMÓRIA] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group GRUPO-DE-SEGURANÇA", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service INSTÂNCIA_DE_SERVIÇO", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NOME VALOR", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USUÁRIO ORG FUNÇÃO\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG COTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USUÁRIO ORG ESPAÇO FUNÇÃO\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space ESPAÇO", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG ESPAÇO", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s ESPAÇO]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group GRUPO-DE-SEGURANÇA", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group GRUPO-DE-SEGURANÇA ORG ESPAÇO", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group GRUPO-DE-SEGURANÇA", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMÍNIO [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NOME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USUÁRIO ORG FUNÇÃO\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USUÁRIO ORG ESPAÇO FUNÇÃO\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p CAMINHO] [-i POSIÇÃO] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group GRUPO-DE-SEGURANÇA ARQUIVO-DE-REGRAS-JSON", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LEGENDA PROVEDOR TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER [-u USERNAME] [-p PASSWORD] [--url URL]", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARÂMETROS] [-l SYSLOG-DRAIN-URL]'\n\nEXEMPLO:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"usuário\":\"admin\",\"senha\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERRO CRIANDO ARQUIVO DE LOG {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Permitido criar instâncias de serviços pagos", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Não é possível exibir serviços disponíveis no mercado sem ter um espaço alvo definido", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Proibido subscrever à planos de serviços pagos", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Não é possível especificar as opções bloquear e desbloquear simultaneamente.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Não é possível especificar {{.Enabled}} e {{.Disabled}} simultaneamente.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Não é possível especificar bits do buildpack em conjunto com bloquear/desbloquear.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Alterar ou exibir a quantidade de instâncias, limite no disco rígido, e limite de memória para um aplicativo", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Modificar senha do usuário", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Modificando senha...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Comando não encontrado", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Conectado, mostrando logs recentes para app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Conectado, mostrando logs continuadamente para app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Não foi possível vincular ao serviço {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Não foi possível determinar o diretório de trabalho atual!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Não foi possível encontrar domínio padrão", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Não foi possível encontrar aplicativo com o nome '{{.AppName}}' no manifesto", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Não foi possível encontrar plano com nome {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Não foi possível encontrar serviço {{.ServiceName}} para vincular à {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Não foi possível analisar o número da versão: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Não foi possível serializar informações", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Não foi possível serializar atualizações.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Não foi possível definir organização como alvo.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Não foi possível criar arquivo temporário para upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Não foi possível abrir arquivo buildpack", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Não foi possível gravar arquivo zip", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Criar um buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Criar um domínio em uma organização para uso posterior", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Criar um domínio que pode ser utilizado por todas as organizações (somente admin)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Criar novo usuário", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Criar uma rota randômica para este app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Criar um grupo de segurança", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Criar um token de autenticação de serviço", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Criar um corretor de serviços", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Criar uma instância de serviço", + "modified": false + }, + { + "id": "Create a space", + "translation": "Criar um espaço", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Criar uma rota URL em um espaço para uso posterior", + "modified": false + }, + { + "id": "Create an org", + "translation": "Criar uma organização", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Criando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Criando buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Criando domínio {{.DomainName}} para org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Criando org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Criando cota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Criando rota {{.Hostname}} para org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Criando rota {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Criando grupo de segurança {{.security_group}} como {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Criando tokens de autenticação de serviços como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Criando corretor de serviços {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Criando serviço {{.ServiceName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Criando domínio compartilhado {{.DomainName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Criando espaço {{.SpaceName}} na org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Criando serviço fornecido por usuário {{.ServiceName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Criando usuário {{.TargetUser}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credenciais foram rejeitadas, por favor, tente novamente.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Senha atual", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Senha atual inválida", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Buildpack personalizado por nome (e.g. meu-buildpack) ou URL GIT (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Cabeçalhos personalizados para incluir no pedido. Sinalizador pode ser utilizado múltiplas vezes.", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMÍNIOS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Definir uma nova cota de recursos", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Remover um buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Remover um domínio", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Remover uma cota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Remover uma rota", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Remover um token de autenticação de serviço", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Remover um corretor de serviços", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Remover instância de serviço", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Remover um domínio compartilhado", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Remover um espaço", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Remover um usuário", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Remover todas as rotas órfãs (e.g.: aquelas que não estão mapeadas a um app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Remover um aplicativo", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Remover uma organização", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Remoção cancelada", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Remove um grupo de segurança", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removendo app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Removendo buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Removendo domínio {{.DomainName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Removendo org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Removendo cota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Removendo rota {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Removendo rota {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Removendo grupo de segurança {{.security_group}} como {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Removendo token de autenticação de serviço como {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Removendo corretor de serviços {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removendo serviço {{.ServiceName}} em org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removendo espaço {{.TargetSpace}} na org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Removendo usuário {{.TargetUser}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Descrição: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Desabilitar acesso a um serviço ou plano de serviço para uma ou todas as organizações", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Desabilitar um buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Desabilitando acesso a plano {{.PlanName}} para serviço {{.ServiceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Desabilitando acesso a todos os planos do serviço {{.ServiceName}} para todas as organizações como {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Desabilitando acesso a todos os planos do serviço {{.ServiceName}} para a organização {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Desabilitando acesso a plano {{.PlanName}} do serviço {{.ServiceName}} para org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Limite de disco rígido (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Exibir status para um determinado aplicativo", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Não colorir saída", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Não mapeie uma rota para este app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Não inicialize este aplicativo após envio", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "URL de documentação: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domínio (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domínios:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Exibir apenas logs recentes ao invés de continuamente", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "VARIÁVEIS DE AMBIENTE", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXEMPLO:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Habilitar informações de diagnóstico CF_TRACE para todos os pedidos e respostas", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Habilitar proxy HTTP para pedidos API", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Habilitar acesso a um serviço ou plano de serviço para uma ou todas as organizações", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Habilitar ou desabilitar cores", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Habilitar o buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Habilitando acesso a plano {{.PlanName}} para serviço {{.ServiceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Habilitando acesso a todos os planos do serviço {{.ServiceName}} para todas as organizações como {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Variável de ambiente {{.VarName}} não foi definida.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Erro construindo pedido", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Erro criando pedido:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Erro criando arquivo temporário: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Erro criando upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Erro criando usuário {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Erro removendo buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Falha ao remover domínio {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Erro mostrando pedido\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Erro mostrando resposta\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Erro encontrando org disponível\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Erro encontrando espaço disponível\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Erro encontrando comando {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Domínio não encontrado {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Arquivo de manifesto não encontrado", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error encontrando org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Erro encontrando org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Erro encontrando espaço {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Erro no requerimento", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Erro ao realizar marshal do JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Erro abrindo arquivo buildpack", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Erro analisando JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Erro analisando cabeçalhos", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Erro durante pedido", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Erro ao ler arquivo de manifesto:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Erro ao ler resposta", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Erro renomenado buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Erro resolvendo rota:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Erro atualizando buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Erro enviando aplicativo.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Erro enviando buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Erro gravando em arquivo temporário: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Erro zipando aplicativo", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Erro: Nome do aplicativo não definido", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Erro: tempo de espera por trabalho assíncrono '{{.ErrURL}}' esgotado", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Erro: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executar um pedido diretamente contra a API, o content-type é definido como application/json por padrão", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Aplicativos deverá ser uma lista de chave/valores\nErro encontrado no manifesto próximo a:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Aplicativos deverá ser uma lista", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "{{.Name}} deverá ser uma combinação de chave =\u003e valor, ao invés de {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "{{.PropertyName}} deverá ser booliano.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "{{.PropertyName}} deverá ser uma lista de strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "{{.PropertyName}} deverá ser um número, ao invés de {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FALHA", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Falha ao obter buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Falha obtendo domínios.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Falha ao obter eventos.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Falha obtendo usuários da org para função {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Falha ao obter organizações.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Falha ao obter rotas.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Falha ao obter corretores de serviços.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Falha obtendo usuários do espaço para função {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Falha obtendo espaços.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Falha ao criar JSON para pedido resource_match", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Falha ao realizar marshal do JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Falha ao iniciar pedido oauth", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Forçar remoção sem confirmação", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Forçar migração sem confirmação", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Forçar reinicialização do app sem confirmação", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "COMEÇANDO", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "OPÇÕES GLOBAIS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Obtendo todos os serviços disponíveis no mercado...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtendo apps na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Obtendo buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Obtendo domínios na organização {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtendo variáveis de ambiente da app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Obtendo eventos da app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtendo arquivos da app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Obtendo informações para organização {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Obtendo informações sobre grupo de segurança {{.security_group}} como {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Obtendo informações para espaço {{.TargetSpace}} na org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Obtendo organizações como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Obtendo informações da cota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Obtendo cotas como {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Obtendo rotas como {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Obtendo grupos de segurança como {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Obtendo tokens de autenticação de serviços como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Listando corretores de serviços como {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Obtendo serviços disponíveis no mercado para org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Obtendo serviços na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Obtendo espaços na org {{.TargetOrgName}} como {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Obtendo stacks na org {{.OrganizationName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Obtendo usuários na org {{.TargetOrg}} / espaço {{.TargetSpace}} como {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Obtendo usuários na org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "Dados HTTP para inclusão no corpo do pedido", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "Método HTTP (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. meu-subdomínio)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignorar arquivo de manifesto", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Incluir cabeçalhos de resposta na saída", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Utilização incorreta", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Utilização incorreta.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Utilização incorreta. Sinalizadores da linha de comando (com exceção de -f) não podem ser utilizados quando enviando multiplos apps através de um arquivo de manifesto.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Número de argumentos incorreto", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Utilização incorreta", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instância", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Resposta JSON do servidor inválida", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Função inválida {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Certificado SSL inválido para {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Resposta assíncrona do servidor inválida", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Token de autenticação inválido: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Cota de disco rígido inválida: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Cota de disco rígido inválida: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Quantidade de instâncias inválida: {{.InstanceCount}}\nA quantidade de instâncias deve ser um número inteiro positivo", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Quantidade de instâncias inválida: {{.InstancesCount}}\nA quantidade de instâncias deve ser um número inteiro positivo", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Instância inválida: {{.Instance}}\nO valor deverá ser um número inteiro positivo", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Instância inválida: {{.Instance}}\nO valor deverá ser menor que {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Arquivo de manifesto inválido. Deverá ser map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Limite de memória inválido: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Limite de memória inválido: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Limite de memória inválido: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Posição inválida. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Parâmetro de tempo limite inválido: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Utilização inválida", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Valor para {{.PropertyName}} inesperado:\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON inválido: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "Exibir todos os aplicativos num determinado espaço", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "Exibir todos os buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "Exibir todas as organizações", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "Exibir todas as rotas disponíveis no espaço alvo", + "modified": false + }, + { + "id": "List all security groups", + "translation": "Exibir todos os grupos de segurança", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "Exibir todas as instâncias de servico no espaço alvo", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "Exibir todos os espaços em uma org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Exibir todas as stacks (uma stack é um container pré-contruído, incluindo um sistema de arquivos e operacional, capaz de executar apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "Exibir todos os usuários na org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "Exibir ofertas disponíveis no mercado", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "Exibir cotas de utilização disponíveis", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "Exibir domínios na organização alvo", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "Exibir grupos de segurança nos padões de execução", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "Exibir grupos de segurança nos padrões de encenação", + "modified": false + }, + { + "id": "List service access settings", + "translation": "Exibir configurações de acesso de serviços", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "Exibir tokens de autenticação de serviços", + "modified": false + }, + { + "id": "List service brokers", + "translation": "Exibir corretores de serviços", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Bloquear o buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Conectar usuário", + "modified": false + }, + { + "id": "Log user out", + "translation": "Desconectar usuário", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Desconectando...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Terminal loggregator ausente em arquivo de configuração", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Fazer com que um serviço fornecido pelo usuário esteja disponível para aplicativos", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Mapear o domínio raiz para este aplicativo", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Tempo de espera máximo para inicialização do aplicativo, em minutos", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Tempo de espera máximo para encenação do buildpack, em minutos", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Tempo de espera limite para inicialização em segundos", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Limite de memória (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrar instâncias de servicos de um plano de serviço a outro", + "modified": false + }, + { + "id": "NAME:", + "translation": "NOME:", + "modified": false + }, + { + "id": "Name", + "translation": "Nome", + "modified": false + }, + { + "id": "New Password", + "translation": "Nova Senha", + "modified": false + }, + { + "id": "New name", + "translation": "Novo nome", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "Nenhum terminal API definido. Utilize '{{.LoginTip}}' ou '{{.APITip}}' para definir.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Nenhuma ação efetuada. O acesso à todos os planos do serviço {{.ServiceName}} deverá ser removido e subsequentemente habilitado para todas as organizações com exceção da organização {{.OrgName}}.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Nenhuma ação efetuada. O acesso ao plano {{.PlaneName}} do serviço {{.ServiceName}} deverá ser removido e subsequentemente habilitado para todas as organizações com exceção da organização {{.OrgName}}.", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "Terminal de API nao definido. Utilize '{{.Name}}' para definir", + "modified": false + }, + { + "id": "No apps found", + "translation": "Nenhum aplicativo encontrado", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "Nenhum buildpack encontrado", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "Nenhum domínio encontrado", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "Nenhum evento para aplicativo {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "Nenhum sinalizador especificado. Nenhuma modificação foi feita.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "Nennhuma organização ou espaço alvo definido, utilize '{{.Command}}' para definir", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "Organização ou espaço inválido, utilize '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "Organização inválida, utilize '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "Nenhuma organização alvo definida, utilize '{{.Command}}' para definir.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "Nenhuma organização encontrada", + "modified": false + }, + { + "id": "No routes found", + "translation": "Nenhuma rota encontrada", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "Nenhum grupo de segurança de execução encontrado", + "modified": false + }, + { + "id": "No security groups", + "translation": "Nenhum grupo de segurança", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "Nenhum corretor de serviço encontrado", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "Nenhuma oferta de serviço encontrada", + "modified": false + }, + { + "id": "No services found", + "translation": "Nenhum serviço encontrado", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "Espaço inválido, utilize '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "Nenhum espaço alvo definido, utilize '{{.Command}}' para definir.", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "Nenhum espaço assinalado", + "modified": false + }, + { + "id": "No spaces found", + "translation": "Nenhum espaço encontrado", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "Nenhum grupo de segurança de encenação encontrado", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "Nenhuma variável de ambiente fornecida pelo sistema foram definidas", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "Nenhuma variável de ambiente fornecida pelo usuário foram definidas", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "Nenhum dos arquivos de seu aplicativo foram modificados. Nada será enviado.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Não está conectado. Utilize '{{.CFLoginCommand}}' para efetuar o log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Quantidade de instâncias", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ADMINISTRAÇÃO DA ORG", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "OUVINTE DA ORG", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "GERENTE DA ORG", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Organização {{.OrgName}} já existe", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} não existe ou está inacessível", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Organização {{.OrgName}} não existe.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organização", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Substituir caminho para o diretório de configuração padrão", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Planos de serviços pagos", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parâmetros", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Senha", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Verificação de senha nao corresponde", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Caminho para o diretório do aplicativo ou arquivo zip", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Caminho para diretório ou arquivo zip", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Caminho para arquivo de manifesto", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plano {{.ServicePlanName}} não pode ser encontrado", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plano {{.ServicePlanName}} não contém instâncias de servicos a serem migradas", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plano: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Por favor escolha entre permitir ou não. Utilizar ambos os sinalizadores não é permitido no mesmo comando.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Tente evitar", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Por favor conecte novamente", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Exibir diagnósticos de pedidos API", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Exibir lista de arquivos em um diretório ou conteúdo de um arquivo específico", + "modified": false + }, + { + "id": "Print the version", + "translation": "Exibir versão", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Propriedade '{{.PropertyName}}' encontrada no manifesto. Esta função não é mais suportada. Por favor remova e tente novamente.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provedor", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Removendo serviço {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Enviar um novo aplicativo ou sincronizar modificações à um já existente", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Enviar um único app (com ou sem manifesto):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Definição de cota {{.QuotaName}} já existe", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Cota {{.QuotaName}} não existe", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "PEDIDO:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPOSTA:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "FUNÇÕES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROTAS", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Deseja realmente remover rotas órfãs?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Deseja realmente remover {{.ModelType}} {{.ModelName}} e tudo com o que estiver associado?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Deseja realmente remover {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Deseja realmente migrar {{.ServiceInstanceDescription}} do plano {{.OldServicePlanName}} para {{.NewServicePlanName}}?", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Deseja realmente remover oferta de serviço {{.ServiceName}} de Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Certificado SSL inválido recebido de ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Remover recursivamente um serviço e seus objetos filhos do banco de dados do Cloud Foundry, sem fazer contato com o corretor de serviços", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remover uma função do espaço de um usuário", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remover uma rota URL de um aplicativo", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remover uma variável de ambiente", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remover uma função da organização de um usuário", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removendo variável de ambiente {{.VarName}} do app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removendo função {{.Role}} do usuário {{.TargetUser}} na org {{.TargetOrg}} / espaço {{.TargetSpace}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removendo função {{.Role}} do usuário {{.TargetUser}} na org {{.TargetOrg}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removendo rota {{.URL}} do app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removendo rota {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Renomear o buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Renomear um corretor de serviços", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Renomear uma instância de serviço", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Renomear um espaço", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Renomear um aplicativo", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Renomear uma organização", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renomeando app {{.AppName}} para {{.NewName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renomeando buildpack {{.OldBuildpackName}} para {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renomenando org {{.OrgName}} para {{.NewName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renomenando corretor de serviço {{.OldName}} para {{.NewName}} como {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renomeando serviço {{.ServiceName}} para {{.NewServiceName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renomenado espaço {{.OldSpaceName}} para {{.NewSpaceName}} na org {{.OrgName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Re-encenar um aplicativo", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Re-encenando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Reinicializar um aplicativo", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Rota {{.URL}} já existe", + "modified": false + }, + { + "id": "Routes", + "translation": "Rotas", + "modified": false + }, + { + "id": "Rules", + "translation": "Regras", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "GRUPOS DE SEGURANÇA", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "ADMINISTRAÇÃO DE SERVIÇOS", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVIÇOS", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "OUVINTE DO ESPAÇO", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "DEVELOPER DO ESPAÇO", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "GERENTE DO ESPAÇO", + "modified": false + }, + { + "id": "SPACES", + "translation": "ESPAÇOS", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Escalando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Grupos de Segurança:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Grupo de segurança {{.security_group}} não existe", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Grupo de segurança {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Selecione um espaço (ou pressione enter para pular):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Selecione uma org (ou pressione enter para pular):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Erro no servidor, código de resposta: {{.ErrStatusCode}}, código de erro: {{.ErrApiErrorCode}}, mensagem: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Token de autenticação de serviço {{.Label}} {{.Provider}} não existe.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Corretor de serviços {{.Name}} não existe.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Instância de serviço não é fornecida pelo usuário", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Instância de serviço: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Oferta de serviço não existe\nDICA: Se você está tentando remover uma oferta de serviço v1, o sinalizador -p é obrigatório.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Serviço {{.ServiceName}} não existe.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Serviço: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Serviços", + "modified": false + }, + { + "id": "Services:", + "translation": "Serviços:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Definir uma variável de ambiente para um aplicativo", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Definir ou exibir URL da API", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Definir ou exibir organização e/ou espaço alvo", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Definindo terminal API como {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Definindo variável de ambiente '{{.VarName}}' como '{{.VarValue}}' para app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Assinalando cota {{.QuotaName}} para org {{.OrgName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Exibir um único grupo de segurança", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Exibir todas as variáveis de ambiente para um aplicativo", + "modified": false + }, + { + "id": "Show help", + "translation": "Exibir ajuda", + "modified": false + }, + { + "id": "Show org info", + "translation": "Exibir informações da organização", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Exibir usuários da org por função", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Exibir informações de cota", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Exibir eventos recentes para um determinado aplicativo", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Exibir informações da instância de serviço", + "modified": false + }, + { + "id": "Show space info", + "translation": "Exibir informações de espaço", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Exibir usuários do espaço por função", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Mostrando escala atual do app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Mostrando status da app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Espaço", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Espaço {{.SpaceName}} já existe", + "modified": false + }, + { + "id": "Space:", + "translation": "Espaço:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack a ser utilizada (uma stack é um container pré-contruído, incluindo um sistema de arquivos e operacional, capaz de executar apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Iniciar um aplicativo", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Tempo de inicialização limite para aplicativo\n\nDICA: utilize '{{.Command}}' para maiores informações", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Inicialização não sucedida\n\nDICA: utilize '{{.Command}}' para maiores informações", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Inicializando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Comando de inicialização, defina como nulo para redefinir como padrão", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Parar um aplicativo", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Parando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "URL para serviço Syslog", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "Fornecida pelo Sistema:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "DICA:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "DICA: Espaço alvo não definido, utilize '{{.CfTargetCommand}}' para definir.", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "DICA: Para fazer com que estas mudanças entrem em vigor, utilize '{{.CFUnbindCommand}}' para desvincular o serviço, '{{.CFBindComand}}' para re-vincular, e finalmente '{{.CFRestageCommand}}' para atualizar o app app com as novas variáveis de ambiente", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "DICA: Utilize '{{.ApiCommand}}' para continuar com um terminal API inseguro", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "DICA: Utilize '{{.CFCommand}}' para garantir que mudanças nas variáveis de ambiente entrem em vigor", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "DICA: Utilize '{{.Command}}' para garantir que modificações de ambiente entrem em vigor", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "DICA: utilize '{{.CfUpdateBuildpackCommand}}' para atualizar este buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Exibir logs recentes ou continuadamente para um aplicativo", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Organização alvo {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Espaço alvo {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Posição do buildpack em relação à outros buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "O plano já está acessível a todas as organizações", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "O plano já está acessível a esta organização", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "O plano já está inacessível à todas as organizações", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "Este plano já está inacessível para esta organização", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "A rota {{.URL}} já esta em uso.\nDICA: Modifique o hostname usando -n HOSTNAME ou use --random-route para gerar uma nova rota e depois tente novamente.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "Não há instâncias deste aplicativo em execução.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "Existem muitas opções a serem exibidas, por favor digite um nome.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "Este domínio é compartilhado com todas as organizações.\nRemovendo-o irá remover todas as rotas associadas, e fará qualquer aplicativo inacessivle através deste domínio. Tem certeza que deseja remover domínio {{.DomainName}}?", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "Isto fará com que o aplicativo seja reiniciado. Tem certeza que deseja escalar app {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Tempo de espera limite para pedidos de HTTP assíncronos", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Total Memory", + "modified": false + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Quantidade total de memória (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Quantidade total de rotas", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Quantidade total de instâncias de serviços", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Traçar pedidos HTTP", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "Terminal UAA ausente em arquivo de configuração", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USO:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "ADMINISTRAÇÃO DE USUÁRIOS", + "modified": false + }, + { + "id": "USERS", + "translation": "USUÁRIOS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Não é possível acessar espaço {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Não foi possível autenticar.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Não foi possível remover, rota '{{.URL}}' não existe.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Desvincular grupo de segurança de um espaço", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Desvincular um grupo de segurança com a lista de grupos para serem usados durante execução de aplicativos", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Desvincular um grupo de segurança com a lista de grupos para serem usados durante encenação de aplicativos", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Desvincular instância de servico de uma app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Desvinculando app {{.AppName}} do serviço {{.ServiceName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Desvinculando grupo de segurança {{.security_group}} dos padrões de execução como {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Desvinculando grupo de segurança {{.security_group}} dos padrões de encenação como {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Desvinculando grupo de segurança {{.security_group}} da {{.organization}} / espaço {{.space}} como {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Desbloquear um buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Atualizar um buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Atualizar um grupo de segurança", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Atualizar token de autenticação do serviço", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Atualizar corretor de serviço", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Atualizar uma cota de recursos existente", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Atualizar par de valores de nome de instância de serviço fornecido pelo usuário", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Atualizando app {{.AppName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Atualizando buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Atualizando cota {{.QuotaName}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Atualizando grupo de segurança {{.security_group}} como {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Atualizando token de autenticação de serviço como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Atualizando corretor de serviço {{.Name}} como {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Atualizando serviço fornecido pelo usuário {{.ServiceName}} na org {{.OrgName}} / espaço {{.SpaceName}} como {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Enviando app com arquivos do caminho: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Enviando buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Enviando {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Enviando {{.ZipFileBytes}}, {{.FileCount}} arquivos", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Utilize '{{.Name}}' para mostrar ou definir sua organização e espaco alvo", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Utilize uma senha de uso único para conectar", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "Usuário {{.TargetUser}} não existe.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "Fornecida pelo Usuário:", + "modified": false + }, + { + "id": "User:", + "translation": "Usuário:", + "modified": false + }, + { + "id": "Username", + "translation": "Usuário", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Utilizando arquivo de manifesto {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Usando rota {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Utilizando stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSÃO:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verifique Senha", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "ATENÇÃO:\n Fornecendo sua senha através da linha de comando é altamente desaconselhável\n Sua senha poderá ficar visível para outros usuários do sistema, e pode ser salva como parte do seu histórico de shell\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "ATENÇÃO: Esta operação pressupõe que o corretor de serviço responsável por esta oferta de serviço não está mais disponível, e todas as instâncias de serviços foram eliminadas, deixando registros órfãos no banco de dados do Cloud Foundry. Todo o conhecimento sobre o serviço será removido do banco de dados, incluindo instâncias de serviços e vínculos de serviço. Não será feita nenhuma tentativa de contato com o corretor de serviço; execução desse comando sem destruir o corretor de serviço fará com que as instâncias de serviço virem órfãs. Depois de executar este comando, você pode querer executar delete-service-auth-token ou delete-service-broker para completar a limpeza.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "ATENÇÃO: Esta é uma operação interna do Cloud Foundry; não haverá contato com os corretores de serviços e recursos para instâncias de serviços não serão alterados. O caso de utilização primário para esta operação é de substituir um corretor de serviços que implementa a API v1, com um corretor que implementa a API v2, por remapeamento de instâncias de serviços dos planos v1 para os planos v2. Recomendamos que os planos v1 sejam marcados como privados ou desligando o corretor de serviço v1 para evitar que instâncias de serviços adicionais sejam criadas. Uma vez que as instâncias de serviços forem migradas, os serviços e planos v1 podem ser removidos do Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Atenção: Terminal HTTP de API inseguro detectado: utilização de certificados SSL no terminal API é altamente recomendado\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Atenção: falha ao tentar exibir logs continuadamente", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Gravar corpo de resposta curl em arquivo ao invés de stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Arquivo zip não contém um buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-CONTEÚDO ESCONDIDO]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[DADOS PRIVADOS ESCONDIDOS]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[variáveis de ambiente]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[opções globais] comando [argumentos...] [opções de comando]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "accesso", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "configurações de planos específicas à um corretor", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "configurações de planos específicas à uma oferta de serviço", + "modified": true + }, + { + "id": "actor", + "translation": "ator", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "permitido", + "modified": false + }, + { + "id": "already exists", + "translation": "já existe", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app falhou", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "falha em pedido de autenticação", + "modified": false + }, + { + "id": "bound apps", + "translation": "aplicativos vinculados", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "corretor: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "falhando", + "modified": false + }, + { + "id": "description", + "translation": "descrição", + "modified": false + }, + { + "id": "disallowed", + "translation": "não permitido", + "modified": false + }, + { + "id": "disk", + "translation": "disco rígido", + "modified": false + }, + { + "id": "disk:", + "translation": "disco rígido:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "não existe.", + "modified": false + }, + { + "id": "domain", + "translation": "domínio", + "modified": false + }, + { + "id": "domains:", + "translation": "dominios:", + "modified": false + }, + { + "id": "down", + "translation": "indisponivel", + "modified": false + }, + { + "id": "enabled", + "translation": "habilitado", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "variável de ambiente '{{.PropertyName}}' não deve ser nula", + "modified": false + }, + { + "id": "event", + "translation": "evento", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "falha ao desabilitar echo durante entrada de senha:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "nome de arquivo", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "utilização incorreta", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instância: {{.InstanceIndex}}, motivo: {{.ExitDescription}}, código de saída: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instâncias", + "modified": false + }, + { + "id": "instances:", + "translation": "instâncias:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "Caminho de herança inválido no manifesto", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "valor inválido para variável de ambiente CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "valor inválido para variável de ambiente CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "legenda", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limitado", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "bloqueado", + "modified": false + }, + { + "id": "memory", + "translation": "memória", + "modified": false + }, + { + "id": "memory:", + "translation": "memória:", + "modified": false + }, + { + "id": "name", + "translation": "nome", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "inválido para o host solicitado", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organização", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "próprio", + "modified": false + }, + { + "id": "paid service plans", + "translation": "planos de serviços pagos", + "modified": false + }, + { + "id": "plan", + "translation": "plano", + "modified": false + }, + { + "id": "plans", + "translation": "planos", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "planos acessíveis a uma organização em particular", + "modified": false + }, + { + "id": "position", + "translation": "posição", + "modified": false + }, + { + "id": "provider", + "translation": "provedor", + "modified": false + }, + { + "id": "quota:", + "translation": "cota:", + "modified": false + }, + { + "id": "requested state", + "translation": "estado requerido", + "modified": false + }, + { + "id": "requested state:", + "translation": "estado requerido:", + "modified": false + }, + { + "id": "routes", + "translation": "rotas", + "modified": false + }, + { + "id": "running", + "translation": "executando", + "modified": false + }, + { + "id": "security group", + "translation": "grupo de segurança", + "modified": false + }, + { + "id": "service", + "translation": "serviço", + "modified": false + }, + { + "id": "service auth token", + "translation": "token de autenticação de serviço", + "modified": false + }, + { + "id": "service instance", + "translation": "instância de serviço", + "modified": false + }, + { + "id": "service instances", + "translation": "instâncias de serviços", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "compartilhado", + "modified": false + }, + { + "id": "since", + "translation": "desde", + "modified": false + }, + { + "id": "space", + "translation": "espaço", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "espaços:", + "modified": false + }, + { + "id": "starting", + "translation": "iniciando", + "modified": false + }, + { + "id": "state", + "translation": "estado", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "parado", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "interrompido após um redirecionamento", + "modified": false + }, + { + "id": "time", + "translation": "tempo", + "modified": false + }, + { + "id": "total memory limit", + "translation": "total memory limit", + "modified": false + }, + { + "id": "unknown authority", + "translation": "autoridade desconhecida", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "uso:", + "modified": false + }, + { + "id": "user", + "translation": "usuário", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "Gravar valores padrão para configuração", + "modified": false + }, + { + "id": "yes", + "translation": "sim", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (Versão da API: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrado.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} de {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} indisponível", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nDICA: Utilize '{{.CFServicesCommand}}' para mostrar todos os serviços nesta org e espaço.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nDICA: utilize '{{.Command}}' para maiores informações", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} falhando", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} de {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} já existe", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} deverá ser uma string ou valor nulo", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} deverá ser uma string", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} não deve ser nula", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M limite de memória, {{.RoutesLimit}} rotas, {{.ServicesLimit}} serviços, serviços pagos {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} de {{.TotalCount}} instâncias em execução", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} iniciando", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instâncias", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/zh_Hans.all.json b/cf/i18n/resources/zh_Hans.all.json new file mode 100644 index 00000000000..3f6d31c98fe --- /dev/null +++ b/cf/i18n/resources/zh_Hans.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\n小贴士:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\n应用程序已启动\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\n小贴士: 为用户{{.CurrentUser}}分配 '组织权限'及'空间权限", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\n小贴士: 使用'{{.CFTargetCommand}}'指定新空间", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\n小贴士: 使用'{{.Command}}'选择新组织", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\n小贴士: 通过cf login或者cf api命令来忽略'--skip-ssl-validation'错误", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - 创建和管理计费账户和付款信息\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"密码\\\"\" (密码中若有引号,需采用转义引号)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (密码中若有空格,需要转义空格)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (不指定用户名和密码参数,CF_NAME将进一步提示你输入用户名和密码)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME 将提供一个可以获得一次性密码的链接)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (密码中若有引号,需采用转义引号)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (密码中若有空格,需要转义空格)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (指定用户名和密码作为参数)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push 应用程序[-b 包名] [-c 命令] [-d 域名] [-f 部署描述文件路径]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push 应用程序 [-f 部署描述文件路径]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - 只能访问组织的信息和报告\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - 邀请和管理用户,选择和改变服务计划,设置配额限制\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " 路径应该是一个zip文件,一个URL到一个zip文件,或本地目录。位置是一个整数,设置优先级,并进行排序,从最低到最高", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " 使用部署描述文件部署多个应用程序:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - 查看此空间中的日志,报告和设置信息\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - 创建和管理应用程序和服务,查看日志和报告\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - 邀请和管理用户,针对一个指定的空间启用各项功能\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " 查看允许配额而'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i 实例数] [-k 磁盘配额] [-m 内存配额] [-n 主机] [-p 应用本地包所在路径] [-s 栈深度] [-t 超时时间]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " 已启动", + "modified": false + }, + { + "id": " is already stopped", + "translation": " 已停止", + "modified": false + }, + { + "id": " is empty", + "translation": " 是空的", + "modified": false + }, + { + "id": " not found", + "translation": " 未找到", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "与Cloud Foundry交互的命令行工具", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "高级", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API 终端", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API 终端 (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API 终端:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API 终端: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API 终端: {{.ApiEndpoint}} (API 版本: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API 终端: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "应用程序", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "同时删除所有绑定的域名", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "在选择空间之前必须选择一个组织", + "modified": false + }, + { + "id": "App ", + "translation": "应用程序 ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "应用程序名称为必填字段", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "应用程序{{.AppName}}不存在", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "应用程序 {{.AppName}}是一个worker程序,跳过路由的创建", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "应用{{.AppName}}已经与服务{{.ServiceName}}绑定了.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "追加API请求诊断信息到日志文件", + "modified": false + }, + { + "id": "Apps:", + "translation": "应用程序:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "给组织分配配额", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "给用户分配空间角色", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "给用户分配组织角色", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "分配角色:{{.Role}}在组织{{.TargetOrg}}或空间{{.TargetSpace}}中分配权限给用户{{.TargetUser}} (作为用户{{.CurrentUser}}) ...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "分配角色:{{.Role}}在组织{{.TargetOrg}}中分配权限给用户{{.TargetUser}} (作为用户{{.CurrentUser}})...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "正尝试迁移{{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "非交互式用户身份验证", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "正在验证,请等待...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "计费管理", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "构建时间:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "绑定一个服务实例到应用程序", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "{{.InstanceName}}和{{.AppName}}之间没有绑定关系", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}正在绑定服务{{.ServiceInstanceName}}到属于组织{{.OrgName}}和空间{{.SpaceName}}中的应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "通过用户{{.Username}}给到组织{{.OrgName}}/空间{{.SpaceName}}下的应用 {{.AppName}} 绑定服务 {{.ServiceName}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "绑定{{.URL}}到{{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "buildpack {{.BuildpackName}} 已经存在", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "buildpack {{.BuildpackName}} 不存在", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "字节数量,必须是以M,MB,G或GB为单位的正整数", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app 应用程序名", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth 用户名 密码\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service 应用程序 服务实例", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout 超时_以分钟为单位] [--trace true | false | 文件访问路径] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack \u003cBUILDPACK\u003e \u003c路径\u003e \u003c位置\u003e [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org 组织", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service 服务类型 服务计划 实例名称\n\n示例:\n CF_NAME create-service cleardb spark clear-db-mine\n\n小贴士:\n 使用'CF_NAME create-user-provided-service'来创建由用户提供的服务供应用使用。", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space 空间 [-o 组织]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user 用户名 密码", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service 服务实例 [-p 参数] [-l SYSLOG-syslog转发地址]\n\n 通过逗号分隔参数来使用交互模式:\n CF_NAME create-user-provided-service 服务实例 -p \"逗号,分隔的,参数,名称\"\n\n 传递JSON格式的参数来使用非交互方式创建服务:\n CF_NAME create-user-provided-service 服务实例 -p '{\"名称\":\"值\",\"名称\":\"值\"}'\n\n示例:\n CF_NAME create-user-provided-service oracle-db-mine -p \"主机, 端口, 数据库名, 用户名, 密码\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"用户名\":\"admin\",\"密码\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete 应用程序名 [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org 组织 [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service 服务实例 [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space 空间 [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user 用户名 [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env 应用程序名", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events 应用程序名", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files 应用程序名 [路径]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u 用户名] [-p 密码] [-o 组织] [-s 空间]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs 应用程序名", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_服务名称 v1_提供者 v1_服务计划 v2_服务名称 v2_服务计划\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org 组织", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users 组织", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering 服务 [-p 提供者]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename 应用程序名 新应用程序名", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack buildpack名称 新的buildpack名称", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org 组织 新组织", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service 原服务实例名称 新服务实例名称", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space 空间 新空间", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage 应用程序名", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart 应用程序名", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale 应用程序 [-i 实例数] [-k 磁盘] [-m 内存] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service 服务实例", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env 应用程序名 环境名 值", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role 用户名 组织 角色\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota 组织 配额\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role 用户名 组织 空间 角色\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space 空间", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users 组织 空间", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start 应用程序名", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop 应用程序", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o 组织] [-s 空间]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service 应用程序 服务实例", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env 应用程序 名称", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role 用户名 组织 角色\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role 用户名 组织 空间 角色\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p 路径] [-i 位置] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service 服务实例 [-p 参数] [-l SYSLOG-syslog转发地址]'\n\n示例:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"用户名\":\"admin\",\"密码\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE 创建日志文件错误 {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "在没有指定空间的情况下无法获取服务列表", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "不能同时指定锁定和解锁选项", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "不能同时指定{{.Enabled}}和{{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "无法指定buildpack以及对其加锁/解锁", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "更改或查看应用程序的实例个数,磁盘空间配额和内存配额", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "更改用户密码", + "modified": false + }, + { + "id": "Changing password...", + "translation": "正在更改密码...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "无效命令", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "已连接,用户{{.Username}}生成组织 {{.OrgName}} / 空间 {{.SpaceName}}下应用程序{{.AppName}} 的日志...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "已连接,用户{{.Username}}读取组织 {{.OrgName}} / 空间 {{.SpaceName}}下应用程序{{.AppName}} 的日志...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "无法绑定到服务{{.ServiceName}}\n错误为: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "无法确定当前的工作目录!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "找不到默认域名", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "无法在部署描述文件中找到名为'{{.AppName}}'的应用程序", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "无法找到名为{{.ServicePlanName}}的服务计划", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "无法找到可用服务{{.ServiceName}}绑定到{{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "无法解析版本号: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "无法序列化信息", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "无法序列化更新部分", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "无法选择组织.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "无法创建上传所需的临时文件", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "无法打开buildpack文件", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "无法写入zip文件", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "创建 buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "创建一个新用户", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "为当前应用程序创建随机路由", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "创建服务实例", + "modified": false + }, + { + "id": "Create a space", + "translation": "创造空间", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "创建组织", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "用户{{.Username}}在组织{{.OrgName}}/空间{{.SpaceName}}中创建应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "创建buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "用户{{.Username}}创建组织{{.OrgName}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "创建路由 {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}在组织{{.OrgName}}/空间{{.SpaceName}}中创建服务{{.ServiceName}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "创建空间中:用户{{.CurrentUser}}在组织{{.OrgName}}中创建空间{{.SpaceName}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}在组织{{.OrgName}}/空间{{.SpaceName}}中创建由用户提供的服务{{.ServiceName}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "创建用户中:当前用户{{.CurrentUser}}正在创建新用户{{.TargetUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "验证失败,请重试", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "当前密码", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "密码不匹配", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "使用名称(e.g. my-buildpack)或GIT网址(e.g. https://github.com/heroku/heroku-buildpack-play.git) 自定义buildpack", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "在请求中自定义HTTP报头, 可以多次使用", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "域", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "删除buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "删除服务实例", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "删除空间", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "删除用户", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "删除一个应用程序", + "modified": false + }, + { + "id": "Delete an org", + "translation": "删除组织", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "撤销删除", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}在组织{{.OrgName}}/空间{{.SpaceName}}中删除应用程序{{.AppName}} ...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "删除buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "用户{{.Username}}删除组织{{.OrgName}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "删除用户{{.CurrentUser}}在组织{{.OrgName}}/空间{{.ServiceName}}中的服务{{.SpaceName}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}删除组织{{.TargetOrg}}中的空间{{.TargetSpace}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "删除用户中:当前用户{{.CurrentUser}}正在删除用户{{.TargetUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "描述: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "禁用buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "磁盘限额(例如256M,1024M,1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "显示应用程序的健康状态", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "禁止彩色输出", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "不为这个应用映射一个路由", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "推送后不启动应用", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "文档URL: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "域名(例如example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "域名:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "生成最近的日志文件,而非读取日志内容", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "环境变量", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "例子:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "启动CF_TRACE输出所有请求和响应", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "为API请求启用HTTP代理", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "启用或禁用彩打输出", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "启用buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "环境变量{{.VarName}}未设置", + "modified": false + }, + { + "id": "Error building request", + "translation": "生成请求错误", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "创建请求错误:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "无法创建临时文件: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "创建上传任务错误", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "创建用户{{.TargetUser}}错误.\n错误: {{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "删除 buildpack {{.Name}},\n错误:{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "打印请求体错误\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "打印响应错误\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "无法找到可用的组织\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "无法找到可用的空间\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "未找到命令 {.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "无法找到 manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "找不到组织{{.OrgName}}\n错误信息: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "无法找到组织 {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "无法找到空间 {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "请求错误", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "转换JSON格式错误", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "打开buildpack文件时出错", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "解析JSON错误", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "头域解析错误", + "modified": false + }, + { + "id": "Error performing request", + "translation": "执行请求错误", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "读取部署描述文件错误:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "读取响应错误", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "重命名buildpack {{.Name}}\n错误:{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "更新错误 buildpack {{.Name}}\n错误:{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "上传应用程序:\n{{.ApiErr}}错误", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "上传buildpack {{.Name}},\n错误:{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "临时文件写入错误: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "压缩应用程序错误", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "错误: 没有找到该应用程序名", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "错误: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "错误: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "执行原始请求,content-type默认设置为application / json", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "预计申请成为键/值pairs\n错误列表发生在舱单附近:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "应用程序集应该是一个列表", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "预期令{{.Name}}为一组关键=\u003e 价值,但它是一{{.Type}}", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "{{.PropertyName}} 应为布尔变量", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "{{.PropertyName}} 应为字符串", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "{{.PropertyName}} 应为数字,不是{{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "失败", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "抓取buildpack失败\n错误:{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "提取事件失败\n错误: {{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "无法获取组织用户的角色{{.OrgRoleToDisplayName}}.\n错误: {{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "提取组织失败.\n错误: {{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "未能获取空间用户的角色{{.SpaceRoleToDisplayName}}.\n错误: {{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "获取不到空间.\n错误信息: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "无法创建JSON格式的resource_match请求", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "转换JSON格式错误", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "无法启动开放授权请求", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "不通过确认强制删除", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "强制迁移", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "无推送强制重启应用", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "入门", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "全局选项", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "从服务市场获取所有服务...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}获取在组织 {{.OrgName}} / 空间 {{.SpaceName}} 中的应用列表...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "获取buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}获取在组织{{.OrgName}} / 空间{{.SpaceName}} 中的应用{{.AppName}} 的环境变量 ...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "作为用户{{.Username}}获取在组织{{.OrgName}} / 空间{{.SpaceName}} 中的应用{{.AppName}} 的事件信息 ...", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}获取在组织{{.OrgName}} / 空间{{.SpaceName}} 中的应用{{.AppName}} 的文件信息 ...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "用户{{.Username}}请求组织{{.OrgName}}的信息...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "获取空间信息中:用户{{.CurrentUser}}在组织{{.OrgName}}中的空间{{.TargetSpace}}信息...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "用户{{.Username}}请求组织...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "从服务市场获取用户{{.CurrentUser}}分配在组织{{.OrgName}}/空间{{.SpaceName}}中的服务...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "获取用户{{.CurrentUser}}在组织{{.OrgName}}/空间{{.SpaceName}}中的服务...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "获取空间:用户{{.CurrentUser}}在组织{{.TargetOrgName}}中获取空间...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "用户{{.Username}}请求分配组织{{.OrganizationName}}/空间{{.SpaceName}}中的stacks...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "用户{{.CurrentUser}}请求分配组织{{.TargetOrg}}/空间{{.TargetSpace}}中的用户", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}请求分配组织{{.TargetOrg}}中的用户...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "包含到请求语句中的HTTP数据", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "域名前缀 (例如: my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "忽略部署描述文件", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "输出包含HTTP响应头", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "不正确的使用\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "不正确使用方法。利用部署描述文件部署多个应用程序时,不能使用命令行标志(除了-f)", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "参数个数错误", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "不正确的使用", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "无效的服务器JSON响应", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "无效的角色 {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "访问{{.URL}}的无效SSL证书\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "服务器无效的异步响应", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "无效的身份验证令牌: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "无效的磁盘配额: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "无效的磁盘配额: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "无效的实例数: {{.InstanceCount}}\n实例数必须是正整数", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "无效的实例数: {{.InstancesCount}}\n实例数量必须是个正整数", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "无效的配置", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "无效的内存配额: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "无效的内存配额: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "无效的超时参数设定: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "无效使用", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "非法{{.PropertyName}}值:\n错误: {{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "无效的JSON: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "列出目标空间中的所有应用程序", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "列出所有buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "列出所有组织", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "列出目标空间中的所有服务实例", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "列出组织中所有的空间", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "列出所有stacks (stack是预先建立好的文件系统, 包含可以运行应用程序的操作系统)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "列出组织中所有的机构", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "列出所有可用的服务", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "锁定该 buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "用户登录", + "modified": false + }, + { + "id": "Log user out", + "translation": "用户退出", + "modified": false + }, + { + "id": "Logging out...", + "translation": "正在退出...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "配置文件中没有loggregator地址信息", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "使这个由用户提供的服务实例对应用生效", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "映射根域名到此应用程序", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "应用实例启动的最长等待时间,以分钟为单位", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "buildpack打包的最长等待时间,以分钟为单位", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "在数秒内启动超时", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "内存配额(例如256M,1024M,1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "将服务实例从一个服务计划迁移到另一个", + "modified": false + }, + { + "id": "NAME:", + "translation": "名称:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "新的密码", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "没有指定API终端。使用'{{.LoginTip}}'或'{{.APITip}}'选择终端", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "没有找到应用程序", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "buildpack未找到", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "没有找到应用程序{{.AppName}}的事件", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "没有指定的参数。未进行任何更改。", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "没有指定组织和空间,使用'{{.Command}}'选择组织和空间", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "没有指定组织或空间,使用'{{.CFTargetCommand}}'选择组织或空间", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "没有指定组织,使用'{{.CFTargetCommand}}'选择组织", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "没有指定组织,使用'{{.Command}}'选择组织.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "没有找到任何组织", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "没有找到服务", + "modified": false + }, + { + "id": "No services found", + "translation": "没有找到服务", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "没有指定空间,使用'{{.CFTargetCommand}}'指定空间", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "没有指定空间,使用'{{.Command}}'指定空间", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "没有找到空间", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "系统提供的环境变量未设置", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "用户定义的环境变量未设置", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "你的应用程序文件没有发生变动,不会上传任何内容", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "尚未登录,请使用'{{.CFLoginCommand}}'来登录", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "实例数", + "modified": false + }, + { + "id": "OK", + "translation": "通过", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "组织管理员", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "组织审计", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "组织管理", + "modified": false + }, + { + "id": "ORGS", + "translation": "组织", + "modified": false + }, + { + "id": "Org", + "translation": "组织", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "组织{{.OrgName}}已经存在", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "组织{{.OrgName}}不存在或无法访问", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "组织{{.OrgName}}不存在", + "modified": false + }, + { + "id": "Org:", + "translation": "组织:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "修改cf配置文件config.json的路径(该路径默认为~/.cf)", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "参数", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "密码", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "密码验证不匹配", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "应用程序目录或zip压缩文件路径", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "目录或zip文件路径", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "部署描述文件的路径", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "无效的服务计划{{.ServicePlanName}}", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "服务计划{{.ServicePlanName}}没有服务实例可迁移", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "请不要", + "modified": false + }, + { + "id": "Please log in again", + "translation": "请重新登录", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "打印API请求诊断信息到标准输出", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "打印目录下的文件清单,或者特定文件的内容", + "modified": false + }, + { + "id": "Print the version", + "translation": "打印版本号", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "清单中有'{{.PropertyName}}'。不再支持此功能。请删除它,再试一次", + "modified": false + }, + { + "id": "Provider", + "translation": "提供者", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "清理服务{{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "部署新的应用程序或同步更改已存在的应用程序", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "部署单一应用程序(带或不带部署描述文件):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "请求:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "响应:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "角色:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "路由", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "确定删除{{.ModelType}} {{.ModelName}}以及所有相关数据和文件?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "确定删除{{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "您确定要将服务{{.ServiceInstanceDescription}}从服务计划{{.OldServicePlanName}}迁移到计划{{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "确定要从Cloud Foundry的清理服务{{.ServiceName}}吗?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "接收到无效的SSL证书, 从: ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "不经过请求服务令牌,递归地从Cloud Foundry的数据库中删除一个服务对象和子对象", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "删除用户在空间中的角色", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "删除一个环境变量", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "删除用户在组织中的角色", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}删除组织{{.OrgName}}/空间{{.SpaceName}}中应用程序{{.AppName}}的环境变量{{.VarName}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "删除角色:用户{{.CurrentUser}}在组织{{.TargetOrg}}或者空间{{.TargetSpace}}中删除用户{{.TargetUser}}的角色{{.Role}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "删除角色:用户{{.CurrentUser}}在组织中{{.TargetOrg}}中删除用户{{.TargetUser}}的角色{{.Role}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "删除路由 {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "重命名buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "重命名服务实例", + "modified": false + }, + { + "id": "Rename a space", + "translation": "重命名空间", + "modified": false + }, + { + "id": "Rename an app", + "translation": "重命名一个应用程序", + "modified": false + }, + { + "id": "Rename an org", + "translation": "重命名一个组织", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "用户{{.Username}}将组织{{.OrgName}} /空间{{.SpaceName}}中的应用程序{{.AppName}}重命名为{{.NewName}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "重命名 buildpack {{.OldBuildpackName}} 到 {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "重命名组织{{.OrgName}}到{{.NewName}}为用户{{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "将服务{{.ServiceName}}重命名为{{.NewServiceName}},该服务属于组织{{.OrgName}} /空间{{.SpaceName}}的{{.CurrentUser}}用户...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "重命名空间:用户{{.CurrentUser}}在组织{{.OrgName}}中将{{.OldSpaceName}}重命名为{{.NewSpaceName}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "重新装载一个应用程序", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}},在组织{{.OrgName}}/空间{{.SpaceName}}中restaging 应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "重新启动一个应用程序", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "服务管理员", + "modified": false + }, + { + "id": "SERVICES", + "translation": "服务", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "空间审计", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "空间开发者", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "空间管理者", + "modified": false + }, + { + "id": "SPACES", + "translation": "空间", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}伸缩组织{{.OrgName}}/空间{{.SpaceName}}中的应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "安全组:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "选择一个空间(或按回车继续):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "选择一个组织(或按回车继续):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "服务器错误,状态代码: {{.ErrStatusCode}}, 错误代码: {{.ErrApiErrorCode}}, 信息: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "不是用户定义的服务实例", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "服务实例: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "服务不存在\n小贴士: 如果你想清理一个v1的服务,请设置-p参数。", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "服务{{.ServiceName}}不存在", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "服务描述: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "服务:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "为一个应用程序设置环境变量", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "设置或查看指定的API网络地址", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "设置或查看指定的组织或空间", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "将API终端设置为 {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "作为用户{{.CurrentUser}}设置组织{{.OrgName}}/空间{{.SpaceName}}中的应用程序{{.AppName}}的环境变量'{{.VarName}}'为'{{.VarValue}}'...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "用户{{.Username}}为组织{{.OrgName}}设置配额{{.QuotaName}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "显示应用程序所有环境变量", + "modified": false + }, + { + "id": "Show help", + "translation": "显示帮助", + "modified": false + }, + { + "id": "Show org info", + "translation": "展示组织信息", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "通过角色展现组织的用户", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "显示应用程序最近的事件", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "显示服务实例的信息", + "modified": false + }, + { + "id": "Show space info", + "translation": "显示空间信息", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "通过角色展现空间的用户", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}显示组织{{.OrgName}}/空间{{.SpaceName}}中应用程序{{.AppName}}的实例数 ...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}显示组织{{.OrgName}}/空间{{.SpaceName}}应用程序{{.AppName}}的健康状态...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "空间{{.SpaceName}}已经存在", + "modified": false + }, + { + "id": "Space:", + "translation": "空间:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "堆栈使用(堆栈是一个预先构建的文件系统,包括一个操作系统,可以用来运行应用程序)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "启动应用程序", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "启动应用程序超时\n\n小贴士: 使用'{{.Command}}'以获取更多信息", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "启动不成功\n\n小贴士: 使用'{{.Command}}'以获取更多信息", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "作为用户{{.CurrentUser}}启动组织{{.OrgName}}/空间{{.SpaceName}}中的应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "启动命令,设置为null可以重置为默认启动命令", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "停止一个应用程序", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "作为用户{{.CurrentUser}}停止组织{{.OrgName}}中/空间{{.SpaceName}}中的应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog转发地址", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "系统提供的:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "小贴士:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "小贴士: 没有指定空间,使用'{{.CfTargetCommand}}'指定空间", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "小贴士: 为了使这些更改生效, 使用'{{.CFUnbindCommand}}解除绑定的服务,使用'{{.CFBindComand}}重新绑定服务,然后使用'{{.CFRestageCommand}}'更新应用使环境变量生效。", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "小贴士: 使用'{{.ApiCommand}}'继续非安全的API终端访问", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "小贴士: 使用'{{.CFCommand}}',来确保您的环境变量更改生效", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "小贴士: 使用'{{.Command}}'来确保你的环境变量更改生效", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "小贴士: 使用'{{.CfUpdateBuildpackCommand}}' 来更新此buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "获取一个应用程序尾部信息或最近的日志", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "已选择组织 {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "已选择空间 {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "其中buildpack buildpacks位置", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "路由 {{.URL}} 已被占用\n小贴士: 请使用-n HOSTNAME 命令行改变主机名称,或使用--random-route命令生成一个新路由,然后重新使用push命令", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "这个程序没有正在运行的实例", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "内容太多,无法显示,请输入名称", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "这将导致应用程序重新启动。您确定要伸缩{{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "异步HTTP请求超时", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memory", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "跟踪HTTP请求", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "配置文件中没有UAA API地址信息", + "modified": false + }, + { + "id": "USAGE:", + "translation": "用法:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "用户管理员", + "modified": false + }, + { + "id": "USERS", + "translation": "用户", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "无法访问空间{{.SpaceName}},\n错误: {{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "无法验证.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "从一个应用程序解绑一个服务实例", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}正在将应用程序{{.AppName}}从属于组织{{.OrgName}}/空间{{.SpaceName}}的服务{{.ServiceName}}上解绑...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "解锁buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "更新buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "更新用户提供的服务实例名称值对", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "作为用户{{.Username}}更新组织{{.OrgName}}/空间{{.SpaceName}}中的应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "更新buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "用户{{.CurrentUser}}正在更新属于组织{{.OrgName}}/空间{{.SpaceName}}的由用户提供的服务{{.ServiceName}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "上传应用程序文件,从: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "上传buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "上传应用程序{{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "上传{{.ZipFileBytes}}, {{.FileCount}}文件", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "使用'{{.Name}}'来查看或设置你选择的组织和空间", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "使用一次性密码登录", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "用户{{.TargetUser}}不存在.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "用户提供的:", + "modified": false + }, + { + "id": "User:", + "translation": "用户:", + "modified": false + }, + { + "id": "Username", + "translation": "用户名", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "使用配置文件{{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "使用路由 {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "使用堆栈{{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "版本:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "校验密码", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "警告:\n 强烈建议不要在命令行参数里指定密码,\n 以防他人读取或通过命令历史记录查找到密码\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "警告: 此操作认为Service Broker所负责这项服务已经不再可用,所有服务实例已被删除,并且已经在Cloud Foundry的数据库中留下了孤儿记录。该操作执行后Cloud Foundry中所有有关该服务的信息都将被删除,包括服务实例和服务绑定。该操作不会尝试通知Service Brocker;所以在不删除Service Brocker的情况下运行此命令将会遗留孤儿服务实例。故运行此命令后,建议您执行delete-service-auth-token或者delete-service-broker来完成这次清理工作.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": " 警告:这是一个Cloud Foundry内部操作;Service Broker不会被通知,服务实例所分配的资源也不会被改变。此项操作的主要适用场景是通过将服务实例从v1服务计划映射到v2服务计划,来将对应的v1 API的Service Broker替换为v2版本。我们建议您关闭v1的Service Brocker并设置v1的服务计划为私有,以防止后续操作创建额外的实例。一旦服务已经迁移完成,就可以从Cloud Foundry中删除v1的服务和服务计划。", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "警告: 检测到不安全的HTTP APT终端,建议使用HTTP安全版 API终端\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "警告: 获取日志出错", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "压缩文档中没有buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA 数据内容隐藏]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[私有数据隐藏]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[环境变量]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[全局选项] 命令 [参数...] [命令选项]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "执行者", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "允许", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "应用程序", + "modified": false + }, + { + "id": "app crashed", + "translation": "应用程序崩溃", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "身份验证请求失败", + "modified": false + }, + { + "id": "bound apps", + "translation": "已绑定的应用", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "CPU内核", + "modified": false + }, + { + "id": "crashing", + "translation": "崩溃", + "modified": false + }, + { + "id": "description", + "translation": "描述", + "modified": false + }, + { + "id": "disallowed", + "translation": "禁止", + "modified": false + }, + { + "id": "disk", + "translation": "磁盘", + "modified": false + }, + { + "id": "disk:", + "translation": "磁盘:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "域:", + "modified": true + }, + { + "id": "down", + "translation": "没在运行", + "modified": false + }, + { + "id": "enabled", + "translation": "已启用", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "环境变量'{{.PropertyName}}'不能为空", + "modified": false + }, + { + "id": "event", + "translation": "事件", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "没有关闭输入显示,你的密码将被显示:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "文件名", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "不正确的使用", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "实例: {{.InstanceIndex}}, 原因: {{.ExitDescription}}, 退出状态/返回码: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "实例", + "modified": false + }, + { + "id": "instances:", + "translation": "实例:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "清单中无效继承路径", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "无效的环境变量值CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "无效的环境变量值CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "锁定", + "modified": false + }, + { + "id": "memory", + "translation": "内存", + "modified": false + }, + { + "id": "memory:", + "translation": "内存:", + "modified": false + }, + { + "id": "name", + "translation": "名称", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "请求的主机名无效", + "modified": false + }, + { + "id": "org", + "translation": "组织", + "modified": false + }, + { + "id": "organization", + "translation": "组织", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "服务计划", + "modified": false + }, + { + "id": "plans", + "translation": "服务计划", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "位置", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "配额:", + "modified": true + }, + { + "id": "requested state", + "translation": "请求状态", + "modified": false + }, + { + "id": "requested state:", + "translation": "请求状态:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "运行", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "服务", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "服务实例", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "从", + "modified": false + }, + { + "id": "space", + "translation": "空间", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "空间:", + "modified": true + }, + { + "id": "starting", + "translation": "启动中", + "modified": false + }, + { + "id": "state", + "translation": "状态", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "已停止", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "一次重定位后停止", + "modified": false + }, + { + "id": "time", + "translation": "时间", + "modified": false + }, + { + "id": "total memory limit", + "translation": "memory limit", + "modified": true + }, + { + "id": "unknown authority", + "translation": "未知的认证", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "网址", + "modified": false + }, + { + "id": "urls:", + "translation": "网址:", + "modified": false + }, + { + "id": "usage:", + "translation": "用法:", + "modified": false + }, + { + "id": "user", + "translation": "用户", + "modified": false + }, + { + "id": "user-provided", + "translation": "由用户提供的", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "在配置文件中写入默认配置", + "modified": false + }, + { + "id": "yes", + "translation": "是的", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API 版本: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [命令]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} 迁移.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskQuota}}中的{{.DiskUsage}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} 失效", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\n小贴士: 使用'{{.CFServicesCommand}}'来查看这个组织和空间里的所有服务。", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\n小贴士: 使用'{{.Command}}'的更多信息", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} 失败", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemQuota}}中的{{.MemUsage}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} 已存在", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} 必须是一个字符串或空值", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} 必须是一个字符串值", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} 不能为空", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M 内存限制, {{.RoutesLimit}} 路由, {{.ServicesLimit}} 服务, 有偿服务 {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.TotalCount}}中的{{.RunningCount}}个实例正在运行", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}}正在启动", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} 乘以 {{.InstanceCount}}实例数", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/resources/zh_Hant.all.json b/cf/i18n/resources/zh_Hant.all.json new file mode 100644 index 00000000000..0feddbd48c3 --- /dev/null +++ b/cf/i18n/resources/zh_Hant.all.json @@ -0,0 +1,4707 @@ +[ + { + "id": "\n\nTIP:\n", + "translation": "\n\nTIP:\n", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "\n\nYour JSON string syntax is invalid. Proper syntax is this: cf set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "\n* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "translation": "* The denoted service plans have specific costs associated with them. If a service instance of this type is created, a cost will be incurred.", + "modified": true + }, + { + "id": "\nApp started\n", + "translation": "\nApp started\n", + "modified": false + }, + { + "id": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "translation": "\nApp {{.AppName}} was started using this command `{{.Command}}`\n", + "modified": false + }, + { + "id": "\nRoute to be unmapped is not currently mapped to the application.", + "translation": "\nRoute to be unmapped is not currently mapped to the application.", + "modified": false + }, + { + "id": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "translation": "\nTIP: Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.", + "modified": false + }, + { + "id": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "translation": "\nTIP: Assign roles with '{{.CurrentUser}} set-org-role' and '{{.CurrentUser}} set-space-role'", + "modified": false + }, + { + "id": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "translation": "\nTIP: Use '{{.CFTargetCommand}}' to target new space", + "modified": false + }, + { + "id": "\nTIP: Use '{{.Command}}' to target new org", + "translation": "\nTIP: Use '{{.Command}}' to target new org", + "modified": false + }, + { + "id": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "translation": "\nTIP: use 'cf login -a API --skip-ssl-validation' or 'cf api API --skip-ssl-validation' to suppress this error", + "modified": false + }, + { + "id": " BillingManager - Create and manage the billing account and payment info\n", + "translation": " BillingManager - Create and manage the billing account and payment info\n", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "translation": " CF_NAME copy-source SOURCE-APP TARGET-APP [-o TARGET-ORG] [-s TARGET-SPACE] [--no-restart]\n", + "modified": false + }, + { + "id": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "translation": " CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n", + "modified": false + }, + { + "id": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "translation": " CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time password to login)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "translation": " CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "translation": " CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", + "modified": false + }, + { + "id": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "translation": " CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", + "modified": false + }, + { + "id": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push APP [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " CF_NAME push [-f MANIFEST_PATH]\n", + "translation": " CF_NAME push [-f MANIFEST_PATH]\n", + "modified": false + }, + { + "id": " OrgAuditor - Read-only access to org info and reports\n", + "translation": " OrgAuditor - Read-only access to org info and reports\n", + "modified": false + }, + { + "id": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "translation": " OrgManager - Invite and manage users, select and change plans, and set spending limits\n", + "modified": false + }, + { + "id": " Path should be a zip file, a url to a zip file, or a local directory. Position is a positive integer, sets priority, and is sorted from lowest to highest.", + "translation": " Path should be a zip file, a url to a zip file, or a local directory. Position is an integer, sets priority, and is sorted from lowest to highest.", + "modified": true + }, + { + "id": " Push multiple apps with a manifest:\n", + "translation": " Push multiple apps with a manifest:\n", + "modified": false + }, + { + "id": " SpaceAuditor - View logs, reports, and settings on this space\n", + "translation": " SpaceAuditor - View logs, reports, and settings on this space\n", + "modified": false + }, + { + "id": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "translation": " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n", + "modified": false + }, + { + "id": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "translation": " SpaceManager - Invite and manage users, and enable features for a given space\n", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "translation": " The provided path can be an absolute or relative path to a file.\n It should have a single array with JSON objects inside describing the rules.", + "modified": false + }, + { + "id": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "translation": " The provided path can be an absolute or relative path to a file. The file should have\n a single array with JSON objects inside describing the rules. The JSON Base Object is \n omitted and only the square brackets and associated child object are required in the file. \n\n Valid json file example:\n [\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n ]", + "modified": false + }, + { + "id": " View allowable quotas with 'CF_NAME quotas'", + "translation": " View allowable quotas with 'CF_NAME quotas'", + "modified": false + }, + { + "id": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "translation": " [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n", + "modified": false + }, + { + "id": " is already started", + "translation": " is already started", + "modified": false + }, + { + "id": " is already stopped", + "translation": " is already stopped", + "modified": false + }, + { + "id": " is empty", + "translation": " is empty", + "modified": false + }, + { + "id": " not found", + "translation": " not found", + "modified": false + }, + { + "id": "A command line tool to interact with Cloud Foundry", + "translation": "A command line tool to interact with Cloud Foundry", + "modified": false + }, + { + "id": "ADVANCED", + "translation": "ADVANCED", + "modified": false + }, + { + "id": "API endpoint", + "translation": "API endpoint", + "modified": false + }, + { + "id": "API endpoint (e.g. https://api.example.com)", + "translation": "API endpoint (e.g. https://api.example.com)", + "modified": false + }, + { + "id": "API endpoint:", + "translation": "API endpoint:", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}}", + "translation": "API endpoint: {{.ApiEndpoint}}", + "modified": false + }, + { + "id": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "translation": "API endpoint: {{.ApiEndpoint}} (API version: {{.ApiVersion}})", + "modified": false + }, + { + "id": "API endpoint: {{.Endpoint}}", + "translation": "API endpoint: {{.Endpoint}}", + "modified": false + }, + { + "id": "APPS", + "translation": "APPS", + "modified": false + }, + { + "id": "Acquiring running security groups as '{{.username}}'", + "translation": "Acquiring running security groups as '{{.username}}'", + "modified": false + }, + { + "id": "Acquiring staging security group as {{.username}}", + "translation": "Acquiring staging security group as {{.username}}", + "modified": false + }, + { + "id": "Add a url route to an app", + "translation": "Add a url route to an app", + "modified": false + }, + { + "id": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Adding route {{.URL}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "All plans of the service are already accessible for all orgs", + "translation": "All plans of the service are already accessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already accessible for this org", + "translation": "All plans of the service are already accessible for the org", + "modified": true + }, + { + "id": "All plans of the service are already inaccessible for all orgs", + "translation": "All plans of the service are already inaccessible for all orgs", + "modified": false + }, + { + "id": "All plans of the service are already inaccessible for this org", + "translation": "All plans of the service are already inaccessible for the org", + "modified": true + }, + { + "id": "Also delete any mapped routes", + "translation": "Also delete any mapped routes", + "modified": false + }, + { + "id": "An org must be targeted before targeting a space", + "translation": "An org must be targeted before targeting a space", + "modified": false + }, + { + "id": "App ", + "translation": "App ", + "modified": false + }, + { + "id": "App name is a required field", + "translation": "App name is a required field", + "modified": false + }, + { + "id": "App {{.AppName}} does not exist.", + "translation": "App {{.AppName}} does not exist.", + "modified": false + }, + { + "id": "App {{.AppName}} is a worker, skipping route creation", + "translation": "App {{.AppName}} is a worker, skipping route creation", + "modified": false + }, + { + "id": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "translation": "App {{.AppName}} is already bound to {{.ServiceName}}.", + "modified": false + }, + { + "id": "Append API request diagnostics to a log file", + "translation": "Append API request diagnostics to a log file", + "modified": false + }, + { + "id": "Apps:", + "translation": "Apps:", + "modified": false + }, + { + "id": "Assign a quota to an org", + "translation": "Assign a quota to an org", + "modified": false + }, + { + "id": "Assign a space quota definition to a space", + "translation": "Assign a space quota definition to a space", + "modified": false + }, + { + "id": "Assign a space role to a user", + "translation": "Assign a space role to a user", + "modified": false + }, + { + "id": "Assign an org role to a user", + "translation": "Assign an org role to a user", + "modified": false + }, + { + "id": "Assigned Value", + "translation": "Assigned Value", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Assigning role {{.Role}} to user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "translation": "Assigning security group {{.security_group}} to space {{.space}} in org {{.organization}} as {{.username}}...", + "modified": false + }, + { + "id": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "translation": "Assigning space quota {{.QuotaName}} to space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "translation": "Attempting to migrate {{.ServiceInstanceDescription}}...", + "modified": false + }, + { + "id": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "translation": "Attention: The plan `{{.PlanName}}` of service `{{.ServiceName}}` is not free. The instance `{{.ServiceInstanceName}}` will incur a cost. Contact your administrator if you think this is in error.", + "modified": false + }, + { + "id": "Authenticate user non-interactively", + "translation": "Authenticate user non-interactively", + "modified": false + }, + { + "id": "Authenticating...", + "translation": "Authenticating...", + "modified": false + }, + { + "id": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "translation": "Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a \u003cendpoint\u003e -u \u003cuser\u003e -o \u003corg\u003e -s \u003cspace\u003e` to log back in and re-authenticate.", + "modified": false + }, + { + "id": "BILLING MANAGER", + "translation": "BILLING MANAGER", + "modified": false + }, + { + "id": "BUILD TIME:", + "translation": "BUILD TIME:", + "modified": false + }, + { + "id": "BUILDPACKS", + "translation": "BUILDPACKS", + "modified": false + }, + { + "id": "Binary file '{{.BinaryFile}}' not found", + "translation": "Binary file '{{.BinaryFile}}' not found", + "modified": false + }, + { + "id": "Bind a security group to a space", + "translation": "Bind a security group to a space", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for running applications", + "translation": "Bind a security group to the list of security groups to be used for running applications", + "modified": false + }, + { + "id": "Bind a security group to the list of security groups to be used for staging applications", + "translation": "Bind a security group to the list of security groups to be used for staging applications", + "modified": false + }, + { + "id": "Bind a service instance to an app", + "translation": "Bind a service instance to an app", + "modified": false + }, + { + "id": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "translation": "Binding between {{.InstanceName}} and {{.AppName}} did not exist", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "translation": "Binding security group {{.security_group}} to defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Binding security group {{.security_group}} to staging as {{.username}}", + "translation": "Binding security group {{.security_group}} to staging as {{.username}}", + "modified": false + }, + { + "id": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Binding service {{.ServiceInstanceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Binding {{.URL}} to {{.AppName}}...", + "translation": "Binding {{.URL}} to {{.AppName}}...", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} already exists", + "translation": "Buildpack {{.BuildpackName}} already exists", + "modified": false + }, + { + "id": "Buildpack {{.BuildpackName}} does not exist.", + "translation": "Buildpack {{.BuildpackName}} does not exist.", + "modified": false + }, + { + "id": "Byte quantity must be an integer with a unit of measurement like M, MB, G, or GB", + "translation": "Byte quantity must be a positive integer with a unit of measurement like M, MB, G, or GB", + "modified": true + }, + { + "id": "CF_NAME api [URL]", + "translation": "CF_NAME api [URL]", + "modified": false + }, + { + "id": "CF_NAME app APP", + "translation": "CF_NAME app APP", + "modified": false + }, + { + "id": "CF_NAME auth USERNAME PASSWORD\n\n", + "translation": "CF_NAME auth USERNAME PASSWORD\n\n", + "modified": false + }, + { + "id": "CF_NAME bind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME bind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME bind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME bind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME bind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME buildpacks", + "translation": "CF_NAME buildpacks", + "modified": false + }, + { + "id": "CF_NAME check-route HOST DOMAIN", + "translation": "CF_NAME check-route HOST DOMAIN", + "modified": false + }, + { + "id": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false] [--locale (LOCALE | CLEAR)]", + "translation": "CF_NAME config [--async-timeout TIMEOUT_IN_MINUTES] [--trace true | false | path/to/file] [--color true | false]", + "modified": true + }, + { + "id": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "translation": "CF_NAME create-buildpack BUILDPACK PATH POSITION [--enable|--disable]", + "modified": false + }, + { + "id": "CF_NAME create-domain ORG DOMAIN", + "translation": "CF_NAME create-domain ORG DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-org ORG", + "translation": "CF_NAME create-org ORG", + "modified": false + }, + { + "id": "CF_NAME create-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-quota QUOTA [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME create-route SPACE DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME create-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "translation": "CF_NAME create-service SERVICE PLAN SERVICE_INSTANCE\n\nEXAMPLE:\n CF_NAME create-service cleardb spark clear-db-mine\n\nTIP:\n Use 'CF_NAME create-user-provided-service' to make user-provided services available to cf apps", + "modified": false + }, + { + "id": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME create-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": false + }, + { + "id": "CF_NAME create-shared-domain DOMAIN", + "translation": "CF_NAME create-shared-domain DOMAIN", + "modified": false + }, + { + "id": "CF_NAME create-space SPACE [-o ORG] [-q SPACE-QUOTA]", + "translation": "CF_NAME create-space SPACE [-o ORG]", + "modified": true + }, + { + "id": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "translation": "CF_NAME create-space-quota QUOTA [-i INSTANCE_MEMORY] [-m MEMORY] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans]", + "modified": false + }, + { + "id": "CF_NAME create-user USERNAME PASSWORD", + "translation": "CF_NAME create-user USERNAME PASSWORD", + "modified": false + }, + { + "id": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "translation": "CF_NAME create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG-DRAIN-URL]\n\n Pass comma separated credential parameter names to enable interactive mode:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n\n Pass credential parameters as JSON to create a service non-interactively:\n CF_NAME create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n\nEXAMPLE:\n CF_NAME create-user-provided-service oracle-db-mine -p \"username, password\"\n CF_NAME create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME create-user-provided-service my-drain-service -l syslog://example.com\n", + "modified": true + }, + { + "id": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "translation": "CF_NAME curl PATH [-iv] [-X METHOD] [-H HEADER] [-d DATA] [--output FILE]", + "modified": false + }, + { + "id": "CF_NAME delete APP [-f -r]", + "translation": "CF_NAME delete APP [-f -r]", + "modified": false + }, + { + "id": "CF_NAME delete-buildpack BUILDPACK [-f]", + "translation": "CF_NAME delete-buildpack BUILDPACK [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-domain DOMAIN [-f]", + "translation": "CF_NAME delete-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-org ORG [-f]", + "translation": "CF_NAME delete-org ORG [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-orphaned-routes [-f]", + "translation": "CF_NAME delete-orphaned-routes [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-quota QUOTA [-f]", + "translation": "CF_NAME delete-quota QUOTA [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "translation": "CF_NAME delete-route DOMAIN [-n HOSTNAME] [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "translation": "CF_NAME delete-security-group SECURITY_GROUP [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "translation": "CF_NAME delete-service SERVICE_INSTANCE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "translation": "CF_NAME delete-service-auth-token LABEL PROVIDER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "translation": "CF_NAME delete-service-broker SERVICE_BROKER [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-shared-domain DOMAIN [-f]", + "translation": "CF_NAME delete-shared-domain DOMAIN [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space SPACE [-f]", + "translation": "CF_NAME delete-space SPACE [-f]", + "modified": false + }, + { + "id": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "translation": "CF_NAME delete-space-quota SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME delete-user USERNAME [-f]", + "translation": "CF_NAME delete-user USERNAME [-f]", + "modified": false + }, + { + "id": "CF_NAME disable-feature-flag FEATURE_NAME", + "translation": "CF_NAME disable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME enable-feature-flag FEATURE_NAME", + "translation": "CF_NAME enable-feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME env APP", + "translation": "CF_NAME env APP", + "modified": false + }, + { + "id": "CF_NAME events APP", + "translation": "CF_NAME events APP", + "modified": false + }, + { + "id": "CF_NAME feature-flag FEATURE_NAME", + "translation": "CF_NAME feature-flag FEATURE_NAME", + "modified": false + }, + { + "id": "CF_NAME feature-flags", + "translation": "CF_NAME feature-flags", + "modified": false + }, + { + "id": "CF_NAME files APP [-i INSTANCE] [PATH]", + "translation": "CF_NAME files APP [PATH]", + "modified": true + }, + { + "id": "CF_NAME install-plugin PATH/TO/PLUGIN", + "translation": "CF_NAME install-plugin PATH/TO/PLUGIN", + "modified": false + }, + { + "id": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "translation": "CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", + "modified": false + }, + { + "id": "CF_NAME logout", + "translation": "CF_NAME logout", + "modified": false + }, + { + "id": "CF_NAME logs APP", + "translation": "CF_NAME logs APP", + "modified": false + }, + { + "id": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME map-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "translation": "CF_NAME migrate-service-instances v1_SERVICE v1_PROVIDER v1_PLAN v2_SERVICE v2_PLAN\n\n", + "modified": false + }, + { + "id": "CF_NAME oauth-token", + "translation": "CF_NAME oauth-token", + "modified": false + }, + { + "id": "CF_NAME org ORG", + "translation": "CF_NAME org ORG", + "modified": false + }, + { + "id": "CF_NAME org-users ORG", + "translation": "CF_NAME org-users ORG", + "modified": false + }, + { + "id": "CF_NAME passwd", + "translation": "CF_NAME passwd", + "modified": false + }, + { + "id": "CF_NAME plugins", + "translation": "CF_NAME plugins", + "modified": false + }, + { + "id": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "translation": "CF_NAME purge-service-offering SERVICE [-p PROVIDER]", + "modified": false + }, + { + "id": "CF_NAME quota QUOTA", + "translation": "CF_NAME quota QUOTA", + "modified": false + }, + { + "id": "CF_NAME quotas", + "translation": "CF_NAME quotas", + "modified": false + }, + { + "id": "CF_NAME rename APP_NAME NEW_APP_NAME", + "translation": "CF_NAME rename APP_NAME NEW_APP_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "translation": "CF_NAME rename-buildpack BUILDPACK_NAME NEW_BUILDPACK_NAME", + "modified": false + }, + { + "id": "CF_NAME rename-org ORG NEW_ORG", + "translation": "CF_NAME rename-org ORG NEW_ORG", + "modified": false + }, + { + "id": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "translation": "CF_NAME rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "translation": "CF_NAME rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", + "modified": false + }, + { + "id": "CF_NAME rename-space SPACE NEW_SPACE", + "translation": "CF_NAME rename-space SPACE NEW_SPACE", + "modified": false + }, + { + "id": "CF_NAME restage APP", + "translation": "CF_NAME restage APP", + "modified": false + }, + { + "id": "CF_NAME restart APP", + "translation": "CF_NAME restart APP", + "modified": false + }, + { + "id": "CF_NAME running-environment-variable-group", + "translation": "cf running-environment-variable-group", + "modified": true + }, + { + "id": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "translation": "CF_NAME scale APP [-i INSTANCES] [-k DISK] [-m MEMORY] [-f]", + "modified": false + }, + { + "id": "CF_NAME security-group SECURITY_GROUP", + "translation": "CF_NAME security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME service SERVICE_INSTANCE", + "translation": "CF_NAME service SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME service-auth-tokens", + "translation": "CF_NAME service-auth-tokens", + "modified": false + }, + { + "id": "CF_NAME set-env APP NAME VALUE", + "translation": "CF_NAME set-env APP NAME VALUE", + "modified": false + }, + { + "id": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME set-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-quota ORG QUOTA\n\n", + "translation": "CF_NAME set-quota ORG QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-running-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "translation": "CF_NAME set-space-quota SPACE-NAME SPACE-QUOTA-NAME", + "modified": false + }, + { + "id": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME set-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "translation": "CF_NAME set-staging-environment-variable-group '{\"name\":\"value\",\"name\":\"value\"}'", + "modified": false + }, + { + "id": "CF_NAME space SPACE", + "translation": "CF_NAME space SPACE", + "modified": false + }, + { + "id": "CF_NAME space-quota SPACE_QUOTA_NAME", + "translation": "CF_NAME space-quota SPACE_QUOTA_NAME", + "modified": false + }, + { + "id": "CF_NAME space-quotas", + "translation": "CF_NAME space-quotas", + "modified": false + }, + { + "id": "CF_NAME space-users ORG SPACE", + "translation": "CF_NAME space-users ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME spaces", + "translation": "CF_NAME spaces", + "modified": false + }, + { + "id": "CF_NAME stacks", + "translation": "CF_NAME stacks", + "modified": false + }, + { + "id": "CF_NAME staging-environment-variable-group", + "translation": "CF_NAME staging-environment-variable-group", + "modified": false + }, + { + "id": "CF_NAME start APP", + "translation": "CF_NAME start APP", + "modified": false + }, + { + "id": "CF_NAME stop APP", + "translation": "CF_NAME stop APP", + "modified": false + }, + { + "id": "CF_NAME target [-o ORG] [-s SPACE]", + "translation": "CF_NAME target [-o ORG] [-s SPACE]", + "modified": false + }, + { + "id": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-running-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "translation": "CF_NAME unbind-security-group SECURITY_GROUP ORG SPACE", + "modified": false + }, + { + "id": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "translation": "CF_NAME unbind-service APP SERVICE_INSTANCE", + "modified": false + }, + { + "id": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "translation": "CF_NAME unbind-staging-security-group SECURITY_GROUP", + "modified": false + }, + { + "id": "CF_NAME uninstall-plugin PLUGIN-NAME", + "translation": "CF_NAME uninstall-plugin PLUGIN-NAME", + "modified": false + }, + { + "id": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "translation": "CF_NAME unmap-route APP DOMAIN [-n HOSTNAME]", + "modified": false + }, + { + "id": "CF_NAME unset-env APP NAME", + "translation": "CF_NAME unset-env APP NAME", + "modified": false + }, + { + "id": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "translation": "CF_NAME unset-org-role USERNAME ORG ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "translation": "CF_NAME unset-space-quota SPACE QUOTA\n\n", + "modified": false + }, + { + "id": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "translation": "CF_NAME unset-space-role USERNAME ORG SPACE ROLE\n\n", + "modified": false + }, + { + "id": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "translation": "CF_NAME update-buildpack BUILDPACK [-p PATH] [-i POSITION] [--enable|--disable] [--lock|--unlock]", + "modified": false + }, + { + "id": "CF_NAME update-quota QUOTA [-m TOTAL_MEMORY] [-i INSTANCE_MEMORY][-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-quota QUOTA [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICE_INSTANCES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "modified": true + }, + { + "id": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "translation": "CF_NAME update-security-group SECURITY_GROUP PATH_TO_JSON_RULES_FILE", + "modified": false + }, + { + "id": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "translation": "CF_NAME update-service SERVICE [-p NEW_PLAN]", + "modified": false + }, + { + "id": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "translation": "CF_NAME update-service-auth-token LABEL PROVIDER TOKEN", + "modified": false + }, + { + "id": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "translation": "CF_NAME update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", + "modified": true + }, + { + "id": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-paid-service-plans | --disallow-paid-service-plans]", + "translation": "CF_NAME update-space-quota SPACE-QUOTA-NAME [-i MAX-INSTANCE-MEMORY] [-m MEMORY] [-n NEW_NAME] [-r ROUTES] [-s SERVICES] [--allow-non-basic-services | --disallow-non-basic-services]", + "modified": true + }, + { + "id": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "translation": "CF_NAME update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\nEXAMPLE:\n CF_NAME update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n CF_NAME update-user-provided-service my-drain-service -l syslog://example.com", + "modified": false + }, + { + "id": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "translation": "CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + "modified": false + }, + { + "id": "Can not provision instances of paid service plans", + "translation": "Can not provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans", + "translation": "Can provision instances of paid service plans", + "modified": false + }, + { + "id": "Can provision instances of paid service plans (Default: disallowed)", + "translation": "Can provision instances of paid service plans (Default: disallowed)", + "modified": false + }, + { + "id": "Cannot list marketplace services without a targeted space", + "translation": "Cannot list marketplace services without a targeted space", + "modified": false + }, + { + "id": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "translation": "Cannot list plan information for {{.ServiceName}} without a targeted space", + "modified": false + }, + { + "id": "Cannot provision instances of paid service plans", + "translation": "Cannot provision instances of paid service plans", + "modified": false + }, + { + "id": "Cannot specify both lock and unlock options.", + "translation": "Cannot specify both lock and unlock options.", + "modified": false + }, + { + "id": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "translation": "Cannot specify both {{.Enabled}} and {{.Disabled}}.", + "modified": false + }, + { + "id": "Cannot specify buildpack bits and lock/unlock.", + "translation": "Cannot specify buildpack bits and lock/unlock.", + "modified": false + }, + { + "id": "Change or view the instance count, disk space limit, and memory limit for an app", + "translation": "Change or view the instance count, disk space limit, and memory limit for an app", + "modified": false + }, + { + "id": "Change service plan for a service instance", + "translation": "Change service plan for a service instance", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Changing password...", + "translation": "Changing password...", + "modified": false + }, + { + "id": "Checking for route...", + "translation": "Checking for route...", + "modified": false + }, + { + "id": "Command Help", + "translation": "Command Help", + "modified": false + }, + { + "id": "Command Name", + "translation": "Command name", + "modified": true + }, + { + "id": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "translation": "Command `{{.Command}}` in the plugin being installed is a native CF command. Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.", + "modified": false + }, + { + "id": "Command not found", + "translation": "Command not found", + "modified": false + }, + { + "id": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, dumping recent logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Connected, tailing logs for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Copying source from app {{.SourceApp}} to target app {{.TargetApp}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "translation": "Could not bind to service {{.ServiceName}}\nError: {{.Err}}", + "modified": false + }, + { + "id": "Could not copy plugin binary: \n{{.Error}}", + "translation": "Could not copy plugin binary: \n{{.Error}}", + "modified": false + }, + { + "id": "Could not determine the current working directory!", + "translation": "Could not determine the current working directory!", + "modified": false + }, + { + "id": "Could not find a default domain", + "translation": "Could not find a default domain", + "modified": false + }, + { + "id": "Could not find app named '{{.AppName}}' in manifest", + "translation": "Could not find app named '{{.AppName}}' in manifest", + "modified": false + }, + { + "id": "Could not find plan with name {{.ServicePlanName}}", + "translation": "Could not find plan with name {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "translation": "Could not find service {{.ServiceName}} to bind to {{.AppName}}", + "modified": false + }, + { + "id": "Could not find space {{.Space}} in organization {{.Org}}", + "translation": "Could not find space {{.Space}} in organization {{.Org}}", + "modified": false + }, + { + "id": "Could not parse version number: {{.Input}}", + "translation": "Could not parse version number: {{.Input}}", + "modified": false + }, + { + "id": "Could not serialize information", + "translation": "Could not serialize information", + "modified": false + }, + { + "id": "Could not serialize updates.", + "translation": "Could not serialize updates.", + "modified": false + }, + { + "id": "Could not target org.\n{{.ApiErr}}", + "translation": "Could not target org.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Couldn't create temp file for upload", + "translation": "Couldn't create temp file for upload", + "modified": false + }, + { + "id": "Couldn't open buildpack file", + "translation": "Couldn't open buildpack file", + "modified": false + }, + { + "id": "Couldn't write zip file", + "translation": "Couldn't write zip file", + "modified": false + }, + { + "id": "Create a buildpack", + "translation": "Create a buildpack", + "modified": false + }, + { + "id": "Create a domain in an org for later use", + "translation": "Create a domain in an org for later use", + "modified": false + }, + { + "id": "Create a domain that can be used by all orgs (admin-only)", + "translation": "Create a domain that can be used by all orgs (admin-only)", + "modified": false + }, + { + "id": "Create a new user", + "translation": "Create a new user", + "modified": false + }, + { + "id": "Create a random route for this app", + "translation": "Create a random route for this app", + "modified": false + }, + { + "id": "Create a security group", + "translation": "Create a security group", + "modified": false + }, + { + "id": "Create a service auth token", + "translation": "Create a service auth token", + "modified": false + }, + { + "id": "Create a service broker", + "translation": "Create a service broker", + "modified": false + }, + { + "id": "Create a service instance", + "translation": "Create a service instance", + "modified": false + }, + { + "id": "Create a space", + "translation": "Create a space", + "modified": false + }, + { + "id": "Create a url route in a space for later use", + "translation": "Create a url route in a space for later use", + "modified": false + }, + { + "id": "Create an org", + "translation": "Create an org", + "modified": false + }, + { + "id": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating buildpack {{.BuildpackName}}...", + "translation": "Creating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating domain {{.DomainName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating org {{.OrgName}} as {{.Username}}...", + "translation": "Creating org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Creating route {{.Hostname}} for org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating route {{.Hostname}}...", + "translation": "Creating route {{.Hostname}}...", + "modified": false + }, + { + "id": "Creating security group {{.security_group}} as {{.username}}", + "translation": "Creating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Creating service auth token as {{.CurrentUser}}...", + "translation": "Creating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating service broker {{.Name}} as {{.Username}}...", + "translation": "Creating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "translation": "Creating shared domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Creating space quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Creating quota {{.QuotaName}} for org {{.OrgName}} as {{.Username}}...", + "modified": true + }, + { + "id": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Creating space {{.SpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Creating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Creating user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Credentials", + "translation": "Credentials", + "modified": false + }, + { + "id": "Credentials were rejected, please try again.", + "translation": "Credentials were rejected, please try again.", + "modified": false + }, + { + "id": "Current CF API version {{.ApiVersion}}", + "translation": "Current CF API version {{.ApiVersion}}", + "modified": false + }, + { + "id": "Current CF CLI version {{.Version}}", + "translation": "Current CF CLI version {{.Version}}", + "modified": false + }, + { + "id": "Current Password", + "translation": "Current Password", + "modified": false + }, + { + "id": "Current password did not match", + "translation": "Current password did not match", + "modified": false + }, + { + "id": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "translation": "Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. https://github.com/heroku/heroku-buildpack-play.git)", + "modified": false + }, + { + "id": "Custom headers to include in the request, flag can be specified multiple times", + "translation": "Custom headers to include in the request, flag can be specified multiple times", + "modified": false + }, + { + "id": "DOMAINS", + "translation": "DOMAINS", + "modified": false + }, + { + "id": "Define a new resource quota", + "translation": "Define a new resource quota", + "modified": false + }, + { + "id": "Define a new space resource quota", + "translation": "Define a new space resource quota", + "modified": false + }, + { + "id": "Delete a buildpack", + "translation": "Delete a buildpack", + "modified": false + }, + { + "id": "Delete a domain", + "translation": "Delete a domain", + "modified": false + }, + { + "id": "Delete a quota", + "translation": "Delete a quota", + "modified": false + }, + { + "id": "Delete a route", + "translation": "Delete a route", + "modified": false + }, + { + "id": "Delete a service auth token", + "translation": "Delete a service auth token", + "modified": false + }, + { + "id": "Delete a service broker", + "translation": "Delete a service broker", + "modified": false + }, + { + "id": "Delete a service instance", + "translation": "Delete a service instance", + "modified": false + }, + { + "id": "Delete a shared domain", + "translation": "Delete a shared domain", + "modified": false + }, + { + "id": "Delete a space", + "translation": "Delete a space", + "modified": false + }, + { + "id": "Delete a space quota definition and unassign the space quota from all spaces", + "translation": " Delete a space quota definition and unassign the space quota from all spaces", + "modified": true + }, + { + "id": "Delete a user", + "translation": "Delete a user", + "modified": false + }, + { + "id": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "translation": "Delete all orphaned routes (e.g.: those that are not mapped to an app)", + "modified": false + }, + { + "id": "Delete an app", + "translation": "Delete an app", + "modified": false + }, + { + "id": "Delete an org", + "translation": "Delete an org", + "modified": false + }, + { + "id": "Delete cancelled", + "translation": "Delete cancelled", + "modified": false + }, + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Deleting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting buildpack {{.BuildpackName}}...", + "translation": "Deleting buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting org {{.OrgName}} as {{.Username}}...", + "translation": "Deleting org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting route {{.Route}}...", + "translation": "Deleting route {{.Route}}...", + "modified": false + }, + { + "id": "Deleting route {{.URL}}...", + "translation": "Deleting route {{.URL}}...", + "modified": false + }, + { + "id": "Deleting security group {{.security_group}} as {{.username}}", + "translation": "Deleting security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Deleting service auth token as {{.CurrentUser}}", + "translation": "Deleting service auth token as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Deleting service broker {{.Name}} as {{.Username}}...", + "translation": "Deleting service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Deleting service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "translation": "Deleting space quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Deleting space {{.TargetSpace}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "translation": "Deleting user {{.TargetUser}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Description: {{.ServiceDescription}}", + "translation": "Description: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Disable access for a specified organization", + "translation": "Disable access for a specified organization", + "modified": false + }, + { + "id": "Disable access to a service or service plan for one or all orgs", + "translation": "Disable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Disable access to a specified service plan", + "translation": "Disable access to a specified service plan", + "modified": false + }, + { + "id": "Disable the buildpack from being used for staging", + "translation": "Disable the buildpack", + "modified": true + }, + { + "id": "Disable the use of a feature so that users have access to and can use the feature.", + "translation": "Disable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Disabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for all orgs as {{.UserName}}...", + "modified": false + }, + { + "id": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Disabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Disk limit (e.g. 256M, 1024M, 1G)", + "translation": "Disk limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Display health and status for app", + "translation": "Display health and status for app", + "modified": false + }, + { + "id": "Do not colorize output", + "translation": "Do not colorize output", + "modified": false + }, + { + "id": "Do not map a route to this app and remove routes from previous pushes of this app.", + "translation": "Do not map a route to this app", + "modified": true + }, + { + "id": "Do not start an app after pushing", + "translation": "Do not start an app after pushing", + "modified": false + }, + { + "id": "Documentation url: {{.URL}}", + "translation": "Documentation url: {{.URL}}", + "modified": false + }, + { + "id": "Domain (e.g. example.com)", + "translation": "Domain (e.g. example.com)", + "modified": false + }, + { + "id": "Domains:", + "translation": "Domains:", + "modified": false + }, + { + "id": "Dump recent logs instead of tailing", + "translation": "Dump recent logs instead of tailing", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLE GROUPS", + "translation": "ENVIRONMENT VARIABLE GROUPS", + "modified": false + }, + { + "id": "ENVIRONMENT VARIABLES", + "translation": "ENVIRONMENT VARIABLES", + "modified": false + }, + { + "id": "EXAMPLE:\n", + "translation": "EXAMPLE:\n", + "modified": false + }, + { + "id": "Enable CF_TRACE output for all requests and responses", + "translation": "Enable CF_TRACE output for all requests and responses", + "modified": false + }, + { + "id": "Enable HTTP proxying for API requests", + "translation": "Enable HTTP proxying for API requests", + "modified": false + }, + { + "id": "Enable access for a specified organization", + "translation": "Enable access for a specified organization", + "modified": false + }, + { + "id": "Enable access to a service or service plan for one or all orgs", + "translation": "Enable access to a service or service plan for one or all orgs", + "modified": false + }, + { + "id": "Enable access to a specified service plan", + "translation": "Enable access to a specified service plan", + "modified": false + }, + { + "id": "Enable or disable color", + "translation": "Enable or disable color", + "modified": false + }, + { + "id": "Enable the buildpack to be used for staging", + "translation": "Enable the buildpack", + "modified": true + }, + { + "id": "Enable the use of a feature so that users have access to and can use the feature.", + "translation": "Enable the use of a feature so that users have access to and can use the feature.", + "modified": false + }, + { + "id": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "translation": "Enabling access of plan {{.PlanName}} for service {{.ServiceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for all orgs as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to all plans of service {{.ServiceName}} for the org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "translation": "Enabling access to plan {{.PlanName}} of service {{.ServiceName}} for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Env variable {{.VarName}} was not set.", + "translation": "Env variable {{.VarName}} was not set.", + "modified": false + }, + { + "id": "Error building request", + "translation": "Error building request", + "modified": false + }, + { + "id": "Error creating request:\n{{.Err}}", + "translation": "Error creating request:\n{{.Err}}", + "modified": false + }, + { + "id": "Error creating tmp file: {{.Err}}", + "translation": "Error creating tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error creating upload", + "translation": "Error creating upload", + "modified": false + }, + { + "id": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "translation": "Error creating user {{.TargetUser}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "translation": "Error deleting buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error deleting domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error dumping request\n{{.Err}}\n", + "translation": "Error dumping request\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error dumping response\n{{.Err}}\n", + "translation": "Error dumping response\n{{.Err}}\n", + "modified": false + }, + { + "id": "Error finding available orgs\n{{.ApiErr}}", + "translation": "Error finding available orgs\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding available spaces\n{{.Err}}", + "translation": "Error finding available spaces\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding command {{.CmdName}}\n", + "translation": "Error finding command {{.CmdName}}\n", + "modified": false + }, + { + "id": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "translation": "Error finding domain {{.DomainName}}\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error finding manifest", + "translation": "Error finding manifest", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "translation": "Error finding org {{.OrgName}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Error finding org {{.OrgName}}\n{{.Err}}", + "translation": "Error finding org {{.OrgName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error finding space {{.SpaceName}}\n{{.Err}}", + "translation": "Error finding space {{.SpaceName}}\n{{.Err}}", + "modified": false + }, + { + "id": "Error getting command list from plugin {{.FilePath}}", + "translation": "Error getting command list from plugin {{.FilePath}}", + "modified": false + }, + { + "id": "Error getting file info", + "translation": "Error getting file info", + "modified": false + }, + { + "id": "Error in requirement", + "translation": "Error in requirement", + "modified": false + }, + { + "id": "Error marshaling JSON", + "translation": "Error marshaling JSON", + "modified": false + }, + { + "id": "Error opening buildpack file", + "translation": "Error opening buildpack file", + "modified": false + }, + { + "id": "Error parsing JSON", + "translation": "Error parsing JSON", + "modified": false + }, + { + "id": "Error parsing headers", + "translation": "Error parsing headers", + "modified": false + }, + { + "id": "Error performing request", + "translation": "Error performing request", + "modified": false + }, + { + "id": "Error reading manifest file:\n{{.Err}}", + "translation": "Error reading manifest file:\n{{.Err}}", + "modified": false + }, + { + "id": "Error reading response", + "translation": "Error reading response", + "modified": false + }, + { + "id": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "translation": "Error renaming buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error resolving route:\n{{.Err}}", + "translation": "Error resolving route:\n{{.Err}}", + "modified": false + }, + { + "id": "Error updating buildpack {{.Name}}\n{{.Error}}", + "translation": "Error updating buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error uploading application.\n{{.ApiErr}}", + "translation": "Error uploading application.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "translation": "Error uploading buildpack {{.Name}}\n{{.Error}}", + "modified": false + }, + { + "id": "Error writing to tmp file: {{.Err}}", + "translation": "Error writing to tmp file: {{.Err}}", + "modified": false + }, + { + "id": "Error zipping application", + "translation": "Error zipping application", + "modified": false + }, + { + "id": "Error: No name found for app", + "translation": "Error: No name found for app", + "modified": false + }, + { + "id": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "translation": "Error: timed out waiting for async job '{{.ErrURL}}' to finish", + "modified": false + }, + { + "id": "Error: {{.Err}}", + "translation": "Error: {{.Err}}", + "modified": false + }, + { + "id": "Executes a raw request, content-type set to application/json by default", + "translation": "Executes a raw request, content-type set to application/json by default", + "modified": false + }, + { + "id": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "translation": "Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + "modified": false + }, + { + "id": "Expected applications to be a list", + "translation": "Expected applications to be a list", + "modified": false + }, + { + "id": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "translation": "Expected {{.Name}} to be a set of key =\u003e value, but it was a {{.Type}}.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a boolean.", + "translation": "Expected {{.PropertyName}} to be a boolean.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a list of strings.", + "translation": "Expected {{.PropertyName}} to be a list of strings.", + "modified": false + }, + { + "id": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "translation": "Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + "modified": false + }, + { + "id": "FAILED", + "translation": "FAILED", + "modified": false + }, + { + "id": "FEATURE FLAGS", + "translation": "FEATURE FLAGS", + "modified": false + }, + { + "id": "Failed fetching buildpacks.\n{{.Error}}", + "translation": "Failed fetching buildpacks.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching domains.\n{{.ApiErr}}", + "translation": "Failed fetching domains.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching events.\n{{.ApiErr}}", + "translation": "Failed fetching events.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching org-users for role {{.OrgRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching orgs.\n{{.ApiErr}}", + "translation": "Failed fetching orgs.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Failed fetching routes.\n{{.Err}}", + "translation": "Failed fetching routes.\n{{.Err}}", + "modified": false + }, + { + "id": "Failed fetching service brokers.\n{{.Error}}", + "translation": "Failed fetching service brokers.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "translation": "Failed fetching space-users for role {{.SpaceRoleToDisplayName}}.\n{{.Error}}", + "modified": false + }, + { + "id": "Failed fetching spaces.\n{{.ErrorDescription}}", + "translation": "Failed fetching spaces.\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Failed to create json for resource_match request", + "translation": "Failed to create json for resource_match request", + "modified": false + }, + { + "id": "Failed to marshal JSON", + "translation": "Failed to marshal JSON", + "modified": false + }, + { + "id": "Failed to start oauth request", + "translation": "Failed to start oauth request", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Disabled.", + "translation": "Feature {{.FeatureFlag}} Disabled.", + "modified": false + }, + { + "id": "Feature {{.FeatureFlag}} Enabled.", + "translation": "Feature {{.FeatureFlag}} Enabled.", + "modified": false + }, + { + "id": "Features", + "translation": "Features", + "modified": false + }, + { + "id": "Force delete (do not prompt for confirmation)", + "translation": "Force delete (do not prompt for confirmation)", + "modified": false + }, + { + "id": "Force deletion without confirmation", + "translation": "Force deletion without confirmation", + "modified": false + }, + { + "id": "Force migration without confirmation", + "translation": "Force migration without confirmation", + "modified": false + }, + { + "id": "Force restart of app without prompt", + "translation": "Force restart of app without prompt", + "modified": false + }, + { + "id": "GETTING STARTED", + "translation": "GETTING STARTED", + "modified": false + }, + { + "id": "GLOBAL OPTIONS", + "translation": "GLOBAL OPTIONS", + "modified": false + }, + { + "id": "Getting OAuth token...", + "translation": "Getting OAuth token...", + "modified": false + }, + { + "id": "Getting all services from marketplace...", + "translation": "Getting all services from marketplace...", + "modified": false + }, + { + "id": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting apps in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting buildpacks...\n", + "translation": "Getting buildpacks...\n", + "modified": false + }, + { + "id": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "translation": "Getting domains in org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting env variables for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "translation": "Getting events for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting files for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for org {{.OrgName}} as {{.Username}}...", + "translation": "Getting info for org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting info for security group {{.security_group}} as {{.username}}", + "translation": "Getting info for security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Getting info for space {{.TargetSpace}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting orgs as {{.Username}}...\n", + "translation": "Getting orgs as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "translation": "Getting quota {{.QuotaName}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting quotas as {{.Username}}...", + "translation": "Getting quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting routes as {{.Username}} ...\n", + "translation": "Getting routes as {{.Username}} ...\n", + "modified": false + }, + { + "id": "Getting security groups as {{.username}}", + "translation": "Getting security groups as {{.username}}", + "modified": false + }, + { + "id": "Getting service access as {{.Username}}...", + "translation": "getting service access as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} and service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for broker {{.Broker}} as {{.Username}}...", + "translation": "getting service access for broker {{.Broker}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} and organization {{.Organization}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service access for service {{.Service}} as {{.Username}}...", + "translation": "getting service access for service {{.Service}} as {{.Username}}...", + "modified": true + }, + { + "id": "Getting service auth tokens as {{.CurrentUser}}...", + "translation": "Getting service auth tokens as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service brokers as {{.Username}}...\n", + "translation": "Getting service brokers as {{.Username}}...\n", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "translation": "Getting service plan information for service {{.ServiceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting service plan information for service {{.ServiceName}}...", + "translation": "Getting service plan information for service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services from marketplace in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Getting services in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Getting space quota {{.Quota}} info as {{.Username}}...", + "translation": "Getting space quota {{.Quota}} info as {{.Username}}...", + "modified": false + }, + { + "id": "Getting space quotas as {{.Username}}...", + "translation": "Getting space quotas as {{.Username}}...", + "modified": false + }, + { + "id": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "translation": "Getting spaces in org {{.TargetOrgName}} as {{.CurrentUser}}...\n", + "modified": false + }, + { + "id": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Getting stacks in org {{.OrganizationName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "translation": "Getting users in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}", + "modified": false + }, + { + "id": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Getting users in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "HTTP data to include in the request body", + "translation": "HTTP data to include in the request body", + "modified": false + }, + { + "id": "HTTP method (GET,POST,PUT,DELETE,etc)", + "translation": "HTTP method (GET,POST,PUT,DELETE,etc)", + "modified": false + }, + { + "id": "Hostname", + "translation": "Hostname", + "modified": false + }, + { + "id": "Hostname (e.g. my-subdomain)", + "translation": "Hostname (e.g. my-subdomain)", + "modified": false + }, + { + "id": "Ignore manifest file", + "translation": "Ignore manifest file", + "modified": false + }, + { + "id": "Include response headers in the output", + "translation": "Include response headers in the output", + "modified": false + }, + { + "id": "Incorrect Usage", + "translation": "Incorrect Usage", + "modified": false + }, + { + "id": "Incorrect Usage.\n", + "translation": "Incorrect Usage.\n", + "modified": false + }, + { + "id": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "translation": "Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.", + "modified": false + }, + { + "id": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "translation": "Incorrect json format: file: {{.JSONFile}}\n\u0009\u0009\nValid json file example:\n[\n {\n \"protocol\": \"tcp\",\n \"destination\": \"10.244.1.18\",\n \"ports\": \"3306\"\n }\n]", + "modified": false + }, + { + "id": "Incorrect number of arguments", + "translation": "Incorrect number of arguments", + "modified": false + }, + { + "id": "Incorrect usage", + "translation": "Incorrect usage", + "modified": false + }, + { + "id": "Install the plugin defined in command argument", + "translation": "install-plugin PATH/TO/PLUGIN-NAME - Install the plugin defined in command argument", + "modified": true + }, + { + "id": "Installing plugin {{.PluginPath}}...", + "translation": "Installing plugin {{.PluginPath}}...", + "modified": false + }, + { + "id": "Instance", + "translation": "Instance", + "modified": false + }, + { + "id": "Instance Memory", + "translation": "Instance Memory", + "modified": false + }, + { + "id": "Invalid JSON response from server", + "translation": "Invalid JSON response from server", + "modified": false + }, + { + "id": "Invalid Role {{.Role}}", + "translation": "Invalid Role {{.Role}}", + "modified": false + }, + { + "id": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "translation": "Invalid SSL Cert for {{.URL}}\n{{.TipMessage}}", + "modified": false + }, + { + "id": "Invalid async response from server", + "translation": "Invalid async response from server", + "modified": false + }, + { + "id": "Invalid auth token: ", + "translation": "Invalid auth token: ", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "translation": "Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstanceCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "translation": "Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid instance memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "translation": "Invalid instance: {{.Instance}}\nInstance must be a positive integer", + "modified": false + }, + { + "id": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "translation": "Invalid instance: {{.Instance}}\nInstance must be less than {{.InstanceCount}}", + "modified": false + }, + { + "id": "Invalid manifest. Expected a map", + "translation": "Invalid manifest. Expected a map", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "translation": "Invalid memory limit: {{.MemoryLimit}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "translation": "Invalid memory limit: {{.Memory}}\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid position. {{.ErrorDescription}}", + "translation": "Invalid position. {{.ErrorDescription}}", + "modified": false + }, + { + "id": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "translation": "Invalid timeout param: {{.Timeout}}\n{{.Err}}", + "modified": false + }, + { + "id": "Invalid usage", + "translation": "Invalid usage", + "modified": false + }, + { + "id": "Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + "translation": "Unexpected value for {{.PropertyName}} :\n{{.Error}}", + "modified": true + }, + { + "id": "JSON is invalid: {{.ErrorDescription}}", + "translation": "JSON is invalid: {{.ErrorDescription}}", + "modified": false + }, + { + "id": "List all apps in the target space", + "translation": "List all apps in the target space", + "modified": false + }, + { + "id": "List all buildpacks", + "translation": "List all buildpacks", + "modified": false + }, + { + "id": "List all orgs", + "translation": "List all orgs", + "modified": false + }, + { + "id": "List all routes in the current space", + "translation": "List all routes in the current space", + "modified": false + }, + { + "id": "List all security groups", + "translation": "List all security groups", + "modified": false + }, + { + "id": "List all service instances in the target space", + "translation": "List all service instances in the target space", + "modified": false + }, + { + "id": "List all spaces in an org", + "translation": "List all spaces in an org", + "modified": false + }, + { + "id": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "List all stacks (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "List all users in the org", + "translation": "List all users in the org", + "modified": false + }, + { + "id": "List available offerings in the marketplace", + "translation": "List available offerings in the marketplace", + "modified": false + }, + { + "id": "List available space resource quotas", + "translation": "List available space resource quotas", + "modified": false + }, + { + "id": "List available usage quotas", + "translation": "List available usage quotas", + "modified": false + }, + { + "id": "List domains in the target org", + "translation": "List domains in the target org", + "modified": false + }, + { + "id": "List security groups in the set of security groups for running applications", + "translation": "List security groups in the set of security groups for running applications", + "modified": false + }, + { + "id": "List security groups in the staging set for applications", + "translation": "List security groups in the staging set for applications", + "modified": false + }, + { + "id": "List service access settings", + "translation": "List service access settings", + "modified": false + }, + { + "id": "List service auth tokens", + "translation": "List service auth tokens", + "modified": false + }, + { + "id": "List service brokers", + "translation": "List service brokers", + "modified": false + }, + { + "id": "Listing Installed Plugins...", + "translation": "Listing Installed Plugins...", + "modified": false + }, + { + "id": "Lock the buildpack to prevent updates", + "translation": "Lock the buildpack", + "modified": true + }, + { + "id": "Log user in", + "translation": "Log user in", + "modified": false + }, + { + "id": "Log user out", + "translation": "Log user out", + "modified": false + }, + { + "id": "Logging out...", + "translation": "Logging out...", + "modified": false + }, + { + "id": "Loggregator endpoint missing from config file", + "translation": "Loggregator endpoint missing from config file", + "modified": false + }, + { + "id": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "translation": "Make a copy of app source code from one application to another. Unless overridden, the copy-source command will restart the application.", + "modified": false + }, + { + "id": "Make a user-provided service instance available to cf apps", + "translation": "Make a user-provided service instance available to cf apps", + "modified": false + }, + { + "id": "Map the root domain to this app", + "translation": "Map the root domain to this app", + "modified": false + }, + { + "id": "Max wait time for app instance startup, in minutes", + "translation": "Max wait time for app instance startup, in minutes", + "modified": false + }, + { + "id": "Max wait time for buildpack staging, in minutes", + "translation": "Max wait time for buildpack staging, in minutes", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "translation": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount.", + "modified": false + }, + { + "id": "Maximum amount of memory an application instance can have (e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "translation": "Maximum amount of memory an application instance can have(e.g. 1024M, 1G, 10G). -1 represents an unlimited amount. (Default: unlimited)", + "modified": true + }, + { + "id": "Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply", + "translation": "Start timeout in seconds", + "modified": true + }, + { + "id": "Memory limit (e.g. 256M, 1024M, 1G)", + "translation": "Memory limit (e.g. 256M, 1024M, 1G)", + "modified": false + }, + { + "id": "Migrate service instances from one service plan to another", + "translation": "Migrate service instances from one service plan to another", + "modified": false + }, + { + "id": "NAME:", + "translation": "NAME:", + "modified": false + }, + { + "id": "Name", + "translation": "Name", + "modified": false + }, + { + "id": "New Password", + "translation": "New Password", + "modified": false + }, + { + "id": "New name", + "translation": "New name", + "modified": false + }, + { + "id": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "translation": "No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + "modified": true + }, + { + "id": "No action taken. You must disable access to all plans of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "Plans are accessible for all orgs. Try removing access for all orgs, then enable access for select orgs.", + "modified": true + }, + { + "id": "No action taken. You must disable access to the {{.PlanName}} plan of {{.ServiceName}} service for all orgs and then grant access for all orgs except the {{.OrgName}} org.", + "translation": "The plan {{.PlaneName}} of service {{.ServiceName}} is already inaccessible for org {{.OrgName}}", + "modified": true + }, + { + "id": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "translation": "No api endpoint set. Use '{{.Name}}' to set an endpoint", + "modified": false + }, + { + "id": "No apps found", + "translation": "No apps found", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "No changes were made", + "translation": "No changes were made", + "modified": false + }, + { + "id": "No domains found", + "translation": "No domains found", + "modified": false + }, + { + "id": "No events for app {{.AppName}}", + "translation": "No events for app {{.AppName}}", + "modified": false + }, + { + "id": "No flags specified. No changes were made.", + "translation": "No flags specified. No changes were made.", + "modified": false + }, + { + "id": "No org and space targeted, use '{{.Command}}' to target an org and space", + "translation": "No org and space targeted, use '{{.Command}}' to target an org and space", + "modified": false + }, + { + "id": "No org or space targeted, use '{{.CFTargetCommand}}'", + "translation": "No org or space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.CFTargetCommand}}'", + "translation": "No org targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No org targeted, use '{{.Command}}' to target an org.", + "translation": "No org targeted, use '{{.Command}}' to target an org.", + "modified": false + }, + { + "id": "No orgs found", + "translation": "No orgs found", + "modified": false + }, + { + "id": "No routes found", + "translation": "No routes found", + "modified": false + }, + { + "id": "No running env variables have been set", + "translation": "No running env variables have been set", + "modified": false + }, + { + "id": "No running security groups set", + "translation": "No running security groups set", + "modified": false + }, + { + "id": "No security groups", + "translation": "No security groups", + "modified": false + }, + { + "id": "No service brokers found", + "translation": "No service brokers found", + "modified": false + }, + { + "id": "No service offerings found", + "translation": "No service offerings found", + "modified": false + }, + { + "id": "No services found", + "translation": "No services found", + "modified": false + }, + { + "id": "No space targeted, use '{{.CFTargetCommand}}'", + "translation": "No space targeted, use '{{.CFTargetCommand}}'", + "modified": false + }, + { + "id": "No space targeted, use '{{.Command}}' to target a space", + "translation": "No space targeted, use '{{.Command}}' to target a space", + "modified": false + }, + { + "id": "No spaces assigned", + "translation": "No spaces assigned", + "modified": false + }, + { + "id": "No spaces found", + "translation": "No spaces found", + "modified": false + }, + { + "id": "No staging env variables have been set", + "translation": "No staging env variables have been set", + "modified": false + }, + { + "id": "No staging security group set", + "translation": "No staging security group set", + "modified": false + }, + { + "id": "No system-provided env variables have been set", + "translation": "No system-provided env variables have been set", + "modified": false + }, + { + "id": "No user-defined env variables have been set", + "translation": "No user-defined env variables have been set", + "modified": false + }, + { + "id": "None of your application files have changed. Nothing will be uploaded.", + "translation": "None of your application files have changed. Nothing will be uploaded.", + "modified": false + }, + { + "id": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "translation": "Not logged in. Use '{{.CFLoginCommand}}' to log in.", + "modified": false + }, + { + "id": "Note: this may take some time", + "translation": "Note: this may take some time", + "modified": false + }, + { + "id": "Number of instances", + "translation": "Number of instances", + "modified": false + }, + { + "id": "OK", + "translation": "OK", + "modified": false + }, + { + "id": "ORG ADMIN", + "translation": "ORG ADMIN", + "modified": false + }, + { + "id": "ORG AUDITOR", + "translation": "ORG AUDITOR", + "modified": false + }, + { + "id": "ORG MANAGER", + "translation": "ORG MANAGER", + "modified": false + }, + { + "id": "ORGS", + "translation": "ORGS", + "modified": false + }, + { + "id": "Org", + "translation": "Org", + "modified": false + }, + { + "id": "Org that contains the target application", + "translation": "Org that contains the target application", + "modified": false + }, + { + "id": "Org {{.OrgName}} already exists", + "translation": "Org {{.OrgName}} already exists", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist or is not accessible", + "translation": "Org {{.OrgName}} does not exist or is not accessible", + "modified": false + }, + { + "id": "Org {{.OrgName}} does not exist.", + "translation": "Org {{.OrgName}} does not exist.", + "modified": false + }, + { + "id": "Org:", + "translation": "Org:", + "modified": false + }, + { + "id": "Organization", + "translation": "Organization", + "modified": false + }, + { + "id": "Override path to default config directory", + "translation": "Override path to default config directory", + "modified": false + }, + { + "id": "Override path to default plugin config directory", + "translation": "Override path to default plugin config directory", + "modified": false + }, + { + "id": "Override restart of the application in target environment after copy-source completes", + "translation": "Override restart of the application in target environment after copy-source completes", + "modified": false + }, + { + "id": "PLUGIN", + "translation": "PLUGIN", + "modified": false + }, + { + "id": "PLUGIN COMMANDS", + "translation": "PLUGIN COMMANDS", + "modified": false + }, + { + "id": "Paid service plans", + "translation": "Paid service plans", + "modified": false + }, + { + "id": "Parameters", + "translation": "Parameters", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a running environment variable group", + "translation": "Pass parameters as JSON to create a running environment variable group", + "modified": false + }, + { + "id": "Pass parameters as JSON to create a staging environment variable group", + "translation": "Pass parameters as JSON to create a staging environment variable group", + "modified": false + }, + { + "id": "Password", + "translation": "Password", + "modified": false + }, + { + "id": "Password verification does not match", + "translation": "Password verification does not match", + "modified": false + }, + { + "id": "Path to app directory or file", + "translation": "Path of app directory or zip file", + "modified": true + }, + { + "id": "Path to directory or zip file", + "translation": "Path to directory or zip file", + "modified": false + }, + { + "id": "Path to manifest", + "translation": "Path to manifest", + "modified": false + }, + { + "id": "Perform a simple check to determine whether a route currently exists or not.", + "translation": "Perform a simple check to determine whether a route currently exists or not.", + "modified": false + }, + { + "id": "Plan does not exist for the {{.ServiceName}} service", + "translation": "Plan does not exist for the {{.ServiceName}} service", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} cannot be found", + "translation": "Plan {{.ServicePlanName}} cannot be found", + "modified": false + }, + { + "id": "Plan {{.ServicePlanName}} has no service instances to migrate", + "translation": "Plan {{.ServicePlanName}} has no service instances to migrate", + "modified": false + }, + { + "id": "Plan: {{.ServicePlanName}}", + "translation": "Plan: {{.ServicePlanName}}", + "modified": false + }, + { + "id": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "translation": "Please choose either allow or disallow. Both flags are not permitted to be passed in the same command.", + "modified": false + }, + { + "id": "Please don't", + "translation": "Please don't", + "modified": false + }, + { + "id": "Please log in again", + "translation": "Please log in again", + "modified": false + }, + { + "id": "Please provide the space within the organization containing the target application", + "translation": "Please provide the space within the organization containing the target application", + "modified": false + }, + { + "id": "Plugin Name", + "translation": "Plugin name", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} does not exist", + "translation": "Plugin name {{.PluginName}} does not exist", + "modified": true + }, + { + "id": "Plugin name {{.PluginName}} is already taken", + "translation": "Plugin name {{.PluginName}} is already taken", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully installed.", + "translation": "Plugin {{.PluginName}} successfully installed.", + "modified": false + }, + { + "id": "Plugin {{.PluginName}} successfully uninstalled.", + "translation": "Plugin name {{.PluginName}} successfully uninstalled", + "modified": true + }, + { + "id": "Print API request diagnostics to stdout", + "translation": "Print API request diagnostics to stdout", + "modified": false + }, + { + "id": "Print out a list of files in a directory or the contents of a specific file", + "translation": "Print out a list of files in a directory or the contents of a specific file", + "modified": false + }, + { + "id": "Print the version", + "translation": "Print the version", + "modified": false + }, + { + "id": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "translation": "Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + "modified": false + }, + { + "id": "Provider", + "translation": "Provider", + "modified": false + }, + { + "id": "Purging service {{.ServiceName}}...", + "translation": "Purging service {{.ServiceName}}...", + "modified": false + }, + { + "id": "Push a new app or sync changes to an existing app", + "translation": "Push a new app or sync changes to an existing app", + "modified": false + }, + { + "id": "Push a single app (with or without a manifest):\n", + "translation": "Push a single app (with or without a manifest):\n", + "modified": false + }, + { + "id": "Quota Definition {{.QuotaName}} already exists", + "translation": "Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created org (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "translation": "Quota to assign to the newly created space (excluding this option results in assignment of default quota)", + "modified": false + }, + { + "id": "Quota {{.QuotaName}} does not exist", + "translation": "Quota {{.QuotaName}} does not exist", + "modified": false + }, + { + "id": "REQUEST:", + "translation": "REQUEST:", + "modified": false + }, + { + "id": "RESPONSE:", + "translation": "RESPONSE:", + "modified": false + }, + { + "id": "ROLES:\n", + "translation": "ROLES:\n", + "modified": false + }, + { + "id": "ROUTES", + "translation": "ROUTES", + "modified": false + }, + { + "id": "Really delete orphaned routes?{{.Prompt}}", + "translation": "Really delete orphaned routes?{{.Prompt}}", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + "modified": false + }, + { + "id": "Really delete the {{.ModelType}} {{.ModelName}}?", + "translation": "Really delete the {{.ModelType}} {{.ModelName}}?", + "modified": false + }, + { + "id": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "translation": "Really migrate {{.ServiceInstanceDescription}} from plan {{.OldServicePlanName}} to {{.NewServicePlanName}}?\u003e", + "modified": false + }, + { + "id": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "translation": "Really purge service offering {{.ServiceName}} from Cloud Foundry?", + "modified": false + }, + { + "id": "Received invalid SSL certificate from ", + "translation": "Received invalid SSL certificate from ", + "modified": false + }, + { + "id": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "translation": "Recursively remove a service and child objects from Cloud Foundry database without making requests to a service broker", + "modified": false + }, + { + "id": "Remove a space role from a user", + "translation": "Remove a space role from a user", + "modified": false + }, + { + "id": "Remove a url route from an app", + "translation": "Remove a url route from an app", + "modified": false + }, + { + "id": "Remove an env variable", + "translation": "Remove an env variable", + "modified": false + }, + { + "id": "Remove an org role from a user", + "translation": "Remove an org role from a user", + "modified": false + }, + { + "id": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Removing env variable {{.VarName}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} / space {{.TargetSpace}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "translation": "Removing role {{.Role}} from user {{.TargetUser}} in org {{.TargetOrg}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Removing route {{.URL}} from app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Removing route {{.URL}}...", + "translation": "Removing route {{.URL}}...", + "modified": false + }, + { + "id": "Rename a buildpack", + "translation": "Rename a buildpack", + "modified": false + }, + { + "id": "Rename a service broker", + "translation": "Rename a service broker", + "modified": false + }, + { + "id": "Rename a service instance", + "translation": "Rename a service instance", + "modified": false + }, + { + "id": "Rename a space", + "translation": "Rename a space", + "modified": false + }, + { + "id": "Rename an app", + "translation": "Rename an app", + "modified": false + }, + { + "id": "Rename an org", + "translation": "Rename an org", + "modified": false + }, + { + "id": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Renaming app {{.AppName}} to {{.NewName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "translation": "Renaming buildpack {{.OldBuildpackName}} to {{.NewBuildpackName}}...", + "modified": false + }, + { + "id": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "translation": "Renaming org {{.OrgName}} to {{.NewName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "translation": "Renaming service broker {{.OldName}} to {{.NewName}} as {{.Username}}", + "modified": false + }, + { + "id": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Renaming service {{.ServiceName}} to {{.NewServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "translation": "Renaming space {{.OldSpaceName}} to {{.NewSpaceName}} in org {{.OrgName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restage an app", + "translation": "Restage an app", + "modified": false + }, + { + "id": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Restaging app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Restart an app", + "translation": "Restart an app", + "modified": false + }, + { + "id": "Retrieve an individual feature flag with status", + "translation": "Retrieve an individual feature flag with status", + "modified": false + }, + { + "id": "Retrieve and display the OAuth token for the current session", + "translation": "Retrieve and display the OAuth token for the current session", + "modified": false + }, + { + "id": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "translation": "Retrieve and display the given app's guid. All other health and status output for the app is suppressed.", + "modified": false + }, + { + "id": "Retrieve list of feature flags with status of each flag-able feature", + "translation": "Retrieve list of feature flags with status of each flag-able feature", + "modified": false + }, + { + "id": "Retrieve the contents of the running environment variable group", + "translation": "Retrieve the contents of the running environment variable group", + "modified": false + }, + { + "id": "Retrieve the contents of the staging environment variable group", + "translation": "Retrieve the contents of the staging environment variable group", + "modified": false + }, + { + "id": "Retrieving status of all flagged features as {{.Username}}...", + "translation": "Retrieving status of all flagged features as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Retrieving status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "translation": "Retrieving the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does exist", + "modified": false + }, + { + "id": "Route {{.HostName}}.{{.DomainName}} does not exist", + "translation": "Route {{.HostName}}.{{.DomainName}} does not exist", + "modified": false + }, + { + "id": "Route {{.URL}} already exists", + "translation": "Route {{.URL}} already exists", + "modified": false + }, + { + "id": "Routes", + "translation": "Routes", + "modified": false + }, + { + "id": "Rules", + "translation": "Rules", + "modified": false + }, + { + "id": "Running Environment Variable Groups:", + "translation": "Running Environment Variable Groups:", + "modified": false + }, + { + "id": "SECURITY GROUP", + "translation": "SECURITY GROUP", + "modified": false + }, + { + "id": "SERVICE ADMIN", + "translation": "SERVICE ADMIN", + "modified": false + }, + { + "id": "SERVICES", + "translation": "SERVICES", + "modified": false + }, + { + "id": "SPACE ADMIN", + "translation": "SPACE ADMIN", + "modified": false + }, + { + "id": "SPACE AUDITOR", + "translation": "SPACE AUDITOR", + "modified": false + }, + { + "id": "SPACE DEVELOPER", + "translation": "SPACE DEVELOPER", + "modified": false + }, + { + "id": "SPACE MANAGER", + "translation": "SPACE MANAGER", + "modified": false + }, + { + "id": "SPACES", + "translation": "SPACES", + "modified": false + }, + { + "id": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Scaling app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Security Groups:", + "translation": "Security Groups:", + "modified": false + }, + { + "id": "Security group {{.security_group}} does not exist", + "translation": "Security group {{.security_group}} does not exist", + "modified": false + }, + { + "id": "Security group {{.security_group}} {{.error_message}}", + "translation": "Security group {{.security_group}} {{.error_message}}", + "modified": false + }, + { + "id": "Select a space (or press enter to skip):", + "translation": "Select a space (or press enter to skip):", + "modified": false + }, + { + "id": "Select an org (or press enter to skip):", + "translation": "Select an org (or press enter to skip):", + "modified": false + }, + { + "id": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "translation": "Server error, status code: 403: Access is denied. You do not have privileges to execute this command.", + "modified": false + }, + { + "id": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "translation": "Server error, status code: {{.ErrStatusCode}}, error code: {{.ErrApiErrorCode}}, message: {{.ErrDescription}}", + "modified": false + }, + { + "id": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "translation": "Service Auth Token {{.Label}} {{.Provider}} does not exist.", + "modified": false + }, + { + "id": "Service Broker {{.Name}} does not exist.", + "translation": "Service Broker {{.Name}} does not exist.", + "modified": false + }, + { + "id": "Service Instance is not user provided", + "translation": "Service Instance is not user provided", + "modified": false + }, + { + "id": "Service instance: {{.ServiceName}}", + "translation": "Service instance: {{.ServiceName}}", + "modified": false + }, + { + "id": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "translation": "Service offering does not exist\nTIP: If you are trying to purge a v1 service offering, you must set the -p flag.", + "modified": false + }, + { + "id": "Service offering not found", + "translation": "Service offering not found", + "modified": false + }, + { + "id": "Service {{.ServiceName}} does not exist.", + "translation": "Service {{.ServiceName}} does not exist.", + "modified": false + }, + { + "id": "Service: {{.ServiceDescription}}", + "translation": "Service: {{.ServiceDescription}}", + "modified": false + }, + { + "id": "Services", + "translation": "Services", + "modified": false + }, + { + "id": "Services:", + "translation": "Services:", + "modified": false + }, + { + "id": "Set an env variable for an app", + "translation": "Set an env variable for an app", + "modified": false + }, + { + "id": "Set or view target api url", + "translation": "Set or view target api url", + "modified": false + }, + { + "id": "Set or view the targeted org or space", + "translation": "Set or view the targeted org or space", + "modified": false + }, + { + "id": "Setting api endpoint to {{.Endpoint}}...", + "translation": "Setting api endpoint to {{.Endpoint}}...", + "modified": false + }, + { + "id": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Setting env variable '{{.VarName}}' to '{{.VarValue}}' for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "translation": "Setting quota {{.QuotaName}} to org {{.OrgName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "translation": "Setting status of {{.FeatureFlag}} as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the running environment variable group as {{.Username}}...", + "translation": "Setting the contents of the running environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Setting the contents of the staging environment variable group as {{.Username}}...", + "translation": "Setting the contents of the staging environment variable group as {{.Username}}...", + "modified": false + }, + { + "id": "Show a single security group", + "translation": "Show a single security group", + "modified": false + }, + { + "id": "Show all env variables for an app", + "translation": "Show all env variables for an app", + "modified": false + }, + { + "id": "Show help", + "translation": "Show help", + "modified": false + }, + { + "id": "Show org info", + "translation": "Show org info", + "modified": false + }, + { + "id": "Show org users by role", + "translation": "Show org users by role", + "modified": false + }, + { + "id": "Show plan details for a particular service offering", + "translation": "Show plan details for a particular service offering", + "modified": false + }, + { + "id": "Show quota info", + "translation": "Show quota info", + "modified": false + }, + { + "id": "Show recent app events", + "translation": "Show recent app events", + "modified": false + }, + { + "id": "Show service instance info", + "translation": "Show service instance info", + "modified": false + }, + { + "id": "Show space info", + "translation": "Show space info", + "modified": false + }, + { + "id": "Show space quota info", + "translation": "Show space quota info", + "modified": false + }, + { + "id": "Show space users by role", + "translation": "Show space users by role", + "modified": false + }, + { + "id": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Showing current scale of app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Space", + "translation": "Space", + "modified": false + }, + { + "id": "Space Quota Definition {{.QuotaName}} already exists", + "translation": "Space Quota Definition {{.QuotaName}} already exists", + "modified": false + }, + { + "id": "Space Quota:", + "translation": "Space Quota:", + "modified": false + }, + { + "id": "Space that contains the target application", + "translation": "Space that contains the target application", + "modified": false + }, + { + "id": "Space {{.SpaceName}} already exists", + "translation": "Space {{.SpaceName}} already exists", + "modified": false + }, + { + "id": "Space:", + "translation": "Space:", + "modified": false + }, + { + "id": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "translation": "Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)", + "modified": false + }, + { + "id": "Staging Environment Variable Groups:", + "translation": "Staging Environment Variable Groups:", + "modified": false + }, + { + "id": "Start an app", + "translation": "Start an app", + "modified": false + }, + { + "id": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start app timeout\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "translation": "Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Startup command, set to null to reset to default start command", + "translation": "Startup command, set to null to reset to default start command", + "modified": false + }, + { + "id": "State", + "translation": "State", + "modified": false + }, + { + "id": "Stop an app", + "translation": "Stop an app", + "modified": false + }, + { + "id": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Stopping app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Syslog Drain Url", + "translation": "Syslog Drain Url", + "modified": false + }, + { + "id": "System-Provided:", + "translation": "System-Provided:", + "modified": false + }, + { + "id": "TIP:\n", + "translation": "TIP:\n", + "modified": false + }, + { + "id": "TIP: Changes will not apply to existing running applications until they are restarted.", + "translation": "TIP: Changes will not apply to existing running applications until they are restarted.", + "modified": false + }, + { + "id": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "translation": "TIP: No space targeted, use '{{.CfTargetCommand}}' to target a space", + "modified": false + }, + { + "id": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "translation": "TIP: To make these changes take effect, use '{{.CFUnbindCommand}}' to unbind the service, '{{.CFBindComand}}' to rebind, and then '{{.CFRestageCommand}}' to update the app with the new env variables", + "modified": false + }, + { + "id": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "translation": "TIP: Use '{{.ApiCommand}}' to continue with an insecure API endpoint", + "modified": false + }, + { + "id": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.CFCommand}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "translation": "TIP: Use '{{.Command}}' to ensure your env variable changes take effect", + "modified": false + }, + { + "id": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "translation": "TIP: use '{{.CfUpdateBuildpackCommand}}' to update this buildpack", + "modified": false + }, + { + "id": "Tail or show recent logs for an app", + "translation": "Tail or show recent logs for an app", + "modified": false + }, + { + "id": "Targeted org {{.OrgName}}\n", + "translation": "Targeted org {{.OrgName}}\n", + "modified": false + }, + { + "id": "Targeted space {{.SpaceName}}\n", + "translation": "Targeted space {{.SpaceName}}\n", + "modified": false + }, + { + "id": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "translation": "The file {{.PluginExecutableName}} already exists under the plugin directory.\n", + "modified": false + }, + { + "id": "The order in which the buildpacks are checked during buildpack auto-detection", + "translation": "Buildpack position among other buildpacks", + "modified": true + }, + { + "id": "The plan is already accessible for all orgs", + "translation": "The plan is already accessible for all orgs", + "modified": false + }, + { + "id": "The plan is already accessible for this org", + "translation": "The plan is already accessible for this org", + "modified": false + }, + { + "id": "The plan is already inaccessible for all orgs", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already accessible for all orgs and no action has been taken at this time.", + "modified": true + }, + { + "id": "The plan is already inaccessible for this org", + "translation": "The plan {{.PlanName}} of service {{.ServiceName}} is already inaccessible for all orgs", + "modified": true + }, + { + "id": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "translation": "The route {{.URL}} is already in use.\nTIP: Change the hostname with -n HOSTNAME or use --random-route to generate a new route and then push again.", + "modified": false + }, + { + "id": "There are no running instances of this app.", + "translation": "There are no running instances of this app.", + "modified": false + }, + { + "id": "There are too many options to display, please type in the name.", + "translation": "There are too many options to display, please type in the name.", + "modified": false + }, + { + "id": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "translation": "This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain {{.DomainName}}? ", + "modified": false + }, + { + "id": "This space already has an assigned space quota.", + "translation": "This space already has an assigned space quota.", + "modified": false + }, + { + "id": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "translation": "This will cause the app to restart. Are you sure you want to scale {{.AppName}}?", + "modified": false + }, + { + "id": "Timeout for async HTTP requests", + "translation": "Timeout for async HTTP requests", + "modified": false + }, + { + "id": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "translation": "To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + "modified": false + }, + { + "id": "Total Memory", + "translation": "Memory", + "modified": true + }, + { + "id": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory (e.g. 1024M, 1G, 10G)", + "modified": false + }, + { + "id": "Total amount of memory a space can have (e.g. 1024M, 1G, 10G)", + "translation": "Total amount of memory a space can have(e.g. 1024M, 1G, 10G)", + "modified": true + }, + { + "id": "Total number of routes", + "translation": "Total number of routes", + "modified": false + }, + { + "id": "Total number of service instances", + "translation": "Total number of service instances", + "modified": false + }, + { + "id": "Trace HTTP requests", + "translation": "Trace HTTP requests", + "modified": false + }, + { + "id": "UAA endpoint missing from config file", + "translation": "UAA endpoint missing from config file", + "modified": false + }, + { + "id": "USAGE:", + "translation": "USAGE:", + "modified": false + }, + { + "id": "USER ADMIN", + "translation": "USER ADMIN", + "modified": false + }, + { + "id": "USERS", + "translation": "USERS", + "modified": false + }, + { + "id": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "translation": "Unable to access space {{.SpaceName}}.\n{{.ApiErr}}", + "modified": false + }, + { + "id": "Unable to authenticate.", + "translation": "Unable to authenticate.", + "modified": false + }, + { + "id": "Unable to delete, route '{{.URL}}' does not exist.", + "translation": "Unable to delete, route '{{.URL}}' does not exist.", + "modified": false + }, + { + "id": "Unable to obtain plugin name for executable {{.Executable}}", + "translation": "Unable to obtain plugin name for executable {{.Executable}}", + "modified": false + }, + { + "id": "Unassign a quota from a space", + "translation": "Unassign a quota from a space", + "modified": false + }, + { + "id": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "translation": "Unassigning space quota {{.QuotaName}} from space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Unbind a security group from a space", + "translation": "Unbind a security group from a space", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for running applications", + "translation": "Unbind a security group from the set of security groups for running applications", + "modified": false + }, + { + "id": "Unbind a security group from the set of security groups for staging applications", + "translation": "Unbind a security group from the set of security groups for staging applications", + "modified": false + }, + { + "id": "Unbind a service instance from an app", + "translation": "Unbind a service instance from an app", + "modified": false + }, + { + "id": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Unbinding app {{.AppName}} from service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for running as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from defaults for staging as {{.username}}", + "modified": false + }, + { + "id": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "translation": "Unbinding security group {{.security_group}} from {{.organization}}/{{.space}} as {{.username}}", + "modified": false + }, + { + "id": "Unexpected error has occurred:\n{{.Error}}", + "translation": "Unexpected error has occurred:\n{{.Error}}", + "modified": false + }, + { + "id": "Uninstall the plugin defined in command argument", + "translation": "PLUGIN-NAME - Uninstall the plugin defined in command argument", + "modified": true + }, + { + "id": "Uninstalling plugin {{.PluginName}}...", + "translation": "Uninstalling plugin {{.PluginName}}...", + "modified": false + }, + { + "id": "Unknown flag", + "translation": "Unknown flag", + "modified": false + }, + { + "id": "Unknown flags:", + "translation": "Unknown flags:", + "modified": false + }, + { + "id": "Unlock the buildpack to enable updates", + "translation": "Unlock the buildpack", + "modified": true + }, + { + "id": "Update a buildpack", + "translation": "Update a buildpack", + "modified": false + }, + { + "id": "Update a security group", + "translation": "Update a security group", + "modified": false + }, + { + "id": "Update a service auth token", + "translation": "Update a service auth token", + "modified": false + }, + { + "id": "Update a service broker", + "translation": "Update a service broker", + "modified": false + }, + { + "id": "Update a service instance", + "translation": "Update a service instance", + "modified": false + }, + { + "id": "Update an existing resource quota", + "translation": "Update an existing resource quota", + "modified": false + }, + { + "id": "Update user-provided service instance name value pairs", + "translation": "Update user-provided service instance name value pairs", + "modified": false + }, + { + "id": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "translation": "Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating buildpack {{.BuildpackName}}...", + "translation": "Updating buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Updating quota {{.QuotaName}} as {{.Username}}...", + "translation": "Updating quota {{.QuotaName}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating security group {{.security_group}} as {{.username}}", + "translation": "Updating security group {{.security_group}} as {{.username}}", + "modified": false + }, + { + "id": "Updating service auth token as {{.CurrentUser}}...", + "translation": "Updating service auth token as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Updating service broker {{.Name}} as {{.Username}}...", + "translation": "Updating service broker {{.Name}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "translation": "Updating service instance {{.ServiceName}} as {{.UserName}}...", + "modified": false + }, + { + "id": "Updating space quota {{.Quota}} as {{.Username}}...", + "translation": "Updating space quota {{.Quota}} as {{.Username}}...", + "modified": false + }, + { + "id": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "translation": "Updating user provided service {{.ServiceName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", + "modified": false + }, + { + "id": "Uploading app files from: {{.Path}}", + "translation": "Uploading app files from: {{.Path}}", + "modified": false + }, + { + "id": "Uploading buildpack {{.BuildpackName}}...", + "translation": "Uploading buildpack {{.BuildpackName}}...", + "modified": false + }, + { + "id": "Uploading {{.AppName}}...", + "translation": "Uploading {{.AppName}}...", + "modified": false + }, + { + "id": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "translation": "Uploading {{.ZipFileBytes}}, {{.FileCount}} files", + "modified": false + }, + { + "id": "Use '{{.Name}}' to view or set your target org and space", + "translation": "Use '{{.Name}}' to view or set your target org and space", + "modified": false + }, + { + "id": "Use a one-time password to login", + "translation": "Use a one-time password to login", + "modified": false + }, + { + "id": "User {{.TargetUser}} does not exist.", + "translation": "User {{.TargetUser}} does not exist.", + "modified": false + }, + { + "id": "User-Provided:", + "translation": "User-Provided:", + "modified": false + }, + { + "id": "User:", + "translation": "User:", + "modified": false + }, + { + "id": "Username", + "translation": "Username", + "modified": false + }, + { + "id": "Using manifest file {{.Path}}\n", + "translation": "Using manifest file {{.Path}}\n", + "modified": false + }, + { + "id": "Using route {{.RouteURL}}", + "translation": "Using route {{.RouteURL}}", + "modified": false + }, + { + "id": "Using stack {{.StackName}}...", + "translation": "Using stack {{.StackName}}...", + "modified": false + }, + { + "id": "VERSION:", + "translation": "VERSION:", + "modified": false + }, + { + "id": "Variable Name", + "translation": "Variable Name", + "modified": false + }, + { + "id": "Verify Password", + "translation": "Verify Password", + "modified": false + }, + { + "id": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "translation": "WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n", + "modified": false + }, + { + "id": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "translation": "WARNING: This operation assumes that the service broker responsible for this service offering is no longer available, and all service instances have been deleted, leaving orphan records in Cloud Foundry's database. All knowledge of the service will be removed from Cloud Foundry, including service instances and service bindings. No attempt will be made to contact the service broker; running this command without destroying the service broker will cause orphan service instances. After running this command you may want to run either delete-service-auth-token or delete-service-broker to complete the cleanup.", + "modified": false + }, + { + "id": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "translation": "WARNING: This operation is internal to Cloud Foundry; service brokers will not be contacted and resources for service instances will not be altered. The primary use case for this operation is to replace a service broker which implements the v1 Service Broker API with a broker which implements the v2 API by remapping service instances from v1 plans to v2 plans. We recommend making the v1 plan private or shutting down the v1 broker to prevent additional instances from being created. Once service instances have been migrated, the v1 services and plans can be removed from Cloud Foundry.", + "modified": false + }, + { + "id": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "translation": "Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n", + "modified": false + }, + { + "id": "Warning: error tailing logs", + "translation": "Warning: error tailing logs", + "modified": false + }, + { + "id": "Write curl body to FILE instead of stdout", + "translation": "Write curl body to FILE instead of stdout", + "modified": false + }, + { + "id": "Zip archive does not contain a buildpack", + "translation": "Zip archive does not contain a buildpack", + "modified": false + }, + { + "id": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "translation": "[MULTIPART/FORM-DATA CONTENT HIDDEN]", + "modified": false + }, + { + "id": "[PRIVATE DATA HIDDEN]", + "translation": "[PRIVATE DATA HIDDEN]", + "modified": false + }, + { + "id": "[environment variables]", + "translation": "[environment variables]", + "modified": false + }, + { + "id": "[global options] command [arguments...] [command options]", + "translation": "[global options] command [arguments...] [command options]", + "modified": false + }, + { + "id": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "translation": "`{{.Command}}` is a command in plugin '{{.PluginName}}'. You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command. However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.", + "modified": false + }, + { + "id": "access", + "translation": "access", + "modified": false + }, + { + "id": "access for plans of a particular broker", + "translation": "settings for a specific service", + "modified": true + }, + { + "id": "access for plans of a particular service offering", + "translation": "settings for a specific broker", + "modified": true + }, + { + "id": "actor", + "translation": "actor", + "modified": false + }, + { + "id": "all", + "translation": "all", + "modified": false + }, + { + "id": "allowed", + "translation": "allowed", + "modified": false + }, + { + "id": "already exists", + "translation": "already exists", + "modified": false + }, + { + "id": "app", + "translation": "app", + "modified": false + }, + { + "id": "app crashed", + "translation": "app crashed", + "modified": false + }, + { + "id": "apps", + "translation": "apps", + "modified": false + }, + { + "id": "auth request failed", + "translation": "auth request failed", + "modified": false + }, + { + "id": "bound apps", + "translation": "bound apps", + "modified": false + }, + { + "id": "broker: {{.Name}}", + "translation": "broker: {{.Name}}", + "modified": false + }, + { + "id": "cpu", + "translation": "cpu", + "modified": false + }, + { + "id": "crashing", + "translation": "crashing", + "modified": false + }, + { + "id": "description", + "translation": "description", + "modified": false + }, + { + "id": "disallowed", + "translation": "disallowed", + "modified": false + }, + { + "id": "disk", + "translation": "disk", + "modified": false + }, + { + "id": "disk:", + "translation": "disk:", + "modified": false + }, + { + "id": "does not exist.", + "translation": "does not exist.", + "modified": false + }, + { + "id": "domain", + "translation": "domain", + "modified": false + }, + { + "id": "domains:", + "translation": "domains:", + "modified": true + }, + { + "id": "down", + "translation": "down", + "modified": false + }, + { + "id": "enabled", + "translation": "enabled", + "modified": false + }, + { + "id": "env var '{{.PropertyName}}' should not be null", + "translation": "env var '{{.PropertyName}}' should not be null", + "modified": false + }, + { + "id": "event", + "translation": "event", + "modified": false + }, + { + "id": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "translation": "failed turning off console echo for password entry:\n{{.ErrorDescription}}", + "modified": false + }, + { + "id": "filename", + "translation": "filename", + "modified": false + }, + { + "id": "free or paid", + "translation": "free or paid", + "modified": false + }, + { + "id": "host", + "translation": "host", + "modified": false + }, + { + "id": "incorrect usage", + "translation": "incorrect usage", + "modified": false + }, + { + "id": "instance memory limit", + "translation": "instance memory limit", + "modified": false + }, + { + "id": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "translation": "instance: {{.InstanceIndex}}, reason: {{.ExitDescription}}, exit_status: {{.ExitStatus}}", + "modified": false + }, + { + "id": "instances", + "translation": "instances", + "modified": false + }, + { + "id": "instances:", + "translation": "instances:", + "modified": false + }, + { + "id": "invalid inherit path in manifest", + "translation": "invalid inherit path in manifest", + "modified": false + }, + { + "id": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "translation": "invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", + "modified": false + }, + { + "id": "label", + "translation": "label", + "modified": false + }, + { + "id": "last uploaded:", + "translation": "package uploaded:", + "modified": true + }, + { + "id": "limited", + "translation": "limited", + "modified": false + }, + { + "id": "list all available plugin commands", + "translation": "list all available plugin commands", + "modified": false + }, + { + "id": "locked", + "translation": "locked", + "modified": false + }, + { + "id": "memory", + "translation": "memory", + "modified": false + }, + { + "id": "memory:", + "translation": "memory:", + "modified": false + }, + { + "id": "name", + "translation": "name", + "modified": false + }, + { + "id": "non basic services", + "translation": "non basic services", + "modified": false + }, + { + "id": "none", + "translation": "none", + "modified": false + }, + { + "id": "not valid for the requested host", + "translation": "not valid for the requested host", + "modified": false + }, + { + "id": "org", + "translation": "org", + "modified": false + }, + { + "id": "organization", + "translation": "organization", + "modified": false + }, + { + "id": "orgs", + "translation": "orgs", + "modified": false + }, + { + "id": "owned", + "translation": "owned", + "modified": false + }, + { + "id": "paid service plans", + "translation": "paid service plans", + "modified": false + }, + { + "id": "plan", + "translation": "plan", + "modified": false + }, + { + "id": "plans", + "translation": "plans", + "modified": false + }, + { + "id": "plans accessible by a particular organization", + "translation": "plans accessible by a particular organization", + "modified": false + }, + { + "id": "position", + "translation": "position", + "modified": false + }, + { + "id": "provider", + "translation": "provider", + "modified": false + }, + { + "id": "quota:", + "translation": "quota:", + "modified": true + }, + { + "id": "requested state", + "translation": "requested state", + "modified": false + }, + { + "id": "requested state:", + "translation": "requested state:", + "modified": false + }, + { + "id": "routes", + "translation": "routes", + "modified": false + }, + { + "id": "running", + "translation": "running", + "modified": false + }, + { + "id": "security group", + "translation": "security group", + "modified": false + }, + { + "id": "service", + "translation": "service", + "modified": false + }, + { + "id": "service auth token", + "translation": "service auth token", + "modified": false + }, + { + "id": "service instance", + "translation": "service instance", + "modified": false + }, + { + "id": "service instances", + "translation": "service instances", + "modified": false + }, + { + "id": "service plan", + "translation": "service plan", + "modified": false + }, + { + "id": "service-broker", + "translation": "service-broker", + "modified": false + }, + { + "id": "services", + "translation": "services", + "modified": false + }, + { + "id": "shared", + "translation": "shared", + "modified": false + }, + { + "id": "since", + "translation": "since", + "modified": false + }, + { + "id": "space", + "translation": "space", + "modified": false + }, + { + "id": "space quotas:", + "translation": "space quotas:", + "modified": false + }, + { + "id": "spaces:", + "translation": "spaces:", + "modified": true + }, + { + "id": "starting", + "translation": "starting", + "modified": false + }, + { + "id": "state", + "translation": "state", + "modified": false + }, + { + "id": "status", + "translation": "status", + "modified": false + }, + { + "id": "stopped", + "translation": "stopped", + "modified": false + }, + { + "id": "stopped after 1 redirect", + "translation": "stopped after 1 redirect", + "modified": false + }, + { + "id": "time", + "translation": "time", + "modified": false + }, + { + "id": "total memory limit", + "translation": "memory limit", + "modified": true + }, + { + "id": "unknown authority", + "translation": "unknown authority", + "modified": false + }, + { + "id": "unlimited", + "translation": "unlimited", + "modified": false + }, + { + "id": "update an existing space quota", + "translation": "update an existing space quota", + "modified": false + }, + { + "id": "url", + "translation": "url", + "modified": false + }, + { + "id": "urls", + "translation": "urls", + "modified": false + }, + { + "id": "urls:", + "translation": "urls:", + "modified": false + }, + { + "id": "usage:", + "translation": "usage:", + "modified": false + }, + { + "id": "user", + "translation": "user", + "modified": false + }, + { + "id": "user-provided", + "translation": "user-provided", + "modified": false + }, + { + "id": "write default values to the config", + "translation": "write default values to the config", + "modified": false + }, + { + "id": "yes", + "translation": "yes", + "modified": false + }, + { + "id": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "translation": "{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + "modified": false + }, + { + "id": "{{.CFName}} api", + "translation": "{{.CFName}} api", + "modified": false + }, + { + "id": "{{.CFName}} login", + "translation": "{{.CFName}} login", + "modified": false + }, + { + "id": "{{.Command}} help [COMMAND]", + "translation": "{{.Command}} help [COMMAND]", + "modified": false + }, + { + "id": "{{.CountOfServices}} migrated.", + "translation": "{{.CountOfServices}} migrated.", + "modified": false + }, + { + "id": "{{.DiskUsage}} of {{.DiskQuota}}", + "translation": "{{.DiskUsage}} of {{.DiskQuota}}", + "modified": false + }, + { + "id": "{{.DownCount}} down", + "translation": "{{.DownCount}} down", + "modified": false + }, + { + "id": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "translation": "{{.ErrorDescription}}\nTIP: Use '{{.CFServicesCommand}}' to view all services in this org and space.", + "modified": false + }, + { + "id": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "translation": "{{.Err}}\n\nTIP: use '{{.Command}}' for more information", + "modified": false + }, + { + "id": "{{.FlappingCount}} failing", + "translation": "{{.FlappingCount}} failing", + "modified": false + }, + { + "id": "{{.MemUsage}} of {{.MemQuota}}", + "translation": "{{.MemUsage}} of {{.MemQuota}}", + "modified": false + }, + { + "id": "{{.ModelType}} {{.ModelName}} already exists", + "translation": "{{.ModelType}} {{.ModelName}} already exists", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string or null value", + "translation": "{{.PropertyName}} must be a string or null value", + "modified": false + }, + { + "id": "{{.PropertyName}} must be a string value", + "translation": "{{.PropertyName}} must be a string value", + "modified": false + }, + { + "id": "{{.PropertyName}} should not be null", + "translation": "{{.PropertyName}} should not be null", + "modified": false + }, + { + "id": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "translation": "{{.QuotaName}} ({{.MemoryLimit}}M memory limit, {{.RoutesLimit}} routes, {{.ServicesLimit}} services, paid services {{.NonBasicServicesAllowed}})", + "modified": false + }, + { + "id": "{{.RunningCount}} of {{.TotalCount}} instances running", + "translation": "{{.RunningCount}} of {{.TotalCount}} instances running", + "modified": false + }, + { + "id": "{{.StartingCount}} starting", + "translation": "{{.StartingCount}} starting", + "modified": false + }, + { + "id": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "translation": "{{.Usage}} {{.FormattedMemory}} x {{.InstanceCount}} instances", + "modified": false + } +] \ No newline at end of file diff --git a/cf/i18n/test_fixtures/en_US.all.json b/cf/i18n/test_fixtures/en_US.all.json new file mode 100644 index 00000000000..04f1fc95ea1 --- /dev/null +++ b/cf/i18n/test_fixtures/en_US.all.json @@ -0,0 +1,22 @@ +[ + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "No buildpacks found", + "modified": false + }, + { + "id": "Change user password", + "translation": "Change user password", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + } +] diff --git a/cf/i18n/test_fixtures/fr_FR.all.json b/cf/i18n/test_fixtures/fr_FR.all.json new file mode 100644 index 00000000000..f309d3a17fe --- /dev/null +++ b/cf/i18n/test_fixtures/fr_FR.all.json @@ -0,0 +1,22 @@ +[ + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "Pas buildpacks trouvés", + "modified": false + }, + { + "id": "Change user password", + "translation": "Changer mot de passe de l'utilisateur", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Suppression domaine {{.DomainName}} comme {{.Username}}...", + "modified": false + } +] diff --git a/cf/i18n/test_fixtures/zh_Hans.all.json b/cf/i18n/test_fixtures/zh_Hans.all.json new file mode 100644 index 00000000000..547d65b099f --- /dev/null +++ b/cf/i18n/test_fixtures/zh_Hans.all.json @@ -0,0 +1,22 @@ +[ + { + "id": "Deletes a security group", + "translation": "Deletes a security group", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "buildpack未找到", + "modified": false + }, + { + "id": "Change user password", + "translation": "更改用户密码", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + } +] diff --git a/cf/i18n/test_fixtures/zh_Hant.all.json b/cf/i18n/test_fixtures/zh_Hant.all.json new file mode 100644 index 00000000000..a9d92908809 --- /dev/null +++ b/cf/i18n/test_fixtures/zh_Hant.all.json @@ -0,0 +1,22 @@ +[ + { + "id": "Deletes a security group", + "translation": "(Hant)Deletes a security group", + "modified": false + }, + { + "id": "No buildpacks found", + "translation": "(Hant)No buildpacks found", + "modified": false + }, + { + "id": "Change user password", + "translation": "(Hant)Change user password", + "modified": false + }, + { + "id": "Deleting domain {{.DomainName}} as {{.Username}}...", + "translation": "(Hant)Deleting domain {{.DomainName}} as {{.Username}}...", + "modified": false + } +] diff --git a/cf/manifest/example_manifest.yml b/cf/manifest/example_manifest.yml new file mode 100644 index 00000000000..abd1459734f --- /dev/null +++ b/cf/manifest/example_manifest.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app-name + memory: 128M + instances: 1 + host: hello + domain: app.example.com + path: path/to/app diff --git a/cf/manifest/manifest.go b/cf/manifest/manifest.go new file mode 100644 index 00000000000..b4b4781b7a4 --- /dev/null +++ b/cf/manifest/manifest.go @@ -0,0 +1,363 @@ +package manifest + +import ( + "fmt" + "path/filepath" + "regexp" + "strconv" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/generic" + "github.com/cloudfoundry/cli/words/generator" +) + +type Manifest struct { + Path string + Data generic.Map +} + +func NewEmptyManifest() (m *Manifest) { + return &Manifest{Data: generic.NewMap()} +} + +func (m Manifest) Applications() (apps []models.AppParams, err error) { + rawData, errs := expandProperties(m.Data, generator.NewWordGenerator()) + if len(errs) > 0 { + err = errors.NewWithSlice(errs) + return + } + + data := generic.NewMap(rawData) + appMaps, errs := m.getAppMaps(data) + if len(errs) > 0 { + err = errors.NewWithSlice(errs) + return + } + + for _, appMap := range appMaps { + app, errs := mapToAppParams(filepath.Dir(m.Path), appMap) + if len(errs) > 0 { + err = errors.NewWithSlice(errs) + continue + } + + apps = append(apps, app) + } + + return +} + +func (m Manifest) getAppMaps(data generic.Map) (apps []generic.Map, errs []error) { + globalProperties := data.Except([]interface{}{"applications"}) + + if data.Has("applications") { + appMaps, ok := data.Get("applications").([]interface{}) + if !ok { + errs = append(errs, errors.New(T("Expected applications to be a list"))) + return + } + + for _, appData := range appMaps { + if !generic.IsMappable(appData) { + errs = append(errs, errors.NewWithFmt(T("Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'", + map[string]interface{}{"YmlSnippet": appData}))) + continue + } + + appMap := generic.DeepMerge(globalProperties, generic.NewMap(appData)) + apps = append(apps, appMap) + } + } else { + apps = append(apps, globalProperties) + } + + return +} + +var propertyRegex = regexp.MustCompile(`\${[\w-]+}`) + +func expandProperties(input interface{}, babbler generator.WordGenerator) (output interface{}, errs []error) { + switch input := input.(type) { + case string: + match := propertyRegex.FindStringSubmatch(input) + if match != nil { + if match[0] == "${random-word}" { + output = strings.Replace(input, "${random-word}", strings.ToLower(babbler.Babble()), -1) + } else { + err := errors.NewWithFmt(T("Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.", + map[string]interface{}{"PropertyName": match[0]})) + errs = append(errs, err) + } + } else { + output = input + } + case []interface{}: + outputSlice := make([]interface{}, len(input)) + for index, item := range input { + itemOutput, itemErrs := expandProperties(item, babbler) + outputSlice[index] = itemOutput + errs = append(errs, itemErrs...) + } + output = outputSlice + case map[interface{}]interface{}: + outputMap := make(map[interface{}]interface{}) + for key, value := range input { + itemOutput, itemErrs := expandProperties(value, babbler) + outputMap[key] = itemOutput + errs = append(errs, itemErrs...) + } + output = outputMap + case generic.Map: + outputMap := generic.NewMap() + generic.Each(input, func(key, value interface{}) { + itemOutput, itemErrs := expandProperties(value, babbler) + outputMap.Set(key, itemOutput) + errs = append(errs, itemErrs...) + }) + output = outputMap + default: + output = input + } + + return +} + +func mapToAppParams(basePath string, yamlMap generic.Map) (appParams models.AppParams, errs []error) { + errs = checkForNulls(yamlMap) + if len(errs) > 0 { + return + } + + appParams.BuildpackUrl = stringValOrDefault(yamlMap, "buildpack", &errs) + appParams.DiskQuota = bytesVal(yamlMap, "disk_quota", &errs) + appParams.Domain = stringVal(yamlMap, "domain", &errs) + appParams.Host = stringVal(yamlMap, "host", &errs) + appParams.Name = stringVal(yamlMap, "name", &errs) + appParams.Path = stringVal(yamlMap, "path", &errs) + appParams.StackName = stringVal(yamlMap, "stack", &errs) + appParams.Command = stringValOrDefault(yamlMap, "command", &errs) + appParams.Memory = bytesVal(yamlMap, "memory", &errs) + appParams.InstanceCount = intVal(yamlMap, "instances", &errs) + appParams.HealthCheckTimeout = intVal(yamlMap, "timeout", &errs) + appParams.NoRoute = boolVal(yamlMap, "no-route", &errs) + appParams.UseRandomHostname = boolVal(yamlMap, "random-route", &errs) + appParams.ServicesToBind = sliceOrEmptyVal(yamlMap, "services", &errs) + appParams.EnvironmentVars = envVarOrEmptyMap(yamlMap, &errs) + + if appParams.Path != nil { + path := *appParams.Path + if filepath.IsAbs(path) { + path = filepath.Clean(path) + } else { + path = filepath.Join(basePath, path) + } + appParams.Path = &path + } + + return +} + +func checkForNulls(yamlMap generic.Map) (errs []error) { + generic.Each(yamlMap, func(key interface{}, value interface{}) { + if key == "command" || key == "buildpack" { + return + } + if value == nil { + errs = append(errs, errors.NewWithFmt(T("{{.PropertyName}} should not be null", map[string]interface{}{"PropertyName": key}))) + } + }) + + return +} + +func stringVal(yamlMap generic.Map, key string, errs *[]error) *string { + val := yamlMap.Get(key) + if val == nil { + return nil + } + result, ok := val.(string) + if !ok { + *errs = append(*errs, errors.NewWithFmt(T("{{.PropertyName}} must be a string value", map[string]interface{}{"PropertyName": key}))) + return nil + } + return &result +} + +func stringValOrDefault(yamlMap generic.Map, key string, errs *[]error) *string { + if !yamlMap.Has(key) { + return nil + } + empty := "" + switch val := yamlMap.Get(key).(type) { + case string: + if val == "default" { + return &empty + } else { + return &val + } + case nil: + return &empty + default: + *errs = append(*errs, errors.NewWithFmt(T("{{.PropertyName}} must be a string or null value", map[string]interface{}{"PropertyName": key}))) + return nil + } +} + +func bytesVal(yamlMap generic.Map, key string, errs *[]error) *int64 { + yamlVal := yamlMap.Get(key) + if yamlVal == nil { + return nil + } + + stringVal := coerceToString(yamlVal) + value, err := formatters.ToMegabytes(stringVal) + if err != nil { + *errs = append(*errs, errors.NewWithFmt(T("Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}", + map[string]interface{}{ + "PropertyName": key, + "Error": err.Error(), + "StringVal": stringVal, + }))) + return nil + } + return &value +} + +func intVal(yamlMap generic.Map, key string, errs *[]error) *int { + var ( + intVal int + err error + ) + + switch val := yamlMap.Get(key).(type) { + case string: + intVal, err = strconv.Atoi(val) + case int: + intVal = val + case int64: + intVal = int(val) + case nil: + return nil + default: + err = errors.NewWithFmt(T("Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.", + map[string]interface{}{"PropertyName": key, "PropertyType": val})) + } + + if err != nil { + *errs = append(*errs, err) + return nil + } + + return &intVal +} + +func coerceToString(value interface{}) string { + return fmt.Sprintf("%v", value) +} + +func boolVal(yamlMap generic.Map, key string, errs *[]error) bool { + switch val := yamlMap.Get(key).(type) { + case nil: + return false + case bool: + return val + case string: + return val == "true" + default: + *errs = append(*errs, errors.NewWithFmt(T("Expected {{.PropertyName}} to be a boolean.", map[string]interface{}{"PropertyName": key}))) + return false + } +} + +func sliceOrEmptyVal(yamlMap generic.Map, key string, errs *[]error) *[]string { + if !yamlMap.Has(key) { + return new([]string) + } + + var ( + stringSlice []string + err error + ) + + sliceErr := errors.NewWithFmt(T("Expected {{.PropertyName}} to be a list of strings.", + map[string]interface{}{"PropertyName": key})) + + switch input := yamlMap.Get(key).(type) { + case []interface{}: + for _, value := range input { + stringValue, ok := value.(string) + if !ok { + err = sliceErr + break + } + stringSlice = append(stringSlice, stringValue) + } + default: + err = sliceErr + } + + if err != nil { + *errs = append(*errs, err) + return nil + } + + return &stringSlice +} + +func envVarOrEmptyMap(yamlMap generic.Map, errs *[]error) *map[string]interface{} { + key := "env" + switch envVars := yamlMap.Get(key).(type) { + case nil: + aMap := make(map[string]interface{}, 0) + return &aMap + case map[string]interface{}: + yamlMap.Set(key, generic.NewMap(yamlMap.Get(key))) + return envVarOrEmptyMap(yamlMap, errs) + case map[interface{}]interface{}: + yamlMap.Set(key, generic.NewMap(yamlMap.Get(key))) + return envVarOrEmptyMap(yamlMap, errs) + case generic.Map: + merrs := validateEnvVars(envVars) + if merrs != nil { + *errs = append(*errs, merrs...) + return nil + } + + result := make(map[string]interface{}, envVars.Count()) + generic.Each(envVars, func(key, value interface{}) { + result[key.(string)] = value + // switch value.(type) { + // case string: + // result[key.(string)] = value.(string) + // case int64, int, int32: + // result[key.(string)] = fmt.Sprintf("%d", value) + // case float32, float64: + // result[key.(string)] = fmt.Sprintf("%f", value) + // default: + // *errs = append(*errs, errors.NewWithFmt(T("Expected environment variable {{.PropertyName}} to have a string value, but it was a {{.PropertyType}}.", + // map[string]interface{}{"PropertyName": key, "PropertyType": value}))) + // } + + }) + return &result + default: + *errs = append(*errs, errors.NewWithFmt(T("Expected {{.Name}} to be a set of key => value, but it was a {{.Type}}.", + map[string]interface{}{"Name": key, "Type": envVars}))) + return nil + } +} + +func validateEnvVars(input generic.Map) (errs []error) { + generic.Each(input, func(key, value interface{}) { + if value == nil { + errs = append(errs, errors.New(fmt.Sprintf(T("env var '{{.PropertyName}}' should not be null", + map[string]interface{}{"PropertyName": key})))) + } + }) + return +} diff --git a/cf/manifest/manifest_disk_repository.go b/cf/manifest/manifest_disk_repository.go new file mode 100644 index 00000000000..90d5ee9ebe5 --- /dev/null +++ b/cf/manifest/manifest_disk_repository.go @@ -0,0 +1,116 @@ +package manifest + +import ( + "github.com/cloudfoundry-incubator/candiedyaml" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/generic" + "io" + "os" + "path/filepath" +) + +type ManifestRepository interface { + ReadManifest(string) (*Manifest, error) +} + +type ManifestDiskRepository struct{} + +func NewManifestDiskRepository() (repo ManifestRepository) { + return ManifestDiskRepository{} +} + +func (repo ManifestDiskRepository) ReadManifest(inputPath string) (*Manifest, error) { + m := NewEmptyManifest() + manifestPath, err := repo.manifestPath(inputPath) + + if err != nil { + return m, errors.NewWithError(T("Error finding manifest"), err) + } + + m.Path = manifestPath + + mapp, err := repo.readAllYAMLFiles(manifestPath) + if err != nil { + return m, err + } + + m.Data = mapp + + return m, nil +} + +func (repo ManifestDiskRepository) readAllYAMLFiles(path string) (mergedMap generic.Map, err error) { + file, err := os.Open(filepath.Clean(path)) + if err != nil { + return + } + defer file.Close() + + mapp, err := parseManifest(file) + if err != nil { + return + } + + if !mapp.Has("inherit") { + mergedMap = mapp + return + } + + inheritedPath, ok := mapp.Get("inherit").(string) + if !ok { + err = errors.New(T("invalid inherit path in manifest")) + return + } + + if !filepath.IsAbs(inheritedPath) { + inheritedPath = filepath.Join(filepath.Dir(path), inheritedPath) + } + + inheritedMap, err := repo.readAllYAMLFiles(inheritedPath) + if err != nil { + return + } + + mergedMap = generic.DeepMerge(inheritedMap, mapp) + return +} + +func parseManifest(file io.Reader) (yamlMap generic.Map, err error) { + decoder := candiedyaml.NewDecoder(file) + yamlMap = generic.NewMap() + err = decoder.Decode(yamlMap) + if err != nil { + return + } + + if !generic.IsMappable(yamlMap) { + err = errors.New(T("Invalid manifest. Expected a map")) + return + } + + return +} + +func (repo ManifestDiskRepository) manifestPath(userSpecifiedPath string) (string, error) { + fileInfo, err := os.Stat(userSpecifiedPath) + if err != nil { + return "", err + } + + if fileInfo.IsDir() { + manifestPaths := []string{ + filepath.Join(userSpecifiedPath, "manifest.yml"), + filepath.Join(userSpecifiedPath, "manifest.yaml"), + } + var err error + for _, manifestPath := range manifestPaths { + if _, err = os.Stat(manifestPath); err == nil { + return manifestPath, err + } + } + return "", err + } else { + return userSpecifiedPath, nil + } +} diff --git a/cf/manifest/manifest_disk_repository_test.go b/cf/manifest/manifest_disk_repository_test.go new file mode 100644 index 00000000000..05209715c17 --- /dev/null +++ b/cf/manifest/manifest_disk_repository_test.go @@ -0,0 +1,156 @@ +package manifest_test + +import ( + "path/filepath" + + . "github.com/cloudfoundry/cli/cf/manifest" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ManifestDiskRepository", func() { + var repo ManifestRepository + + BeforeEach(func() { + repo = NewManifestDiskRepository() + }) + + Describe("given a directory containing a file called 'manifest.yml'", func() { + It("reads that file", func() { + m, err := repo.ReadManifest("../../fixtures/manifests") + + Expect(err).NotTo(HaveOccurred()) + Expect(m.Path).To(Equal(filepath.Clean("../../fixtures/manifests/manifest.yml"))) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].Name).To(Equal("from-default-manifest")) + }) + }) + + Describe("given a directory that doesn't contain a file called 'manifest.y{a}ml'", func() { + It("returns an error", func() { + m, err := repo.ReadManifest("../../fixtures") + + Expect(err).To(HaveOccurred()) + Expect(m.Path).To(BeEmpty()) + }) + }) + + Describe("given a directory that contains a file called 'manifest.yaml'", func() { + It("reads that file", func() { + m, err := repo.ReadManifest("../../fixtures/manifests/only_yaml") + + Expect(err).NotTo(HaveOccurred()) + Expect(m.Path).To(Equal(filepath.Clean("../../fixtures/manifests/only_yaml/manifest.yaml"))) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].Name).To(Equal("from-default-manifest")) + }) + }) + + Describe("given a directory contains files called 'manifest.yml' and 'manifest.yaml'", func() { + It("reads the file named 'manifest.yml'", func() { + m, err := repo.ReadManifest("../../fixtures/manifests/both_yaml_yml") + + Expect(err).NotTo(HaveOccurred()) + Expect(m.Path).To(Equal(filepath.Clean("../../fixtures/manifests/both_yaml_yml/manifest.yml"))) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].Name).To(Equal("yml-extension")) + }) + }) + + Describe("given a path to a file", func() { + var ( + inputPath string + m *Manifest + err error + ) + + BeforeEach(func() { + inputPath = filepath.Clean("../../fixtures/manifests/different-manifest.yml") + m, err = repo.ReadManifest(inputPath) + }) + + It("reads the file at that path", func() { + Expect(err).NotTo(HaveOccurred()) + Expect(m.Path).To(Equal(inputPath)) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].Name).To(Equal("from-different-manifest")) + }) + + It("passes the base directory to the manifest file", func() { + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(len(applications)).To(Equal(1)) + Expect(*applications[0].Name).To(Equal("from-different-manifest")) + + appPath := filepath.Clean("../../fixtures/manifests") + Expect(*applications[0].Path).To(Equal(appPath)) + }) + }) + + Describe("given a path to a file that doesn't exist", func() { + It("returns an error", func() { + _, err := repo.ReadManifest("some/path/that/doesnt/exist/manifest.yml") + Expect(err).To(HaveOccurred()) + }) + + It("returns empty string for the manifest path", func() { + m, _ := repo.ReadManifest("some/path/that/doesnt/exist/manifest.yml") + Expect(m.Path).To(Equal("")) + }) + }) + + Describe("when the manifest is not valid", func() { + It("returns an error", func() { + _, err := repo.ReadManifest("../../fixtures/manifests/empty-manifest.yml") + Expect(err).To(HaveOccurred()) + }) + + It("returns the path to the manifest", func() { + inputPath := filepath.Clean("../../fixtures/manifests/empty-manifest.yml") + m, _ := repo.ReadManifest(inputPath) + Expect(m.Path).To(Equal(inputPath)) + }) + }) + + It("converts nested maps to generic maps", func() { + m, err := repo.ReadManifest("../../fixtures/manifests/different-manifest.yml") + Expect(err).NotTo(HaveOccurred()) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].EnvironmentVars).To(Equal(map[string]interface{}{ + "LD_LIBRARY_PATH": "/usr/lib/somewhere", + })) + }) + + It("merges manifests with their 'inherited' manifests", func() { + m, err := repo.ReadManifest("../../fixtures/manifests/inherited-manifest.yml") + Expect(err).NotTo(HaveOccurred()) + + applications, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*applications[0].Name).To(Equal("base-app")) + Expect(*applications[0].ServicesToBind).To(Equal([]string{"base-service"})) + Expect(*applications[0].EnvironmentVars).To(Equal(map[string]interface{}{ + "foo": "bar", + "will-be-overridden": "my-value", + })) + + Expect(*applications[1].Name).To(Equal("my-app")) + + env := *applications[1].EnvironmentVars + Expect(env["will-be-overridden"]).To(Equal("my-value")) + Expect(env["foo"]).To(Equal("bar")) + + services := *applications[1].ServicesToBind + Expect(services).To(Equal([]string{"base-service", "foo-service"})) + }) +}) diff --git a/cf/manifest/manifest_suite_test.go b/cf/manifest/manifest_suite_test.go new file mode 100644 index 00000000000..d880d7f5919 --- /dev/null +++ b/cf/manifest/manifest_suite_test.go @@ -0,0 +1,19 @@ +package manifest_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestManifest(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Manifest Suite") +} diff --git a/cf/manifest/manifest_test.go b/cf/manifest/manifest_test.go new file mode 100644 index 00000000000..a616901fcde --- /dev/null +++ b/cf/manifest/manifest_test.go @@ -0,0 +1,413 @@ +package manifest_test + +import ( + "runtime" + "strings" + + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/generic" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +func NewManifest(path string, data generic.Map) (m *manifest.Manifest) { + return &manifest.Manifest{Path: path, Data: data} +} + +var _ = Describe("Manifests", func() { + It("merges global properties into each app's properties", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "instances": "3", + "memory": "512M", + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bitcoin-miner", + "no-route": true, + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + + Expect(*apps[0].InstanceCount).To(Equal(3)) + Expect(*apps[0].Memory).To(Equal(int64(512))) + Expect(apps[0].NoRoute).To(BeTrue()) + }) + + Describe("when there is no applications block", func() { + It("returns a single application with the global properties", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "instances": "3", + "memory": "512M", + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(apps)).To(Equal(1)) + Expect(*apps[0].InstanceCount).To(Equal(3)) + Expect(*apps[0].Memory).To(Equal(int64(512))) + }) + }) + + It("returns an error when the memory limit doesn't have a unit", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "instances": "3", + "memory": "512", + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bitcoin-miner", + }, + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Invalid value for 'memory': 512")) + }) + + //candiedyaml returns an integer value when no unit is provided + It("returns an error when the memory limit is a non-string", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "instances": "3", + "memory": 128, + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bitcoin-miner", + }, + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Invalid value for 'memory': 128")) + }) + + It("sets applications' health check timeouts", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bitcoin-miner", + "timeout": "360", + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].HealthCheckTimeout).To(Equal(360)) + }) + + It("allows boolean env var values", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "env": generic.NewMap(map[interface{}]interface{}{ + "bar": true, + }), + })) + + _, err := m.Applications() + Expect(err).ToNot(HaveOccurred()) + }) + + It("does not allow nil values for environment variables", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "env": generic.NewMap(map[interface{}]interface{}{ + "bar": nil, + }), + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bad app", + }, + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("env var 'bar' should not be null")) + }) + + It("returns an empty map when no env was present in the manifest", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{"name": "no-env-vars"}, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].EnvironmentVars).NotTo(BeNil()) + }) + + It("allows applications to have absolute paths", func() { + if runtime.GOOS == "windows" { + m := NewManifest(`C:\some\path\manifest.yml`, generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "path": `C:\another\path`, + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].Path).To(Equal(`C:\another\path`)) + } else { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "path": "/another/path-segment", + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].Path).To(Equal("/another/path-segment")) + } + }) + + It("expands relative app paths based on the manifest's path", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "path": "../another/path-segment", + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + if runtime.GOOS == "windows" { + Expect(*apps[0].Path).To(Equal("\\some\\another\\path-segment")) + } else { + Expect(*apps[0].Path).To(Equal("/some/another/path-segment")) + } + }) + + It("returns errors when there are null values", func() { + m := NewManifest("/some/path", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "disk_quota": nil, + "domain": nil, + "host": nil, + "name": nil, + "path": nil, + "stack": nil, + "memory": nil, + "instances": nil, + "timeout": nil, + "no-route": nil, + "services": nil, + "env": nil, + "random-route": nil, + }, + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + errorSlice := strings.Split(err.Error(), "\n") + manifestKeys := []string{"disk_quota", "domain", "host", "name", "path", "stack", + "memory", "instances", "timeout", "no-route", "services", "env", "random-route"} + + for _, key := range manifestKeys { + Expect(errorSlice).To(ContainSubstrings([]string{key, "not be null"})) + } + }) + + It("parses known manifest keys", func() { + m := NewManifest("/some/path", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{ + "buildpack": "my-buildpack", + "disk_quota": "512M", + "domain": "my-domain", + "host": "my-hostname", + "name": "my-app-name", + "stack": "my-stack", + "memory": "256M", + "instances": 1, + "timeout": 11, + "no-route": true, + "random-route": true, + }, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(len(apps)).To(Equal(1)) + + Expect(*apps[0].BuildpackUrl).To(Equal("my-buildpack")) + Expect(*apps[0].DiskQuota).To(Equal(int64(512))) + Expect(*apps[0].Domain).To(Equal("my-domain")) + Expect(*apps[0].Host).To(Equal("my-hostname")) + Expect(*apps[0].Name).To(Equal("my-app-name")) + Expect(*apps[0].StackName).To(Equal("my-stack")) + Expect(*apps[0].Memory).To(Equal(int64(256))) + Expect(*apps[0].InstanceCount).To(Equal(1)) + Expect(*apps[0].HealthCheckTimeout).To(Equal(11)) + Expect(apps[0].NoRoute).To(BeTrue()) + Expect(apps[0].UseRandomHostname).To(BeTrue()) + }) + + Describe("old-style property syntax", func() { + It("returns an error when the manifest contains non-whitelist properties", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "env": generic.NewMap(map[interface{}]interface{}{ + "bar": "many-${some_property-name}-are-cool", + }), + }), + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("'${some_property-name}'")) + }) + + It("replaces the '${random-word} with a combination of 2 random words", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "env": generic.NewMap(map[interface{}]interface{}{ + "bar": "prefix_${random-word}_suffix", + "foo": "some-value", + }), + }), + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect((*apps[0].EnvironmentVars)["bar"]).To(MatchRegexp(`prefix_\w+-\w+_suffix`)) + Expect((*apps[0].EnvironmentVars)["foo"]).To(Equal("some-value")) + + apps2, _ := m.Applications() + Expect((*apps2[0].EnvironmentVars)["bar"]).To(MatchRegexp(`prefix_\w+-\w+_suffix`)) + Expect((*apps2[0].EnvironmentVars)["bar"]).NotTo(Equal((*apps[0].EnvironmentVars)["bar"])) + }) + }) + + It("sets the command and buildpack to blank when their values are null in the manifest", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "buildpack": nil, + "command": nil, + }), + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].Command).To(Equal("")) + Expect(*apps[0].BuildpackUrl).To(Equal("")) + }) + + It("sets the command and buildpack to blank when their values are 'default' in the manifest", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "command": "default", + "buildpack": "default", + }), + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(*apps[0].Command).To(Equal("")) + Expect(*apps[0].BuildpackUrl).To(Equal("")) + }) + + It("does not set the start command when the manifest doesn't have the 'command' key", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + map[interface{}]interface{}{}, + }, + })) + + apps, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(apps[0].Command).To(BeNil()) + }) + + It("can build the applications multiple times", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "memory": "254m", + "applications": []interface{}{ + map[interface{}]interface{}{ + "name": "bitcoin-miner", + }, + map[interface{}]interface{}{ + "name": "bitcoin-miner", + }, + }, + })) + + apps1, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + + apps2, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + Expect(apps1).To(Equal(apps2)) + }) + + Describe("parsing env vars", func() { + It("handles values that are not strings", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "env": map[interface{}]interface{}{ + "string-key": "value", + "int-key": 1, + "float-key": 11.1, + }, + }), + }, + })) + + app, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + + Expect((*app[0].EnvironmentVars)["string-key"]).To(Equal("value")) + Expect((*app[0].EnvironmentVars)["int-key"]).To(Equal(1)) + Expect((*app[0].EnvironmentVars)["float-key"]).To(Equal(11.1)) + }) + + XIt("handles values that cannot be converted to strings", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "applications": []interface{}{ + generic.NewMap(map[interface{}]interface{}{ + "env": map[interface{}]interface{}{ + "bad-key": map[interface{}]interface{}{}, + }, + }), + }, + })) + + _, err := m.Applications() + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("parsing services", func() { + It("can read a list of service instance names", func() { + m := NewManifest("/some/path/manifest.yml", generic.NewMap(map[interface{}]interface{}{ + "services": []interface{}{"service-1", "service-2"}, + })) + + app, err := m.Applications() + Expect(err).NotTo(HaveOccurred()) + + Expect(*app[0].ServicesToBind).To(Equal([]string{"service-1", "service-2"})) + }) + }) +}) diff --git a/cf/models/app_event.go b/cf/models/app_event.go new file mode 100644 index 00000000000..9bfa7079376 --- /dev/null +++ b/cf/models/app_event.go @@ -0,0 +1,11 @@ +package models + +import "time" + +type EventFields struct { + Guid string + Name string + Timestamp time.Time + Description string + ActorName string +} diff --git a/cf/models/app_file.go b/cf/models/app_file.go new file mode 100644 index 00000000000..34544a7d558 --- /dev/null +++ b/cf/models/app_file.go @@ -0,0 +1,7 @@ +package models + +type AppFileFields struct { + Path string + Sha1 string + Size int64 +} diff --git a/cf/models/app_instance.go b/cf/models/app_instance.go new file mode 100644 index 00000000000..9faa2bbaadf --- /dev/null +++ b/cf/models/app_instance.go @@ -0,0 +1,22 @@ +package models + +import "time" + +type InstanceState string + +const ( + InstanceStarting InstanceState = "starting" + InstanceRunning InstanceState = "running" + InstanceFlapping InstanceState = "flapping" + InstanceDown InstanceState = "down" +) + +type AppInstanceFields struct { + State InstanceState + Since time.Time + CpuUsage float64 // percentage + DiskQuota int64 // in bytes + DiskUsage int64 + MemQuota int64 + MemUsage int64 +} diff --git a/cf/models/application.go b/cf/models/application.go new file mode 100644 index 00000000000..ac63b97c808 --- /dev/null +++ b/cf/models/application.go @@ -0,0 +1,146 @@ +package models + +import ( + "reflect" + "strings" + "time" +) + +type Application struct { + ApplicationFields + Stack *Stack + Routes []RouteSummary +} + +func (model Application) HasRoute(route Route) bool { + for _, boundRoute := range model.Routes { + if boundRoute.Guid == route.Guid { + return true + } + } + return false +} + +func (model Application) ToParams() (params AppParams) { + state := strings.ToUpper(model.State) + params = AppParams{ + Guid: &model.Guid, + Name: &model.Name, + BuildpackUrl: &model.BuildpackUrl, + Command: &model.Command, + DiskQuota: &model.DiskQuota, + InstanceCount: &model.InstanceCount, + Memory: &model.Memory, + State: &state, + SpaceGuid: &model.SpaceGuid, + EnvironmentVars: &model.EnvironmentVars, + } + + if model.Stack != nil { + params.StackGuid = &model.Stack.Guid + } + + return +} + +type ApplicationFields struct { + Guid string + Name string + BuildpackUrl string + Command string + DetectedStartCommand string + DiskQuota int64 // in Megabytes + EnvironmentVars map[string]interface{} + InstanceCount int + Memory int64 // in Megabytes + RunningInstances int + State string + SpaceGuid string + PackageUpdatedAt *time.Time +} + +type AppParams struct { + BuildpackUrl *string + Command *string + DiskQuota *int64 + Domain *string + EnvironmentVars *map[string]interface{} + Guid *string + HealthCheckTimeout *int + Host *string + InstanceCount *int + Memory *int64 + Name *string + NoRoute bool + UseRandomHostname bool + Path *string + ServicesToBind *[]string + SpaceGuid *string + StackGuid *string + StackName *string + State *string +} + +func (app *AppParams) Merge(other *AppParams) { + if other.BuildpackUrl != nil { + app.BuildpackUrl = other.BuildpackUrl + } + if other.Command != nil { + app.Command = other.Command + } + if other.DiskQuota != nil { + app.DiskQuota = other.DiskQuota + } + if other.Domain != nil { + app.Domain = other.Domain + } + if other.EnvironmentVars != nil { + app.EnvironmentVars = other.EnvironmentVars + } + if other.Guid != nil { + app.Guid = other.Guid + } + if other.HealthCheckTimeout != nil { + app.HealthCheckTimeout = other.HealthCheckTimeout + } + if other.Host != nil { + app.Host = other.Host + } + if other.InstanceCount != nil { + app.InstanceCount = other.InstanceCount + } + if other.DiskQuota != nil { + app.DiskQuota = other.DiskQuota + } + if other.Memory != nil { + app.Memory = other.Memory + } + if other.Name != nil { + app.Name = other.Name + } + if other.Path != nil { + app.Path = other.Path + } + if other.ServicesToBind != nil { + app.ServicesToBind = other.ServicesToBind + } + if other.SpaceGuid != nil { + app.SpaceGuid = other.SpaceGuid + } + if other.StackGuid != nil { + app.StackGuid = other.StackGuid + } + if other.StackName != nil { + app.StackName = other.StackName + } + if other.State != nil { + app.State = other.State + } + + app.NoRoute = app.NoRoute || other.NoRoute + app.UseRandomHostname = app.UseRandomHostname || other.UseRandomHostname +} + +func (app *AppParams) IsEmpty() bool { + return reflect.DeepEqual(*app, AppParams{}) +} diff --git a/cf/models/buildpack.go b/cf/models/buildpack.go new file mode 100644 index 00000000000..f53416bf6dd --- /dev/null +++ b/cf/models/buildpack.go @@ -0,0 +1,11 @@ +package models + +type Buildpack struct { + Guid string + Name string + Position *int + Enabled *bool + Key string + Filename string + Locked *bool +} diff --git a/cf/models/domain.go b/cf/models/domain.go new file mode 100644 index 00000000000..e7f68344b7d --- /dev/null +++ b/cf/models/domain.go @@ -0,0 +1,17 @@ +package models + +import "fmt" + +type DomainFields struct { + Guid string + Name string + OwningOrganizationGuid string + Shared bool +} + +func (model DomainFields) UrlForHost(host string) string { + if host == "" { + return model.Name + } + return fmt.Sprintf("%s.%s", host, model.Name) +} diff --git a/cf/models/domain_test.go b/cf/models/domain_test.go new file mode 100644 index 00000000000..8d137eb5944 --- /dev/null +++ b/cf/models/domain_test.go @@ -0,0 +1,28 @@ +package models_test + +import ( + . "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DomainFields", func() { + var route Route + + BeforeEach(func() { + route = Route{} + + domain := DomainFields{} + domain.Name = "example.com" + route.Domain = domain + }) + + It("uses the hostname as part of the URL", func() { + route.Host = "foo" + Expect(route.URL()).To(Equal("foo.example.com")) + }) + + It("omits the hostname when none is given", func() { + Expect(route.URL()).To(Equal("example.com")) + }) +}) diff --git a/cf/models/environment.go b/cf/models/environment.go new file mode 100644 index 00000000000..48a6fe79a9a --- /dev/null +++ b/cf/models/environment.go @@ -0,0 +1,19 @@ +package models + +func NewEnvironment() *Environment { + return &Environment{ + System: make(map[string]interface{}), + Application: make(map[string]interface{}), + Environment: make(map[string]interface{}), + Running: make(map[string]interface{}), + Staging: make(map[string]interface{}), + } +} + +type Environment struct { + System map[string]interface{} `json:"system_env_json,omitempty"` + Environment map[string]interface{} `json:"environment_json,omitempty"` + Running map[string]interface{} `json:"running_env_json,omitempty"` + Staging map[string]interface{} `json:"staging_env_json,omitempty"` + Application map[string]interface{} `json:"application_env_json,omitempty"` +} diff --git a/cf/models/environment_variable.go b/cf/models/environment_variable.go new file mode 100644 index 00000000000..77b9f862148 --- /dev/null +++ b/cf/models/environment_variable.go @@ -0,0 +1,12 @@ +package models + +func NewEnvironmentVariable(name string, value string) (e EnvironmentVariable) { + e.Name = name + e.Value = value + return +} + +type EnvironmentVariable struct { + Name string + Value string +} diff --git a/cf/models/feature_flag.go b/cf/models/feature_flag.go new file mode 100644 index 00000000000..71722bae147 --- /dev/null +++ b/cf/models/feature_flag.go @@ -0,0 +1,14 @@ +package models + +func NewFeatureFlag(name string, enabled bool, errorMessage string) (f FeatureFlag) { + f.Name = name + f.Enabled = enabled + f.ErrorMessage = errorMessage + return +} + +type FeatureFlag struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + ErrorMessage string `json:"error_message"` +} diff --git a/cf/models/models_suite_test.go b/cf/models/models_suite_test.go new file mode 100644 index 00000000000..2d4c6ded196 --- /dev/null +++ b/cf/models/models_suite_test.go @@ -0,0 +1,13 @@ +package models_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestModels(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Models Suite") +} diff --git a/cf/models/organization.go b/cf/models/organization.go new file mode 100644 index 00000000000..ac7d1e59fac --- /dev/null +++ b/cf/models/organization.go @@ -0,0 +1,14 @@ +package models + +type OrganizationFields struct { + Guid string + Name string + QuotaDefinition QuotaFields +} + +type Organization struct { + OrganizationFields + Spaces []SpaceFields + Domains []DomainFields + SpaceQuotas []SpaceQuota +} diff --git a/cf/models/quota.go b/cf/models/quota.go new file mode 100644 index 00000000000..02bc519b15a --- /dev/null +++ b/cf/models/quota.go @@ -0,0 +1,20 @@ +package models + +func NewQuotaFields(name string, memory int64, routes int, services int, nonbasicservices bool) (q QuotaFields) { + q.Name = name + q.MemoryLimit = memory + q.RoutesLimit = routes + q.ServicesLimit = services + q.NonBasicServicesAllowed = nonbasicservices + return +} + +type QuotaFields struct { + Guid string `json:"guid,omitempty"` + Name string `json:"name"` + MemoryLimit int64 `json:"memory_limit"` // in Megabytes + InstanceMemoryLimit int64 `json:"instance_memory_limit"` // in Megabytes + RoutesLimit int `json:"total_routes"` + ServicesLimit int `json:"total_services"` + NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` +} diff --git a/cf/models/route.go b/cf/models/route.go new file mode 100644 index 00000000000..a34760e005f --- /dev/null +++ b/cf/models/route.go @@ -0,0 +1,32 @@ +package models + +import "fmt" + +type Route struct { + Guid string + Host string + Domain DomainFields + + Space SpaceFields + Apps []ApplicationFields +} + +func (route Route) URL() string { + if route.Host == "" { + return route.Domain.Name + } + return fmt.Sprintf("%s.%s", route.Host, route.Domain.Name) +} + +type RouteSummary struct { + Guid string + Host string + Domain DomainFields +} + +func (model RouteSummary) URL() string { + if model.Host == "" { + return model.Domain.Name + } + return fmt.Sprintf("%s.%s", model.Host, model.Domain.Name) +} diff --git a/cf/models/security_group.go b/cf/models/security_group.go new file mode 100644 index 00000000000..3f8c10f1e94 --- /dev/null +++ b/cf/models/security_group.go @@ -0,0 +1,21 @@ +package models + +// represents just the attributes for an security group +type SecurityGroupFields struct { + Name string + Guid string + Rules []map[string]interface{} +} + +// represents the JSON that we send up to CC when the user creates / updates a record +type SecurityGroupParams struct { + Name string `json:"name,omitempty"` + Guid string `json:"guid,omitempty"` + Rules []map[string]interface{} `json:"rules"` +} + +// represents a fully instantiated model returned by the CC (e.g.: with its attributes and the fields for its child objects) +type SecurityGroup struct { + SecurityGroupFields + Spaces []Space +} diff --git a/cf/models/service_auth_token.go b/cf/models/service_auth_token.go new file mode 100644 index 00000000000..2df5c3b2c5a --- /dev/null +++ b/cf/models/service_auth_token.go @@ -0,0 +1,8 @@ +package models + +type ServiceAuthTokenFields struct { + Guid string + Label string + Provider string + Token string +} diff --git a/cf/models/service_binding.go b/cf/models/service_binding.go new file mode 100644 index 00000000000..b30addd6c7d --- /dev/null +++ b/cf/models/service_binding.go @@ -0,0 +1,7 @@ +package models + +type ServiceBindingFields struct { + Guid string + Url string + AppGuid string +} diff --git a/cf/models/service_broker.go b/cf/models/service_broker.go new file mode 100644 index 00000000000..dc7aa792525 --- /dev/null +++ b/cf/models/service_broker.go @@ -0,0 +1,10 @@ +package models + +type ServiceBroker struct { + Guid string + Name string + Username string + Password string + Url string + Services []ServiceOffering +} diff --git a/cf/models/service_instance.go b/cf/models/service_instance.go new file mode 100644 index 00000000000..f3ed5129435 --- /dev/null +++ b/cf/models/service_instance.go @@ -0,0 +1,20 @@ +package models + +type ServiceInstanceFields struct { + Guid string + Name string + SysLogDrainUrl string + ApplicationNames []string + Params map[string]interface{} +} + +type ServiceInstance struct { + ServiceInstanceFields + ServiceBindings []ServiceBindingFields + ServicePlan ServicePlanFields + ServiceOffering ServiceOfferingFields +} + +func (inst ServiceInstance) IsUserProvided() bool { + return inst.ServicePlan.Guid == "" +} diff --git a/cf/models/service_offering.go b/cf/models/service_offering.go new file mode 100644 index 00000000000..658b718d406 --- /dev/null +++ b/cf/models/service_offering.go @@ -0,0 +1,30 @@ +package models + +type ServiceOfferingFields struct { + Guid string + BrokerGuid string + Label string + Provider string + Version string + Description string + DocumentationUrl string +} + +type ServiceOffering struct { + ServiceOfferingFields + Plans []ServicePlanFields +} + +type ServiceOfferings []ServiceOffering + +func (s ServiceOfferings) Len() int { + return len(s) +} + +func (s ServiceOfferings) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ServiceOfferings) Less(i, j int) bool { + return s[i].Label < s[j].Label +} diff --git a/cf/models/service_plan.go b/cf/models/service_plan.go new file mode 100644 index 00000000000..70604a674f1 --- /dev/null +++ b/cf/models/service_plan.go @@ -0,0 +1,29 @@ +package models + +type ServicePlanFields struct { + Guid string + Name string + Free bool + Public bool + Description string + Active bool + ServiceOfferingGuid string + OrgNames []string +} + +type ServicePlan struct { + ServicePlanFields + ServiceOffering ServiceOfferingFields +} + +func (servicePlanFields ServicePlanFields) OrgHasVisibility(orgName string) bool { + if servicePlanFields.Public { + return true + } + for _, org := range servicePlanFields.OrgNames { + if org == orgName { + return true + } + } + return false +} diff --git a/cf/models/service_plan_test.go b/cf/models/service_plan_test.go new file mode 100644 index 00000000000..64c047f70d9 --- /dev/null +++ b/cf/models/service_plan_test.go @@ -0,0 +1,47 @@ +package models_test + +import ( + . "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServicePlanFields", func() { + var servicePlanFields ServicePlanFields + + BeforeEach(func() { + servicePlanFields = ServicePlanFields{ + Guid: "I-am-a-guid", + Name: "BestServicePlanEver", + Free: false, + Public: true, + Description: "A Plan For Service", + Active: true, + ServiceOfferingGuid: "service-offering-guid", + OrgNames: []string{"org1", "org2"}, + } + }) + + Describe(".OrgHasVisibility", func() { + Context("when the service plan is public", func() { + It("returns true", func() { + Expect(servicePlanFields.OrgHasVisibility("anyOrg")).To(BeTrue()) + }) + }) + + Context("when the service plan is not public", func() { + BeforeEach(func() { + servicePlanFields.Public = false + }) + + It("returns true if the orgname is in the list of orgs that have visibility", func() { + Expect(servicePlanFields.OrgHasVisibility("org1")).To(BeTrue()) + }) + + It("returns false if the orgname is not in the list of orgs that have visibility", func() { + Expect(servicePlanFields.OrgHasVisibility("org-that-has-no-visibility")).To(BeFalse()) + }) + }) + + }) +}) diff --git a/cf/models/service_plan_visibility.go b/cf/models/service_plan_visibility.go new file mode 100644 index 00000000000..61417e3f8be --- /dev/null +++ b/cf/models/service_plan_visibility.go @@ -0,0 +1,7 @@ +package models + +type ServicePlanVisibilityFields struct { + Guid string `json:"guid"` + ServicePlanGuid string `json:"service_plan_guid"` + OrganizationGuid string `json:"organization_guid"` +} diff --git a/cf/models/space.go b/cf/models/space.go new file mode 100644 index 00000000000..e52e62144a1 --- /dev/null +++ b/cf/models/space.go @@ -0,0 +1,16 @@ +package models + +type SpaceFields struct { + Guid string + Name string +} + +type Space struct { + SpaceFields + Organization OrganizationFields + Applications []ApplicationFields + ServiceInstances []ServiceInstanceFields + Domains []DomainFields + SecurityGroups []SecurityGroupFields + SpaceQuotaGuid string +} diff --git a/cf/models/space_quota.go b/cf/models/space_quota.go new file mode 100644 index 00000000000..02f15ea96ee --- /dev/null +++ b/cf/models/space_quota.go @@ -0,0 +1,22 @@ +package models + +func NewSpaceQuota(name string, memory int64, routes int, services int, nonbasicservices bool, orgGuid string) (q SpaceQuota) { + q.Name = name + q.MemoryLimit = memory + q.RoutesLimit = routes + q.ServicesLimit = services + q.NonBasicServicesAllowed = nonbasicservices + q.OrgGuid = orgGuid + return +} + +type SpaceQuota struct { + Guid string `json:"guid,omitempty"` + Name string `json:"name"` + MemoryLimit int64 `json:"memory_limit"` // in Megabytes + InstanceMemoryLimit int64 `json:"instance_memory_limit"` // in Megabytes + RoutesLimit int `json:"total_routes"` + ServicesLimit int `json:"total_services"` + NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` + OrgGuid string `json:"organization_guid"` +} diff --git a/cf/models/stack.go b/cf/models/stack.go new file mode 100644 index 00000000000..c3b08a0630e --- /dev/null +++ b/cf/models/stack.go @@ -0,0 +1,7 @@ +package models + +type Stack struct { + Guid string + Name string + Description string +} diff --git a/cf/models/user.go b/cf/models/user.go new file mode 100644 index 00000000000..10ddaa0083c --- /dev/null +++ b/cf/models/user.go @@ -0,0 +1,8 @@ +package models + +type UserFields struct { + Guid string + Username string + Password string + IsAdmin bool +} diff --git a/cf/models/user_roles.go b/cf/models/user_roles.go new file mode 100644 index 00000000000..5413d2840c7 --- /dev/null +++ b/cf/models/user_roles.go @@ -0,0 +1,29 @@ +package models + +const ( + ORG_USER = "OrgUser" + ORG_MANAGER = "OrgManager" + BILLING_MANAGER = "BillingManager" + ORG_AUDITOR = "OrgAuditor" + SPACE_MANAGER = "SpaceManager" + SPACE_DEVELOPER = "SpaceDeveloper" + SPACE_AUDITOR = "SpaceAuditor" +) + +var UserInputToOrgRole = map[string]string{ + "OrgManager": ORG_MANAGER, + "BillingManager": BILLING_MANAGER, + "OrgAuditor": ORG_AUDITOR, +} + +var UserInputToSpaceRole = map[string]string{ + "SpaceManager": SPACE_MANAGER, + "SpaceDeveloper": SPACE_DEVELOPER, + "SpaceAuditor": SPACE_AUDITOR, +} + +var SpaceRoleToUserInput = map[string]string{ + SPACE_MANAGER: "SpaceManager", + SPACE_DEVELOPER: "SpaceDeveloper", + SPACE_AUDITOR: "SpaceAuditor", +} diff --git a/cf/net/cloud_controller_gateway.go b/cf/net/cloud_controller_gateway.go new file mode 100644 index 00000000000..d78ba970feb --- /dev/null +++ b/cf/net/cloud_controller_gateway.go @@ -0,0 +1,34 @@ +package net + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type ccErrorResponse struct { + Code int + Description string +} + +func cloudControllerErrorHandler(statusCode int, body []byte) error { + response := ccErrorResponse{} + json.Unmarshal(body, &response) + + if response.Code == 1000 { // MAGICKAL NUMBERS AHOY + return errors.NewInvalidTokenError(response.Description) + } else { + return errors.NewHttpError(statusCode, strconv.Itoa(response.Code), response.Description) + } +} + +func NewCloudControllerGateway(config core_config.Reader, clock func() time.Time, ui terminal.UI) Gateway { + gateway := newGateway(cloudControllerErrorHandler, config, ui) + gateway.Clock = clock + gateway.PollingEnabled = true + return gateway +} diff --git a/cf/net/cloud_controller_gateway_test.go b/cf/net/cloud_controller_gateway_test.go new file mode 100644 index 00000000000..d1f3626af33 --- /dev/null +++ b/cf/net/cloud_controller_gateway_test.go @@ -0,0 +1,64 @@ +package net_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var failingCloudControllerRequest = func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + jsonResponse := `{ "code": 210003, "description": "The host is taken: test1" }` + fmt.Fprintln(writer, jsonResponse) +} + +var invalidTokenCloudControllerRequest = func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + jsonResponse := `{ "code": 1000, "description": "The token is invalid" }` + fmt.Fprintln(writer, jsonResponse) +} + +var _ = Describe("Cloud Controller Gateway", func() { + var gateway Gateway + var config core_config.Reader + + BeforeEach(func() { + config = testconfig.NewRepository() + gateway = NewCloudControllerGateway(config, time.Now, &testterm.FakeUI{}) + }) + + It("parses error responses", func() { + ts := httptest.NewTLSServer(http.HandlerFunc(failingCloudControllerRequest)) + defer ts.Close() + gateway.SetTrustedCerts(ts.TLS.Certificates) + + request, apiErr := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) + _, apiErr = gateway.PerformRequest(request) + + Expect(apiErr).NotTo(BeNil()) + Expect(apiErr.Error()).To(ContainSubstring("The host is taken: test1")) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(ContainSubstring("210003")) + }) + + It("parses invalid token responses", func() { + ts := httptest.NewTLSServer(http.HandlerFunc(invalidTokenCloudControllerRequest)) + defer ts.Close() + gateway.SetTrustedCerts(ts.TLS.Certificates) + + request, apiErr := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) + _, apiErr = gateway.PerformRequest(request) + + Expect(apiErr).NotTo(BeNil()) + Expect(apiErr.Error()).To(ContainSubstring("The token is invalid")) + Expect(apiErr.(*errors.InvalidTokenError)).To(HaveOccurred()) + }) +}) diff --git a/cf/net/fakes/fake_http_client_interface.go b/cf/net/fakes/fake_http_client_interface.go new file mode 100644 index 00000000000..7a1d68ef594 --- /dev/null +++ b/cf/net/fakes/fake_http_client_interface.go @@ -0,0 +1,56 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "net/http" + "sync" + + "github.com/cloudfoundry/cli/cf/net" +) + +type FakeHttpClientInterface struct { + DoStub func(req *http.Request) (resp *http.Response, err error) + doMutex sync.RWMutex + doArgsForCall []struct { + req *http.Request + } + doReturns struct { + result1 *http.Response + result2 error + } +} + +func (fake *FakeHttpClientInterface) Do(req *http.Request) (resp *http.Response, err error) { + fake.doMutex.Lock() + fake.doArgsForCall = append(fake.doArgsForCall, struct { + req *http.Request + }{req}) + fake.doMutex.Unlock() + if fake.DoStub != nil { + return fake.DoStub(req) + } else { + return fake.doReturns.result1, fake.doReturns.result2 + } +} + +func (fake *FakeHttpClientInterface) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *FakeHttpClientInterface) DoArgsForCall(i int) *http.Request { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return fake.doArgsForCall[i].req +} + +func (fake *FakeHttpClientInterface) DoReturns(result1 *http.Response, result2 error) { + fake.DoStub = nil + fake.doReturns = struct { + result1 *http.Response + result2 error + }{result1, result2} +} + +var _ net.HttpClientInterface = new(FakeHttpClientInterface) diff --git a/cf/net/gateway.go b/cf/net/gateway.go new file mode 100644 index 00000000000..64a85d474fb --- /dev/null +++ b/cf/net/gateway.go @@ -0,0 +1,456 @@ +package net + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "runtime" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" +) + +const ( + JOB_FINISHED = "finished" + JOB_FAILED = "failed" + DEFAULT_POLLING_THROTTLE = 5 * time.Second +) + +type JobResource struct { + Entity struct { + Status string + ErrorDetails struct { + Description string + } `json:"error_details"` + } +} + +type AsyncResource struct { + Metadata struct { + URL string + } +} + +type apiErrorHandler func(statusCode int, body []byte) error + +type tokenRefresher interface { + RefreshAuthToken() (string, error) +} + +type Request struct { + HttpReq *http.Request + SeekableBody io.ReadSeeker +} + +type Gateway struct { + authenticator tokenRefresher + errHandler apiErrorHandler + PollingEnabled bool + PollingThrottle time.Duration + trustedCerts []tls.Certificate + config core_config.Reader + warnings *[]string + Clock func() time.Time + transport *http.Transport + ui terminal.UI +} + +func newGateway(errHandler apiErrorHandler, config core_config.Reader, ui terminal.UI) (gateway Gateway) { + gateway.errHandler = errHandler + gateway.config = config + gateway.PollingThrottle = DEFAULT_POLLING_THROTTLE + gateway.warnings = &[]string{} + gateway.Clock = time.Now + gateway.ui = ui + + return +} + +func (gateway *Gateway) AsyncTimeout() time.Duration { + if gateway.config.AsyncTimeout() > 0 { + return time.Duration(gateway.config.AsyncTimeout()) * time.Minute + } + + return 0 +} + +func (gateway *Gateway) SetTokenRefresher(auth tokenRefresher) { + gateway.authenticator = auth +} + +func (gateway Gateway) GetResource(url string, resource interface{}) (err error) { + request, err := gateway.NewRequest("GET", url, gateway.config.AccessToken(), nil) + if err != nil { + return + } + + _, err = gateway.PerformRequestForJSONResponse(request, resource) + return +} + +func (gateway Gateway) CreateResourceFromStruct(endpoint, url string, resource interface{}) error { + bytes, err := json.Marshal(resource) + if err != nil { + return err + } + + return gateway.CreateResource(endpoint, url, strings.NewReader(string(bytes))) +} + +func (gateway Gateway) UpdateResourceFromStruct(endpoint, apiUrl string, resource interface{}) error { + bytes, err := json.Marshal(resource) + if err != nil { + return err + } + + return gateway.UpdateResource(endpoint, apiUrl, strings.NewReader(string(bytes))) +} + +func (gateway Gateway) CreateResource(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) { + return gateway.createUpdateOrDeleteResource("POST", endpoint, apiUrl, body, false, resource...) +} + +func (gateway Gateway) UpdateResource(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) { + return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiUrl, body, false, resource...) +} + +func (gateway Gateway) UpdateResourceSync(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) { + return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiUrl, body, true, resource...) +} + +func (gateway Gateway) DeleteResource(endpoint, apiUrl string) (apiErr error) { + return gateway.createUpdateOrDeleteResource("DELETE", endpoint, apiUrl, nil, false, &AsyncResource{}) +} + +func (gateway Gateway) ListPaginatedResources(target string, + path string, + resource interface{}, + cb func(interface{}) bool) (apiErr error) { + for path != "" { + pagination := NewPaginatedResources(resource) + + apiErr = gateway.GetResource(fmt.Sprintf("%s%s", target, path), &pagination) + if apiErr != nil { + return + } + + resources, err := pagination.Resources() + if err != nil { + return errors.NewWithError(T("Error parsing JSON"), err) + } + + for _, resource := range resources { + if !cb(resource) { + return + } + } + + path = pagination.NextURL + } + + return +} + +func (gateway Gateway) createUpdateOrDeleteResource(verb, endpoint, apiUrl string, body io.ReadSeeker, sync bool, optionalResource ...interface{}) (apiErr error) { + var resource interface{} + if len(optionalResource) > 0 { + resource = optionalResource[0] + } + + request, apiErr := gateway.NewRequest(verb, endpoint+apiUrl, gateway.config.AccessToken(), body) + if apiErr != nil { + return + } + + if resource == nil { + _, apiErr = gateway.PerformRequest(request) + return + } + + if gateway.PollingEnabled && !sync { + _, apiErr = gateway.PerformPollingRequestForJSONResponse(endpoint, request, resource, gateway.AsyncTimeout()) + return + } else { + _, apiErr = gateway.PerformRequestForJSONResponse(request, resource) + return + } + +} + +func (gateway Gateway) newRequest(request *http.Request, accessToken string, body io.ReadSeeker) (*Request, error) { + if accessToken != "" { + request.Header.Set("Authorization", accessToken) + } + + request.Header.Set("accept", "application/json") + request.Header.Set("content-type", "application/json") + request.Header.Set("User-Agent", "go-cli "+cf.Version+" / "+runtime.GOOS) + return &Request{HttpReq: request, SeekableBody: body}, nil +} + +func (gateway Gateway) NewRequestForFile(method, fullUrl, accessToken string, body *os.File) (req *Request, apiErr error) { + progressReader := NewProgressReader(body, gateway.ui, 5*time.Second) + progressReader.Seek(0, 0) + fileStats, err := body.Stat() + + if err != nil { + apiErr = errors.NewWithError(T("Error getting file info"), err) + return + } + + request, err := http.NewRequest(method, fullUrl, progressReader) + if err != nil { + apiErr = errors.NewWithError(T("Error building request"), err) + return + } + + fileSize := fileStats.Size() + progressReader.SetTotalSize(fileSize) + request.ContentLength = fileSize + + if err != nil { + apiErr = errors.NewWithError(T("Error building request"), err) + return + } + + return gateway.newRequest(request, accessToken, progressReader) +} + +func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (req *Request, apiErr error) { + request, err := http.NewRequest(method, path, body) + if err != nil { + apiErr = errors.NewWithError(T("Error building request"), err) + return + } + return gateway.newRequest(request, accessToken, body) +} + +func (gateway Gateway) PerformRequest(request *Request) (rawResponse *http.Response, apiErr error) { + return gateway.doRequestHandlingAuth(request) +} + +func (gateway Gateway) performRequestForResponseBytes(request *Request) (bytes []byte, headers http.Header, rawResponse *http.Response, apiErr error) { + rawResponse, apiErr = gateway.doRequestHandlingAuth(request) + if apiErr != nil { + return + } + defer rawResponse.Body.Close() + + bytes, err := ioutil.ReadAll(rawResponse.Body) + if err != nil { + apiErr = errors.NewWithError(T("Error reading response"), err) + } + + headers = rawResponse.Header + return +} + +func (gateway Gateway) PerformRequestForTextResponse(request *Request) (response string, headers http.Header, apiErr error) { + bytes, headers, _, apiErr := gateway.performRequestForResponseBytes(request) + response = string(bytes) + return +} + +func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (headers http.Header, apiErr error) { + bytes, headers, rawResponse, apiErr := gateway.performRequestForResponseBytes(request) + if apiErr != nil { + return + } + + if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { + return + } + + err := json.Unmarshal(bytes, &response) + if err != nil { + apiErr = errors.NewWithError(T("Invalid JSON response from server"), err) + } + return +} + +func (gateway Gateway) PerformPollingRequestForJSONResponse(endpoint string, request *Request, response interface{}, timeout time.Duration) (headers http.Header, apiErr error) { + query := request.HttpReq.URL.Query() + query.Add("async", "true") + request.HttpReq.URL.RawQuery = query.Encode() + + bytes, headers, rawResponse, apiErr := gateway.performRequestForResponseBytes(request) + if apiErr != nil { + return + } + defer rawResponse.Body.Close() + + if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { + return + } + + err := json.Unmarshal(bytes, &response) + if err != nil { + apiErr = errors.NewWithError(T("Invalid JSON response from server"), err) + return + } + + asyncResource := &AsyncResource{} + err = json.Unmarshal(bytes, &asyncResource) + if err != nil { + apiErr = errors.NewWithError(T("Invalid async response from server"), err) + return + } + + jobUrl := asyncResource.Metadata.URL + if jobUrl == "" { + return + } + + if !strings.Contains(jobUrl, "/jobs/") { + return + } + + apiErr = gateway.waitForJob(endpoint+jobUrl, request.HttpReq.Header.Get("Authorization"), timeout) + + return +} + +func (gateway Gateway) Warnings() []string { + return *gateway.warnings +} + +func (gateway Gateway) waitForJob(jobUrl, accessToken string, timeout time.Duration) (err error) { + startTime := gateway.Clock() + for true { + if gateway.Clock().Sub(startTime) > timeout && timeout != 0 { + return errors.NewAsyncTimeoutError(jobUrl) + } + var request *Request + request, err = gateway.NewRequest("GET", jobUrl, accessToken, nil) + response := &JobResource{} + _, err = gateway.PerformRequestForJSONResponse(request, response) + if err != nil { + return + } + + switch response.Entity.Status { + case JOB_FINISHED: + return + case JOB_FAILED: + err = errors.New(response.Entity.ErrorDetails.Description) + return + } + + accessToken = request.HttpReq.Header.Get("Authorization") + + time.Sleep(gateway.PollingThrottle) + } + return +} + +func (gateway Gateway) doRequestHandlingAuth(request *Request) (rawResponse *http.Response, err error) { + httpReq := request.HttpReq + + if request.SeekableBody != nil { + httpReq.Body = ioutil.NopCloser(request.SeekableBody) + } + + // perform request + rawResponse, err = gateway.doRequestAndHandlerError(request) + if err == nil || gateway.authenticator == nil { + return + } + + switch err.(type) { + case *errors.InvalidTokenError: + // refresh the auth token + var newToken string + newToken, err = gateway.authenticator.RefreshAuthToken() + if err != nil { + return + } + + // reset the auth token and request body + httpReq.Header.Set("Authorization", newToken) + if request.SeekableBody != nil { + request.SeekableBody.Seek(0, 0) + httpReq.Body = ioutil.NopCloser(request.SeekableBody) + } + + // make the request again + rawResponse, err = gateway.doRequestAndHandlerError(request) + } + + return +} + +func (gateway Gateway) doRequestAndHandlerError(request *Request) (rawResponse *http.Response, err error) { + rawResponse, err = gateway.doRequest(request.HttpReq) + if err != nil { + err = WrapNetworkErrors(request.HttpReq.URL.Host, err) + return + } + + if rawResponse.StatusCode > 299 { + jsonBytes, _ := ioutil.ReadAll(rawResponse.Body) + rawResponse.Body.Close() + rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes)) + err = gateway.errHandler(rawResponse.StatusCode, jsonBytes) + } + + return +} + +func (gateway Gateway) doRequest(request *http.Request) (response *http.Response, err error) { + if gateway.transport == nil { + makeHttpTransport(&gateway) + } + + httpClient := NewHttpClient(gateway.transport) + + dumpRequest(request) + + for i := 0; i < 3; i++ { + response, err = httpClient.Do(request) + if response == nil && err != nil { + continue + } else { + break + } + } + + if err != nil { + return + } + + dumpResponse(response) + + header := http.CanonicalHeaderKey("X-Cf-Warnings") + raw_warnings := response.Header[header] + for _, raw_warning := range raw_warnings { + warning, _ := url.QueryUnescape(raw_warning) + *gateway.warnings = append(*gateway.warnings, warning) + } + + return +} + +func makeHttpTransport(gateway *Gateway) { + gateway.transport = &http.Transport{ + Dial: (&net.Dialer{Timeout: 5 * time.Second}).Dial, + TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()), + Proxy: http.ProxyFromEnvironment, + } +} + +func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) { + gateway.trustedCerts = certificates + makeHttpTransport(gateway) +} diff --git a/cf/net/gateway_test.go b/cf/net/gateway_test.go new file mode 100644 index 00000000000..84e9986c3ea --- /dev/null +++ b/cf/net/gateway_test.go @@ -0,0 +1,612 @@ +package net_test + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "reflect" + "runtime" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/api/authentication" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/cli/cf/net/fakes" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Gateway", func() { + var ( + ccGateway Gateway + uaaGateway Gateway + config core_config.ReadWriter + authRepo authentication.AuthenticationRepository + currentTime time.Time + clock func() time.Time + + client *fakes.FakeHttpClientInterface + ) + + BeforeEach(func() { + currentTime = time.Unix(0, 0) + clock = func() time.Time { return currentTime } + config = testconfig.NewRepository() + + ccGateway = NewCloudControllerGateway(config, clock, &testterm.FakeUI{}) + ccGateway.PollingThrottle = 3 * time.Millisecond + uaaGateway = NewUAAGateway(config, &testterm.FakeUI{}) + }) + + Describe("async timeout", func() { + Context("when the config has a positive async timeout", func() { + It("inherits the async timeout from the config", func() { + config.SetAsyncTimeout(9001) + ccGateway = NewCloudControllerGateway((config), time.Now, &testterm.FakeUI{}) + Expect(ccGateway.AsyncTimeout()).To(Equal(9001 * time.Minute)) + }) + }) + }) + + Describe("Connection errors", func() { + var oldNewHttpClient func(tr *http.Transport) HttpClientInterface + + BeforeEach(func() { + client = &fakes.FakeHttpClientInterface{} + + oldNewHttpClient = NewHttpClient + NewHttpClient = func(tr *http.Transport) HttpClientInterface { + return client + } + }) + + AfterEach(func() { + NewHttpClient = oldNewHttpClient + }) + + It("only retry when response body is nil and error occurred", func() { + client.DoReturns(&http.Response{Status: "internal error", StatusCode: 500}, errors.New("internal error")) + request, apiErr := ccGateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil) + Expect(apiErr).ToNot(HaveOccurred()) + + _, apiErr = ccGateway.PerformRequest(request) + Expect(client.DoCallCount()).To(Equal(1)) + Expect(apiErr).To(HaveOccurred()) + }) + + It("Retries 3 times if we cannot contact the server", func() { + client.DoReturns(nil, errors.New("Connection refused")) + request, apiErr := ccGateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil) + Expect(apiErr).ToNot(HaveOccurred()) + + _, apiErr = ccGateway.PerformRequest(request) + Expect(apiErr).To(HaveOccurred()) + Expect(client.DoCallCount()).To(Equal(3)) + }) + }) + + Describe("NewRequest", func() { + var ( + request *Request + apiErr error + ) + + Context("when the body is nil", func() { + BeforeEach(func() { + request, apiErr = ccGateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("does not use a ProgressReader as the SeekableBody", func() { + Expect(reflect.TypeOf(request.SeekableBody)).To(BeNil()) + }) + + It("sets the Authorization header", func() { + Expect(request.HttpReq.Header.Get("Authorization")).To(Equal("BEARER my-access-token")) + }) + + It("sets the accept header to application/json", func() { + Expect(request.HttpReq.Header.Get("accept")).To(Equal("application/json")) + }) + + It("sets the user agent header", func() { + Expect(request.HttpReq.Header.Get("User-Agent")).To(Equal("go-cli " + cf.Version + " / " + runtime.GOOS)) + }) + }) + + Context("when the body is a file", func() { + BeforeEach(func() { + f, _ := os.Open("../../fixtures/test.file") + request, apiErr = ccGateway.NewRequestForFile("PUT", "https://example.com/v2/apps", "BEARER my-access-token", f) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("Uses a ProgressReader as the SeekableBody", func() { + Expect(reflect.TypeOf(request.SeekableBody).String()).To(ContainSubstring("ProgressReader")) + }) + + }) + + }) + + Describe("CRUD methods", func() { + Describe("Delete", func() { + var apiServer *httptest.Server + + Context("when the config has an async timeout", func() { + BeforeEach(func() { + count := 0 + apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/v2/foobars/SOME_GUID": + writer.WriteHeader(http.StatusNoContent) + case "/v2/foobars/TIMEOUT": + currentTime = currentTime.Add(time.Minute * 31) + fmt.Fprintln(writer, ` +{ + "metadata": { + "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52", + "created_at": "2014-05-15T19:15:01+00:00", + "url": "/v2/jobs/8438916f-5c00-4d44-a19b-1df65abe9d52" + }, + "entity": { + "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52", + "status": "queued" + } +}`) + writer.WriteHeader(http.StatusAccepted) + case "/v2/jobs/8438916f-5c00-4d44-a19b-1df65abe9d52": + if count == 0 { + count++ + currentTime = currentTime.Add(time.Minute * 31) + + writer.WriteHeader(http.StatusOK) + fmt.Fprintln(writer, ` +{ + "entity": { + "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52", + "status": "queued" + } +}`) + } else { + panic("FAIL") + } + default: + panic("shouldn't have made call to this URL: " + request.URL.Path) + } + })) + + config.SetAsyncTimeout(30) + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + }) + + AfterEach(func() { + apiServer.Close() + }) + + It("deletes a resource", func() { + err := ccGateway.DeleteResource(apiServer.URL, "/v2/foobars/SOME_GUID") + Expect(err).ToNot(HaveOccurred()) + }) + + Context("when the request would take longer than the async timeout", func() { + It("returns an error", func() { + apiErr := ccGateway.DeleteResource(apiServer.URL, "/v2/foobars/TIMEOUT") + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr).To(BeAssignableToTypeOf(errors.NewAsyncTimeoutError("http://some.url"))) + }) + }) + }) + }) + }) + + Describe("making an async request", func() { + var ( + jobStatus string + apiServer *httptest.Server + authServer *httptest.Server + statusChannel chan string + ) + + BeforeEach(func() { + jobStatus = "queued" + statusChannel = make(chan string, 10) + + apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + currentTime = currentTime.Add(time.Millisecond * 11) + + updateStatus, ok := <-statusChannel + if ok { + jobStatus = updateStatus + } + + switch request.URL.Path { + case "/v2/foo": + fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`) + case "/v2/jobs/the-job-guid": + fmt.Fprintf(writer, ` + { + "entity": { + "status": "%s", + "error_details": { + "description": "he's dead, Jim" + } + } + }`, jobStatus) + default: + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, `"Unexpected request path '%s'"`, request.URL.Path) + } + })) + + authServer, _ = testnet.NewTLSServer([]testnet.TestRequest{}) + + config, authRepo = createAuthenticationRepository(apiServer, authServer) + ccGateway.SetTokenRefresher(authRepo) + + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + }) + + AfterEach(func() { + apiServer.Close() + authServer.Close() + }) + + It("returns the last response if the job completes before the timeout", func() { + go func() { + statusChannel <- "queued" + statusChannel <- "finished" + }() + + request, _ := ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), nil) + _, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.ApiEndpoint(), request, new(struct{}), 500*time.Millisecond) + Expect(apiErr).NotTo(HaveOccurred()) + }) + + It("returns an error with the right message when the job fails", func() { + go func() { + statusChannel <- "queued" + statusChannel <- "failed" + }() + + request, _ := ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), nil) + _, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.ApiEndpoint(), request, new(struct{}), 500*time.Millisecond) + Expect(apiErr.Error()).To(ContainSubstring("he's dead, Jim")) + }) + + It("returns an error if jobs takes longer than the timeout", func() { + go func() { + statusChannel <- "queued" + statusChannel <- "OHNOES" + }() + request, _ := ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), nil) + _, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.ApiEndpoint(), request, new(struct{}), 10*time.Millisecond) + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr).To(BeAssignableToTypeOf(errors.NewAsyncTimeoutError("http://some.url"))) + }) + }) + + Describe("when uploading a file", func() { + var ( + err error + request *Request + apiErr error + apiServer *httptest.Server + authServer *httptest.Server + fileToUpload *os.File + ) + + BeforeEach(func() { + apiServer = httptest.NewTLSServer(refreshTokenApiEndPoint( + `{ "code": 1000, "description": "Auth token is invalid" }`, + testnet.TestResponse{Status: http.StatusOK}, + )) + + authServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + fmt.Fprintln( + writer, + `{ "access_token": "new-access-token", "token_type": "bearer", "refresh_token": "new-refresh-token"}`) + })) + + fileToUpload, err = ioutil.TempFile("", "test-gateway") + strings.NewReader("expected body").WriteTo(fileToUpload) + + config, auth := createAuthenticationRepository(apiServer, authServer) + ccGateway.SetTokenRefresher(auth) + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + request, apiErr = ccGateway.NewRequestForFile("POST", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), fileToUpload) + }) + + AfterEach(func() { + apiServer.Close() + authServer.Close() + fileToUpload.Close() + os.Remove(fileToUpload.Name()) + }) + + It("sets the content length to the size of the file", func() { + Expect(err).NotTo(HaveOccurred()) + Expect(apiErr).NotTo(HaveOccurred()) + Expect(request.HttpReq.ContentLength).To(Equal(int64(13))) + }) + + Describe("when the access token expires during the upload", func() { + It("successfully re-sends the file on the second request", func() { + _, apiErr = ccGateway.PerformRequest(request) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("refreshing the auth token", func() { + var authServer *httptest.Server + + BeforeEach(func() { + authServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{ + "access_token": "new-access-token", + "token_type": "bearer", + "refresh_token": "new-refresh-token" + }`) + })) + + uaaGateway.SetTrustedCerts(authServer.TLS.Certificates) + }) + + AfterEach(func() { + authServer.Close() + }) + + It("refreshes the token when UAA requests fail", func() { + apiServer := httptest.NewTLSServer(refreshTokenApiEndPoint( + `{ "error": "invalid_token", "error_description": "Auth token is invalid" }`, + testnet.TestResponse{Status: http.StatusOK}, + )) + defer apiServer.Close() + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + config, auth := createAuthenticationRepository(apiServer, authServer) + uaaGateway.SetTokenRefresher(auth) + request, apiErr := uaaGateway.NewRequest("POST", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), strings.NewReader("expected body")) + _, apiErr = uaaGateway.PerformRequest(request) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(config.AccessToken()).To(Equal("bearer new-access-token")) + Expect(config.RefreshToken()).To(Equal("new-refresh-token")) + }) + + It("refreshes the token when CC requests fail", func() { + apiServer := httptest.NewTLSServer(refreshTokenApiEndPoint( + `{ "code": 1000, "description": "Auth token is invalid" }`, + testnet.TestResponse{Status: http.StatusOK})) + defer apiServer.Close() + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + config, auth := createAuthenticationRepository(apiServer, authServer) + ccGateway.SetTokenRefresher(auth) + request, apiErr := ccGateway.NewRequest("POST", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), strings.NewReader("expected body")) + _, apiErr = ccGateway.PerformRequest(request) + + Expect(apiErr).NotTo(HaveOccurred()) + Expect(config.AccessToken()).To(Equal("bearer new-access-token")) + Expect(config.RefreshToken()).To(Equal("new-refresh-token")) + }) + + It("returns a failure response when token refresh fails after a UAA request", func() { + apiServer := httptest.NewTLSServer(refreshTokenApiEndPoint( + `{ "error": "invalid_token", "error_description": "Auth token is invalid" }`, + testnet.TestResponse{Status: http.StatusBadRequest, Body: `{ + "error": "333", "error_description": "bad request" + }`})) + defer apiServer.Close() + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + config, auth := createAuthenticationRepository(apiServer, authServer) + uaaGateway.SetTokenRefresher(auth) + request, apiErr := uaaGateway.NewRequest("POST", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), strings.NewReader("expected body")) + _, apiErr = uaaGateway.PerformRequest(request) + + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(Equal("333")) + }) + + It("returns a failure response when token refresh fails after a CC request", func() { + apiServer := httptest.NewTLSServer(refreshTokenApiEndPoint( + `{ "code": 1000, "description": "Auth token is invalid" }`, + testnet.TestResponse{Status: http.StatusBadRequest, Body: `{ + "code": 333, "description": "bad request" + }`})) + defer apiServer.Close() + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + config, auth := createAuthenticationRepository(apiServer, authServer) + ccGateway.SetTokenRefresher(auth) + request, apiErr := ccGateway.NewRequest("POST", config.ApiEndpoint()+"/v2/foo", config.AccessToken(), strings.NewReader("expected body")) + _, apiErr = ccGateway.PerformRequest(request) + + Expect(apiErr).To(HaveOccurred()) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(Equal("333")) + }) + }) + + Describe("SSL certificate validation errors", func() { + var ( + request *Request + apiServer *httptest.Server + ) + + BeforeEach(func() { + apiServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintln(w, `{}`) + })) + request, _ = ccGateway.NewRequest("POST", apiServer.URL+"/v2/foo", "the-access-token", nil) + }) + + AfterEach(func() { + apiServer.Close() + }) + + Context("when SSL validation is enabled", func() { + It("returns an invalid cert error if the server's CA is unknown (e.g. cert is self-signed)", func() { + apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeSelfSignedTLSCert()} + + _, apiErr := ccGateway.PerformRequest(request) + certErr, ok := apiErr.(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(certErr.URL).To(Equal(getHost(apiServer.URL))) + Expect(certErr.Reason).To(Equal("unknown authority")) + }) + + It("returns an invalid cert error if the server's cert doesn't match its host", func() { + apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeTLSCertWithInvalidHost()} + + _, apiErr := ccGateway.PerformRequest(request) + certErr, ok := apiErr.(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(certErr.URL).To(Equal(getHost(apiServer.URL))) + if runtime.GOOS != "windows" { + Expect(certErr.Reason).To(Equal("not valid for the requested host")) + } + }) + + It("returns an invalid cert error if the server's cert has expired", func() { + apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeExpiredTLSCert()} + + _, apiErr := ccGateway.PerformRequest(request) + certErr, ok := apiErr.(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(certErr.URL).To(Equal(getHost(apiServer.URL))) + if runtime.GOOS != "windows" { + Expect(certErr.Reason).To(Equal("")) + } + }) + }) + + Context("when SSL validation is disabled", func() { + BeforeEach(func() { + apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeExpiredTLSCert()} + config.SetSSLDisabled(true) + }) + + It("succeeds", func() { + _, apiErr := ccGateway.PerformRequest(request) + Expect(apiErr).NotTo(HaveOccurred()) + }) + }) + + }) + + Describe("collecting warnings", func() { + var ( + apiServer *httptest.Server + authServer *httptest.Server + ) + + BeforeEach(func() { + apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/v2/happy": + fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`) + case "/v2/warning1": + writer.Header().Add("X-Cf-Warnings", url.QueryEscape("Something not too awful has happened")) + fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`) + case "/v2/warning2": + writer.Header().Add("X-Cf-Warnings", url.QueryEscape("Something a little awful")) + writer.Header().Add("X-Cf-Warnings", url.QueryEscape("Don't worry, but be careful")) + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, `{ "key": "value" }`) + } + })) + + authServer, _ = testnet.NewTLSServer([]testnet.TestRequest{}) + + config, authRepo = createAuthenticationRepository(apiServer, authServer) + ccGateway.SetTokenRefresher(authRepo) + + ccGateway.SetTrustedCerts(apiServer.TLS.Certificates) + + config, authRepo = createAuthenticationRepository(apiServer, authServer) + }) + + AfterEach(func() { + apiServer.Close() + authServer.Close() + }) + + It("saves all X-Cf-Warnings headers and exposes them", func() { + request, _ := ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/happy", config.AccessToken(), nil) + ccGateway.PerformRequest(request) + request, _ = ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/warning1", config.AccessToken(), nil) + ccGateway.PerformRequest(request) + request, _ = ccGateway.NewRequest("GET", config.ApiEndpoint()+"/v2/warning2", config.AccessToken(), nil) + ccGateway.PerformRequest(request) + + Expect(ccGateway.Warnings()).To(Equal( + []string{"Something not too awful has happened", "Something a little awful", "Don't worry, but be careful"}, + )) + }) + + It("defaults warnings to an empty slice", func() { + Expect(ccGateway.Warnings()).ToNot(BeNil()) + }) + }) +}) + +func getHost(urlString string) string { + url, err := url.Parse(urlString) + if err != nil { + panic(err) + } + return url.Host +} + +func refreshTokenApiEndPoint(unauthorizedBody string, secondReqResp testnet.TestResponse) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + var jsonResponse string + + bodyBytes, err := ioutil.ReadAll(request.Body) + if err != nil || string(bodyBytes) != "expected body" { + writer.WriteHeader(http.StatusInternalServerError) + return + } + + switch request.Header.Get("Authorization") { + case "bearer initial-access-token": + writer.WriteHeader(http.StatusUnauthorized) + jsonResponse = unauthorizedBody + case "bearer new-access-token": + writer.WriteHeader(secondReqResp.Status) + jsonResponse = secondReqResp.Body + default: + writer.WriteHeader(http.StatusInternalServerError) + } + + fmt.Fprintln(writer, jsonResponse) + } +} + +func createAuthenticationRepository(apiServer *httptest.Server, authServer *httptest.Server) (core_config.ReadWriter, authentication.AuthenticationRepository) { + config := testconfig.NewRepository() + config.SetAuthenticationEndpoint(authServer.URL) + config.SetApiEndpoint(apiServer.URL) + config.SetAccessToken("bearer initial-access-token") + config.SetRefreshToken("initial-refresh-token") + + authGateway := NewUAAGateway(config, &testterm.FakeUI{}) + authGateway.SetTrustedCerts(authServer.TLS.Certificates) + + authenticator := authentication.NewUAAAuthenticationRepository(authGateway, config) + + return config, authenticator +} diff --git a/cf/net/http_client.go b/cf/net/http_client.go new file mode 100644 index 00000000000..c0a6cd226d5 --- /dev/null +++ b/cf/net/http_client.go @@ -0,0 +1,86 @@ +package net + +import ( + _ "crypto/sha512" + "crypto/x509" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" + + "code.google.com/p/go.net/websocket" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/trace" +) + +type HttpClientInterface interface { + Do(req *http.Request) (resp *http.Response, err error) +} + +var NewHttpClient = func(tr *http.Transport) HttpClientInterface { + return &http.Client{ + Transport: tr, + CheckRedirect: PrepareRedirect, + } +} + +func PrepareRedirect(req *http.Request, via []*http.Request) error { + if len(via) > 1 { + return errors.New(T("stopped after 1 redirect")) + } + + prevReq := via[len(via)-1] + req.Header.Set("Authorization", prevReq.Header.Get("Authorization")) + dumpRequest(req) + + return nil +} + +func dumpRequest(req *http.Request) { + shouldDisplayBody := !strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") + dumpedRequest, err := httputil.DumpRequest(req, shouldDisplayBody) + if err != nil { + trace.Logger.Printf(T("Error dumping request\n{{.Err}}\n", map[string]interface{}{"Err": err})) + } else { + trace.Logger.Printf("\n%s [%s]\n%s\n", terminal.HeaderColor(T("REQUEST:")), time.Now().Format(time.RFC3339), trace.Sanitize(string(dumpedRequest))) + if !shouldDisplayBody { + trace.Logger.Println(T("[MULTIPART/FORM-DATA CONTENT HIDDEN]")) + } + } +} + +func dumpResponse(res *http.Response) { + dumpedResponse, err := httputil.DumpResponse(res, true) + if err != nil { + trace.Logger.Printf(T("Error dumping response\n{{.Err}}\n", map[string]interface{}{"Err": err})) + } else { + trace.Logger.Printf("\n%s [%s]\n%s\n", terminal.HeaderColor(T("RESPONSE:")), time.Now().Format(time.RFC3339), trace.Sanitize(string(dumpedResponse))) + } +} + +func WrapNetworkErrors(host string, err error) error { + var innerErr error + switch typedErr := err.(type) { + case *url.Error: + innerErr = typedErr.Err + case *websocket.DialError: + innerErr = typedErr.Err + } + + if innerErr != nil { + switch innerErr.(type) { + case x509.UnknownAuthorityError: + return errors.NewInvalidSSLCert(host, T("unknown authority")) + case x509.HostnameError: + return errors.NewInvalidSSLCert(host, T("not valid for the requested host")) + case x509.CertificateInvalidError: + return errors.NewInvalidSSLCert(host, "") + } + } + + return errors.NewWithError(T("Error performing request"), err) + +} diff --git a/cf/net/http_client_test.go b/cf/net/http_client_test.go new file mode 100644 index 00000000000..23ae0d77fda --- /dev/null +++ b/cf/net/http_client_test.go @@ -0,0 +1,104 @@ +package net_test + +import ( + "code.google.com/p/go.net/websocket" + "crypto/x509" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/net" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "net" + "net/http" + "net/url" + "syscall" +) + +var _ = Describe("HTTP Client", func() { + + Describe("PrepareRedirect", func() { + It("transfers authorization headers", func() { + originalReq, err := http.NewRequest("GET", "/foo", nil) + Expect(err).NotTo(HaveOccurred()) + originalReq.Header.Set("Authorization", "my-auth-token") + + redirectReq, err := http.NewRequest("GET", "/bar", nil) + Expect(err).NotTo(HaveOccurred()) + + via := []*http.Request{originalReq} + + err = PrepareRedirect(redirectReq, via) + + Expect(err).NotTo(HaveOccurred()) + Expect(redirectReq.Header.Get("Authorization")).To(Equal("my-auth-token")) + }) + + It("fails after one redirect", func() { + firstReq, err := http.NewRequest("GET", "/foo", nil) + Expect(err).NotTo(HaveOccurred()) + + secondReq, err := http.NewRequest("GET", "/manchu", nil) + Expect(err).NotTo(HaveOccurred()) + + redirectReq, err := http.NewRequest("GET", "/bar", nil) + Expect(err).NotTo(HaveOccurred()) + + via := []*http.Request{firstReq, secondReq} + + err = PrepareRedirect(redirectReq, via) + + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("WrapNetworkErrors", func() { + It("replaces http unknown authority errors with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &url.Error{Err: x509.UnknownAuthorityError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("replaces http hostname errors with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &url.Error{Err: x509.HostnameError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("replaces http certificate invalid errors with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &url.Error{Err: x509.CertificateInvalidError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("replaces websocket unknown authority errors with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &websocket.DialError{Err: x509.UnknownAuthorityError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("replaces websocket hostname with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &websocket.DialError{Err: x509.HostnameError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("replaces http websocket certificate invalid errors with InvalidSSLCert errors", func() { + err, ok := WrapNetworkErrors("example.com", &websocket.DialError{Err: x509.CertificateInvalidError{}}).(*errors.InvalidSSLCert) + Expect(ok).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("provides a nice message for connection errors", func() { + underlyingErr := syscall.Errno(61) + err := WrapNetworkErrors("example.com", &url.Error{Err: &net.OpError{Err: underlyingErr}}) + Expect(err.Error()).To(ContainSubstring("Error performing request")) + }) + + It("wraps other errors in a generic error type", func() { + err := WrapNetworkErrors("example.com", errors.New("whatever")) + Expect(err).To(HaveOccurred()) + + _, ok := err.(*errors.InvalidSSLCert) + Expect(ok).To(BeFalse()) + }) + }) +}) diff --git a/cf/net/net_suite_test.go b/cf/net/net_suite_test.go new file mode 100644 index 00000000000..85e9675d928 --- /dev/null +++ b/cf/net/net_suite_test.go @@ -0,0 +1,19 @@ +package net_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestNet(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Net Suite") +} diff --git a/cf/net/paginated_resources.go b/cf/net/paginated_resources.go new file mode 100644 index 00000000000..0611456e2fc --- /dev/null +++ b/cf/net/paginated_resources.go @@ -0,0 +1,30 @@ +package net + +import ( + "encoding/json" + "reflect" +) + +func NewPaginatedResources(exampleResource interface{}) PaginatedResources { + return PaginatedResources{ + resourceType: reflect.TypeOf(exampleResource), + } +} + +type PaginatedResources struct { + NextURL string `json:"next_url"` + ResourcesBytes json.RawMessage `json:"resources"` + resourceType reflect.Type +} + +func (this PaginatedResources) Resources() ([]interface{}, error) { + slicePtr := reflect.New(reflect.SliceOf(this.resourceType)) + err := json.Unmarshal([]byte(this.ResourcesBytes), slicePtr.Interface()) + slice := reflect.Indirect(slicePtr) + + contents := make([]interface{}, 0, slice.Len()) + for i := 0; i < slice.Len(); i++ { + contents = append(contents, slice.Index(i).Interface()) + } + return contents, err +} diff --git a/cf/net/progress_reader.go b/cf/net/progress_reader.go new file mode 100644 index 00000000000..d1b9f9092b8 --- /dev/null +++ b/cf/net/progress_reader.go @@ -0,0 +1,78 @@ +package net + +import ( + "io" + "os" + "time" + + "github.com/cloudfoundry/cli/cf/formatters" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type ProgressReader struct { + ioReadSeeker io.ReadSeeker + bytesRead int64 + total int64 + quit chan bool + ui terminal.UI + outputInterval time.Duration +} + +func NewProgressReader(readSeeker io.ReadSeeker, ui terminal.UI, outputInterval time.Duration) *ProgressReader { + return &ProgressReader{ + ioReadSeeker: readSeeker, + ui: ui, + outputInterval: outputInterval, + } +} + +func (progressReader *ProgressReader) Read(p []byte) (int, error) { + if progressReader.ioReadSeeker == nil { + return 0, os.ErrInvalid + } + + n, err := progressReader.ioReadSeeker.Read(p) + + if progressReader.total > int64(0) { + if n > 0 { + if progressReader.quit == nil { + progressReader.quit = make(chan bool) + go progressReader.printProgress(progressReader.quit) + } + + progressReader.bytesRead += int64(n) + + if progressReader.total == progressReader.bytesRead { + progressReader.quit <- true + return n, err + } + } + } + + return n, err +} + +func (progressReader *ProgressReader) Seek(offset int64, whence int) (int64, error) { + return progressReader.ioReadSeeker.Seek(offset, whence) +} + +func (progressReader *ProgressReader) printProgress(quit chan bool) { + timer := time.NewTicker(progressReader.outputInterval) + + for { + select { + case <-quit: + //The spaces are there to ensure we overwrite the entire line + //before using the terminal printer to output Done Uploading + progressReader.ui.PrintCapturingNoOutput("\r ") + progressReader.ui.Say("\rDone uploading") + return + case <-timer.C: + progressReader.ui.PrintCapturingNoOutput("\r%s uploaded...", formatters.ByteSize(progressReader.bytesRead)) + } + } +} + +func (progressReader *ProgressReader) SetTotalSize(size int64) { + progressReader.total = size +} diff --git a/cf/net/progress_reader_test.go b/cf/net/progress_reader_test.go new file mode 100644 index 00000000000..b03e60e6345 --- /dev/null +++ b/cf/net/progress_reader_test.go @@ -0,0 +1,67 @@ +package net_test + +import ( + "os" + "time" + + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/net" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ProgressReader", func() { + + var ( + testFile *os.File + err error + progressReader *ProgressReader + ui *testterm.FakeUI + b []byte + fileStat os.FileInfo + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + testFile, err = os.Open("../../fixtures/test.file") + Expect(err).ToNot(HaveOccurred()) + fileStat, err = testFile.Stat() + Expect(err).ToNot(HaveOccurred()) + + b = make([]byte, 1024) + progressReader = NewProgressReader(testFile, ui, 1*time.Millisecond) + progressReader.SetTotalSize(fileStat.Size()) + }) + + It("prints progress while content is being read", func() { + for { + time.Sleep(50 * time.Microsecond) + _, err := progressReader.Read(b) + if err != nil { + break + } + } + + Expect(ui.UncapturedOutput).To(ContainSubstrings([]string{"\r", "uploaded..."})) + Expect(ui.UncapturedOutput).To(ContainSubstrings([]string{"\r "})) + Expect(ui.Outputs).To(ContainSubstrings([]string{"\rDone "})) + }) + + It("reads the correct number of bytes", func() { + bytesRead := 0 + + for { + n, err := progressReader.Read(b) + if err != nil { + break + } + + bytesRead += n + } + + Expect(int64(bytesRead)).To(Equal(fileStat.Size())) + }) +}) diff --git a/cf/net/ssl.go b/cf/net/ssl.go new file mode 100644 index 00000000000..1fc130cbe87 --- /dev/null +++ b/cf/net/ssl.go @@ -0,0 +1,25 @@ +package net + +import ( + "crypto/tls" + "crypto/x509" +) + +func NewTLSConfig(trustedCerts []tls.Certificate, disableSSL bool) (TLSConfig *tls.Config) { + TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS10, + } + + if len(trustedCerts) > 0 { + certPool := x509.NewCertPool() + for _, tlsCert := range trustedCerts { + cert, _ := x509.ParseCertificate(tlsCert.Certificate[0]) + certPool.AddCert(cert) + } + TLSConfig.RootCAs = certPool + } + + TLSConfig.InsecureSkipVerify = disableSSL + + return +} diff --git a/cf/net/uaa_gateway.go b/cf/net/uaa_gateway.go new file mode 100644 index 00000000000..90760426d38 --- /dev/null +++ b/cf/net/uaa_gateway.go @@ -0,0 +1,29 @@ +package net + +import ( + "encoding/json" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type uaaErrorResponse struct { + Code string `json:"error"` + Description string `json:"error_description"` +} + +var uaaErrorHandler = func(statusCode int, body []byte) error { + response := uaaErrorResponse{} + json.Unmarshal(body, &response) + + if response.Code == "invalid_token" { + return errors.NewInvalidTokenError(response.Description) + } else { + return errors.NewHttpError(statusCode, response.Code, response.Description) + } +} + +func NewUAAGateway(config core_config.Reader, ui terminal.UI) Gateway { + return newGateway(uaaErrorHandler, config, ui) +} diff --git a/cf/net/uaa_gateway_test.go b/cf/net/uaa_gateway_test.go new file mode 100644 index 00000000000..c199ba5df7d --- /dev/null +++ b/cf/net/uaa_gateway_test.go @@ -0,0 +1,44 @@ +package net_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + . "github.com/cloudfoundry/cli/cf/net" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var failingUAARequest = func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + jsonResponse := `{ "error": "foo", "error_description": "The foo is wrong..." }` + fmt.Fprintln(writer, jsonResponse) +} + +var _ = Describe("UAA Gateway", func() { + var gateway Gateway + var config core_config.Reader + + BeforeEach(func() { + config = testconfig.NewRepository() + gateway = NewUAAGateway(config, &testterm.FakeUI{}) + }) + + It("parses error responses", func() { + ts := httptest.NewTLSServer(http.HandlerFunc(failingUAARequest)) + defer ts.Close() + gateway.SetTrustedCerts(ts.TLS.Certificates) + + request, apiErr := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) + _, apiErr = gateway.PerformRequest(request) + + Expect(apiErr).NotTo(BeNil()) + Expect(apiErr.Error()).To(ContainSubstring("The foo is wrong")) + Expect(apiErr.(errors.HttpError).ErrorCode()).To(ContainSubstring("foo")) + }) +}) diff --git a/cf/net/warnings_collector.go b/cf/net/warnings_collector.go new file mode 100644 index 00000000000..1bb9034450e --- /dev/null +++ b/cf/net/warnings_collector.go @@ -0,0 +1,59 @@ +package net + +import ( + "os" + "strings" + + "github.com/cloudfoundry/cli/cf/terminal" +) + +type WarningsCollector struct { + ui terminal.UI + warning_producers []WarningProducer +} + +type WarningProducer interface { + Warnings() []string +} + +func NewWarningsCollector(ui terminal.UI, warning_producers ...WarningProducer) (warnings_collector WarningsCollector) { + warnings_collector.ui = ui + warnings_collector.warning_producers = warning_producers + return +} + +func (warnings_collector WarningsCollector) PrintWarnings() { + warnings := []string{} + for _, warning_producer := range warnings_collector.warning_producers { + for _, warning := range warning_producer.Warnings() { + warnings = append(warnings, warning) + } + } + + if os.Getenv("CF_RAISE_ERROR_ON_WARNINGS") != "" { + if len(warnings) > 0 { + panic(strings.Join(warnings, "\n")) + } + } + + warnings = warnings_collector.removeDuplicates(warnings) + + for _, warning := range warnings { + warnings_collector.ui.Warn(warning) + } +} + +func (warnings_collector WarningsCollector) removeDuplicates(stringArray []string) []string { + length := len(stringArray) - 1 + for i := 0; i < length; i++ { + for j := i + 1; j <= length; j++ { + if stringArray[i] == stringArray[j] { + stringArray[j] = stringArray[length] + stringArray = stringArray[0:length] + length-- + j-- + } + } + } + return stringArray +} diff --git a/cf/net/warnings_collector_test.go b/cf/net/warnings_collector_test.go new file mode 100644 index 00000000000..cfa487cf65f --- /dev/null +++ b/cf/net/warnings_collector_test.go @@ -0,0 +1,91 @@ +package net_test + +import ( + "os" + + "github.com/cloudfoundry/cli/cf/net" + testnet "github.com/cloudfoundry/cli/testhelpers/net" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("WarningsCollector", func() { + var ( + ui *testterm.FakeUI + oldRaiseErrorValue string + warningsCollector net.WarningsCollector + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + Describe("PrintWarnings", func() { + BeforeEach(func() { + oldRaiseErrorValue = os.Getenv("CF_RAISE_ERROR_ON_WARNINGS") + }) + + AfterEach(func() { + os.Setenv("CF_RAISE_ERROR_ON_WARNINGS", oldRaiseErrorValue) + }) + + Context("when the CF_RAISE_ERROR_ON_WARNINGS environment variable is set", func() { + BeforeEach(func() { + os.Setenv("CF_RAISE_ERROR_ON_WARNINGS", "true") + }) + + Context("when there are warnings", func() { + BeforeEach(func() { + warning_producer_one := testnet.NewWarningProducer([]string{"Hello", "Darling"}) + warning_producer_two := testnet.NewWarningProducer([]string{"Goodbye", "Sweetie"}) + warning_producer_three := testnet.NewWarningProducer(nil) + warningsCollector = net.NewWarningsCollector(ui, warning_producer_one, warning_producer_two, warning_producer_three) + }) + + It("panics with an error that contains all the warnings", func() { + Expect(warningsCollector.PrintWarnings).To(Panic()) + }) + }) + + Context("when there are no warnings", func() { + BeforeEach(func() { + warningsCollector = net.NewWarningsCollector(ui) + }) + + It("does not panic", func() { + Expect(warningsCollector.PrintWarnings).NotTo(Panic()) + }) + + }) + }) + + Context("when the CF_RAISE_ERROR_ON_WARNINGS environment variable is not set", func() { + BeforeEach(func() { + os.Setenv("CF_RAISE_ERROR_ON_WARNINGS", "") + }) + + It("does not panic", func() { + warning_producer_one := testnet.NewWarningProducer([]string{"Hello", "Darling"}) + warning_producer_two := testnet.NewWarningProducer([]string{"Goodbye", "Sweetie"}) + warning_producer_three := testnet.NewWarningProducer(nil) + warningsCollector := net.NewWarningsCollector(ui, warning_producer_one, warning_producer_two, warning_producer_three) + + Expect(warningsCollector.PrintWarnings).NotTo(Panic()) + }) + + It("does not print out duplicate warnings", func() { + warning_producer_one := testnet.NewWarningProducer([]string{"Hello Darling"}) + warning_producer_two := testnet.NewWarningProducer([]string{"Hello Darling"}) + warningsCollector := net.NewWarningsCollector(ui, warning_producer_one, warning_producer_two) + + warningsCollector.PrintWarnings() + Expect(len(ui.Outputs)).To(Equal(1)) + Expect(ui.Outputs).To(ContainSubstrings([]string{"Hello Darling"})) + }) + }) + }) + +}) diff --git a/cf/panic_printer/panic_printer.go b/cf/panic_printer/panic_printer.go new file mode 100644 index 00000000000..2b076c972e4 --- /dev/null +++ b/cf/panic_printer/panic_printer.go @@ -0,0 +1,61 @@ +package panic_printer + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/terminal" +) + +var UI terminal.UI + +func DisplayCrashDialog(err interface{}, commandArgs string, stackTrace string) { + if err != nil && err != terminal.QuietPanic { + switch err := err.(type) { + case errors.Exception: + if err.DisplayCrashDialog { + printCrashDialog(err.Message, commandArgs, stackTrace) + } else { + fmt.Println(err.Message) + } + case error: + printCrashDialog(err.Error(), commandArgs, stackTrace) + case string: + printCrashDialog(err, commandArgs, stackTrace) + default: + printCrashDialog("An unexpected type of error", commandArgs, stackTrace) + } + } +} + +func CrashDialog(errorMessage string, commandArgs string, stackTrace string) string { + formattedString := ` + + Aww shucks. + + Something completely unexpected happened. This is a bug in %s. + Please file this bug : https://github.com/cloudfoundry/cli/issues + Tell us that you ran this command: + + %s + + using this version of the CLI: + + %s + + and that this error occurred: + + %s + + and this stack trace: + + %s +` + + return fmt.Sprintf(formattedString, cf.Name(), commandArgs, cf.Version, errorMessage, stackTrace) +} + +func printCrashDialog(errorMessage string, commandArgs string, stackTrace string) { + UI.Say(CrashDialog(errorMessage, commandArgs, stackTrace)) +} diff --git a/cf/panic_printer/panic_printer_suite_test.go b/cf/panic_printer/panic_printer_suite_test.go new file mode 100644 index 00000000000..1c379e12ffa --- /dev/null +++ b/cf/panic_printer/panic_printer_suite_test.go @@ -0,0 +1,19 @@ +package panic_printer_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPanicHandler(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "PanicHandler Suite") +} diff --git a/cf/panic_printer/panic_printer_test.go b/cf/panic_printer/panic_printer_test.go new file mode 100644 index 00000000000..58c9cf8d47b --- /dev/null +++ b/cf/panic_printer/panic_printer_test.go @@ -0,0 +1,60 @@ +package panic_printer_test + +import ( + "github.com/cloudfoundry/cli/cf" + . "github.com/cloudfoundry/cli/cf/panic_printer" + "github.com/cloudfoundry/cli/cf/terminal" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Panic Printer", func() { + var ui *testterm.FakeUI + + BeforeEach(func() { + UI = &testterm.FakeUI{} + ui = UI.(*testterm.FakeUI) + }) + + Describe("DisplayCrashDialog", func() { + Context("when given an err set to QuietPanic", func() { + It("should not print anything", func() { + err := terminal.QuietPanic + DisplayCrashDialog(err, "some command", "some trace") + Expect(len(ui.Outputs)).To(Equal(0)) + }) + }) + }) + + Describe("CrashDialog", func() { + var errMsg = "this is an error" + var commandArgs = "command line arguments" + var stackTrace = "1000 bottles of beer" + + It("should return a string containing the default error text", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring("Please file this bug : https://github.com/cloudfoundry/cli/issues")) + }) + + It("should return the command name", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring(cf.Name())) + }) + + It("should return the inputted arguments", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring("command line arguments")) + }) + + It("should return the specific error message", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring("this is an error")) + }) + + It("should return the stack trace", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring("1000 bottles of beer")) + }) + + It("should print the cli version", func() { + Expect(CrashDialog(errMsg, commandArgs, stackTrace)).To(ContainSubstring(cf.Version)) + }) + }) +}) diff --git a/cf/requirements/api_endpoint.go b/cf/requirements/api_endpoint.go new file mode 100644 index 00000000000..43df941df26 --- /dev/null +++ b/cf/requirements/api_endpoint.go @@ -0,0 +1,33 @@ +package requirements + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type ApiEndpointRequirement struct { + ui terminal.UI + config core_config.Reader +} + +func NewApiEndpointRequirement(ui terminal.UI, config core_config.Reader) ApiEndpointRequirement { + return ApiEndpointRequirement{ui, config} +} + +func (req ApiEndpointRequirement) Execute() (success bool) { + if req.config.ApiEndpoint() == "" { + loginTip := terminal.CommandColor(fmt.Sprintf(T("{{.CFName}} login", map[string]interface{}{"CFName": cf.Name()}))) + apiTip := terminal.CommandColor(fmt.Sprintf(T("{{.CFName}} api", map[string]interface{}{"CFName": cf.Name()}))) + req.ui.Say(T("No API endpoint set. Use '{{.LoginTip}}' or '{{.APITip}}' to target an endpoint.", + map[string]interface{}{ + "LoginTip": loginTip, + "APITip": apiTip, + })) + return false + } + return true +} diff --git a/cf/requirements/api_endpoint_test.go b/cf/requirements/api_endpoint_test.go new file mode 100644 index 00000000000..f5f9d0bb042 --- /dev/null +++ b/cf/requirements/api_endpoint_test.go @@ -0,0 +1,39 @@ +package requirements_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/requirements" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("ApiEndpointRequirement", func() { + var ( + ui *testterm.FakeUI + config core_config.Repository + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + config = testconfig.NewRepository() + }) + + It("succeeds when given a config with an API endpoint", func() { + config.SetApiEndpoint("api.example.com") + req := NewApiEndpointRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeTrue()) + }) + + It("fails when given a config without an API endpoint", func() { + req := NewApiEndpointRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeFalse()) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"No API endpoint"})) + }) +}) diff --git a/cf/requirements/application.go b/cf/requirements/application.go new file mode 100644 index 00000000000..8f554f3d09f --- /dev/null +++ b/cf/requirements/application.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api/applications" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type ApplicationRequirement interface { + Requirement + GetApplication() models.Application +} + +type applicationApiRequirement struct { + name string + ui terminal.UI + appRepo applications.ApplicationRepository + application models.Application +} + +func NewApplicationRequirement(name string, ui terminal.UI, aR applications.ApplicationRepository) (req *applicationApiRequirement) { + req = new(applicationApiRequirement) + req.name = name + req.ui = ui + req.appRepo = aR + return +} + +func (req *applicationApiRequirement) Execute() (success bool) { + var apiErr error + req.application, apiErr = req.appRepo.Read(req.name) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *applicationApiRequirement) GetApplication() models.Application { + return req.application +} diff --git a/cf/requirements/application_test.go b/cf/requirements/application_test.go new file mode 100644 index 00000000000..14ae47a033b --- /dev/null +++ b/cf/requirements/application_test.go @@ -0,0 +1,43 @@ +package requirements_test + +import ( + testApplication "github.com/cloudfoundry/cli/cf/api/applications/fakes" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplicationRequirement", func() { + var ui *testterm.FakeUI + var appRepo *testApplication.FakeApplicationRepository + + BeforeEach(func() { + ui = new(testterm.FakeUI) + appRepo = &testApplication.FakeApplicationRepository{} + }) + + It("succeeds when an app with the given name exists", func() { + app := models.Application{} + app.Name = "my-app" + app.Guid = "my-app-guid" + appRepo.ReadReturns.App = app + + appReq := NewApplicationRequirement("foo", ui, appRepo) + + Expect(appReq.Execute()).To(BeTrue()) + Expect(appRepo.ReadArgs.Name).To(Equal("foo")) + Expect(appReq.GetApplication()).To(Equal(app)) + }) + + It("fails when an app with the given name cannot be found", func() { + appRepo.ReadReturns.Error = errors.NewModelNotFoundError("app", "foo") + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewApplicationRequirement("foo", ui, appRepo).Execute() + }) + }) +}) diff --git a/cf/requirements/buildpack.go b/cf/requirements/buildpack.go new file mode 100644 index 00000000000..c1a0d087fd3 --- /dev/null +++ b/cf/requirements/buildpack.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type BuildpackRequirement interface { + Requirement + GetBuildpack() models.Buildpack +} + +type buildpackApiRequirement struct { + name string + ui terminal.UI + buildpackRepo api.BuildpackRepository + buildpack models.Buildpack +} + +func NewBuildpackRequirement(name string, ui terminal.UI, bR api.BuildpackRepository) (req *buildpackApiRequirement) { + req = new(buildpackApiRequirement) + req.name = name + req.ui = ui + req.buildpackRepo = bR + return +} + +func (req *buildpackApiRequirement) Execute() (success bool) { + var apiErr error + req.buildpack, apiErr = req.buildpackRepo.FindByName(req.name) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *buildpackApiRequirement) GetBuildpack() models.Buildpack { + return req.buildpack +} diff --git a/cf/requirements/buildpack_test.go b/cf/requirements/buildpack_test.go new file mode 100644 index 00000000000..df735868165 --- /dev/null +++ b/cf/requirements/buildpack_test.go @@ -0,0 +1,40 @@ +package requirements_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildpackRequirement", func() { + var ( + ui *testterm.FakeUI + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + It("succeeds when a buildpack with the given name exists", func() { + buildpack := models.Buildpack{Name: "my-buildpack"} + buildpackRepo := &testapi.FakeBuildpackRepository{FindByNameBuildpack: buildpack} + + buildpackReq := NewBuildpackRequirement("my-buildpack", ui, buildpackRepo) + + Expect(buildpackReq.Execute()).To(BeTrue()) + Expect(buildpackRepo.FindByNameName).To(Equal("my-buildpack")) + Expect(buildpackReq.GetBuildpack()).To(Equal(buildpack)) + }) + + It("fails when the buildpack cannot be found", func() { + buildpackRepo := &testapi.FakeBuildpackRepository{FindByNameNotFound: true} + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewBuildpackRequirement("foo", ui, buildpackRepo).Execute() + }) + }) +}) diff --git a/cf/requirements/cc_api_version.go b/cf/requirements/cc_api_version.go new file mode 100644 index 00000000000..8e0e67e8bff --- /dev/null +++ b/cf/requirements/cc_api_version.go @@ -0,0 +1,79 @@ +package requirements + +import ( + "strconv" + "strings" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type CCApiVersionRequirement struct { + ui terminal.UI + config core_config.Reader + commandName string + major int + minor int + patch int +} + +func NewCCApiVersionRequirement(ui terminal.UI, config core_config.Reader, commandName string, major, minor, patch int) CCApiVersionRequirement { + return CCApiVersionRequirement{ui, config, commandName, major, minor, patch} +} + +func (req CCApiVersionRequirement) Execute() bool { + versions := strings.Split(req.config.ApiVersion(), ".") + + if len(versions) != 3 { + return true + } + + majorStr := versions[0] + major, err := strconv.Atoi(majorStr) + if err != nil { + return true + } + + minorStr := versions[1] + minor, err := strconv.Atoi(minorStr) + if err != nil { + return true + } + + patchStr := versions[2] + patch, err := strconv.Atoi(patchStr) + if err != nil { + return true + } + + if major > req.major { + return true + } else if major < req.major { + return false + } + + if minor > req.minor { + return true + } else if minor < req.minor { + return false + } + + if patch >= req.patch { + return true + } + + req.ui.Say(terminal.FailureColor(T("FAILED"))) + req.ui.Say(T("Current CF CLI version {{.Version}}", map[string]interface{}{"Version": cf.Version})) + req.ui.Say(T("Current CF API version {{.ApiVersion}}", map[string]interface{}{"ApiVersion": req.config.ApiVersion()})) + req.ui.Say(T("To use the {{.CommandName}} feature, you need to upgrade the CF API to at least {{.MinApiVersionMajor}}.{{.MinApiVersionMinor}}.{{.MinApiVersionPatch}}", + map[string]interface{}{ + "CommandName": req.commandName, + "MinApiVersionMajor": req.major, + "MinApiVersionMinor": req.minor, + "MinApiVersionPatch": req.patch, + })) + + return false +} diff --git a/cf/requirements/cc_api_version_test.go b/cf/requirements/cc_api_version_test.go new file mode 100644 index 00000000000..d9226d61b6c --- /dev/null +++ b/cf/requirements/cc_api_version_test.go @@ -0,0 +1,214 @@ +package requirements_test + +import ( + "fmt" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/requirements" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CcApiVersion", func() { + var ( + ui *testterm.FakeUI + config core_config.Repository + req CCApiVersionRequirement + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + config = testconfig.NewRepository() + }) + + Describe("success", func() { + Describe("when the cc api version has only major version", func() { + BeforeEach(func() { + config.SetApiVersion("1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 0, 0) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc api version has only major and minor versions", func() { + BeforeEach(func() { + config.SetApiVersion("1.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 1, 0) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc api version has three dots", func() { + BeforeEach(func() { + config.SetApiVersion("1.1.1.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 1, 1) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc major version is trash", func() { + BeforeEach(func() { + config.SetApiVersion("garbage.1.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 0, 1, 1) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc minor version is trash", func() { + BeforeEach(func() { + config.SetApiVersion("1.garbage.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 0, 1) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc patch version is trash", func() { + BeforeEach(func() { + config.SetApiVersion("1.1.garbage") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 1, 0) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when cc major version is greater than the required major version", func() { + BeforeEach(func() { + config.SetApiVersion("2.0.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 0, 0) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when cc minor version is greater than the require minor version", func() { + BeforeEach(func() { + config.SetApiVersion("2.1.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 0, 0) + + }) + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when cc patch version is greater than the required patch version", func() { + BeforeEach(func() { + config.SetApiVersion("2.1.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 1, 0) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc major, minor, and patch are equal to the required versions", func() { + BeforeEach(func() { + config.SetApiVersion("2.1.1") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 1, 1) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the cc major version is not higher than require and the minor and patch are", func() { + BeforeEach(func() { + config.SetApiVersion("9.2.2") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 9, 9) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Describe("when the major and minor versions are higher than required and the patch is not", func() { + BeforeEach(func() { + config.SetApiVersion("9.9.2") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 2, 9) + }) + + It("should pass", func() { + Expect(req.Execute()).To(BeTrue()) + }) + }) + }) + + Describe("failure", func() { + Describe("when cc major version is less than the required major version", func() { + BeforeEach(func() { + config.SetApiVersion("1.0.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 0, 0) + }) + + It("should fail", func() { + Expect(req.Execute()).To(BeFalse()) + }) + }) + + Describe("when cc minor version is less than the required minor version", func() { + BeforeEach(func() { + config.SetApiVersion("2.0.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 1, 0) + }) + + It("should fail", func() { + Expect(req.Execute()).To(BeFalse()) + }) + }) + + Describe("when cc patch version is less than the required patch version", func() { + BeforeEach(func() { + config.SetApiVersion("2.1.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 2, 1, 1) + }) + + It("should fail", func() { + Expect(req.Execute()).To(BeFalse()) + }) + }) + + Describe("output", func() { + BeforeEach(func() { + config.SetApiVersion("1.1.0") + req = NewCCApiVersionRequirement(ui, config, "command-name", 1, 1, 1) + req.Execute() + }) + + It("should write to the ui", func() { + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{fmt.Sprintf("Current CF CLI version %s", cf.Version)}, + []string{"Current CF API version 1.1.0"}, + []string{"To use the command-name feature, you need to upgrade the CF API to at least 1.1.1"}, + )) + }) + }) + }) +}) diff --git a/cf/requirements/domain.go b/cf/requirements/domain.go new file mode 100644 index 00000000000..9fd2600ce2d --- /dev/null +++ b/cf/requirements/domain.go @@ -0,0 +1,46 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type DomainRequirement interface { + Requirement + GetDomain() models.DomainFields +} + +type domainApiRequirement struct { + name string + ui terminal.UI + config core_config.Reader + domainRepo api.DomainRepository + domain models.DomainFields +} + +func NewDomainRequirement(name string, ui terminal.UI, config core_config.Reader, domainRepo api.DomainRepository) (req *domainApiRequirement) { + req = new(domainApiRequirement) + req.name = name + req.ui = ui + req.config = config + req.domainRepo = domainRepo + return +} + +func (req *domainApiRequirement) Execute() bool { + var apiErr error + req.domain, apiErr = req.domainRepo.FindByNameInOrg(req.name, req.config.OrganizationFields().Guid) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *domainApiRequirement) GetDomain() models.DomainFields { + return req.domain +} diff --git a/cf/requirements/domain_test.go b/cf/requirements/domain_test.go new file mode 100644 index 00000000000..2a91c763feb --- /dev/null +++ b/cf/requirements/domain_test.go @@ -0,0 +1,55 @@ +package requirements_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/errors" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DomainRequirement", func() { + var config core_config.ReadWriter + var ui *testterm.FakeUI + + BeforeEach(func() { + ui = new(testterm.FakeUI) + config = testconfig.NewRepository() + config.SetOrganizationFields(models.OrganizationFields{Guid: "the-org-guid"}) + }) + + It("succeeds when the domain is found", func() { + domain := models.DomainFields{Name: "example.com", Guid: "domain-guid"} + domainRepo := &testapi.FakeDomainRepository{FindByNameInOrgDomain: domain} + domainReq := NewDomainRequirement("example.com", ui, config, domainRepo) + success := domainReq.Execute() + + Expect(success).To(BeTrue()) + Expect(domainRepo.FindByNameInOrgName).To(Equal("example.com")) + Expect(domainRepo.FindByNameInOrgGuid).To(Equal("the-org-guid")) + Expect(domainReq.GetDomain()).To(Equal(domain)) + }) + + It("fails when the domain is not found", func() { + domainRepo := &testapi.FakeDomainRepository{FindByNameInOrgApiResponse: errors.NewModelNotFoundError("Domain", "")} + domainReq := NewDomainRequirement("example.com", ui, config, domainRepo) + + testassert.AssertPanic(testterm.QuietPanic, func() { + domainReq.Execute() + }) + }) + + It("fails when an error occurs fetching the domain", func() { + domainRepo := &testapi.FakeDomainRepository{FindByNameInOrgApiResponse: errors.NewWithError("", errors.New(""))} + domainReq := NewDomainRequirement("example.com", ui, config, domainRepo) + + testassert.AssertPanic(testterm.QuietPanic, func() { + domainReq.Execute() + }) + }) +}) diff --git a/cf/requirements/factory.go b/cf/requirements/factory.go new file mode 100644 index 00000000000..c0ded65082e --- /dev/null +++ b/cf/requirements/factory.go @@ -0,0 +1,132 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type Requirement interface { + Execute() (success bool) +} + +type Factory interface { + NewApplicationRequirement(name string) ApplicationRequirement + NewServiceInstanceRequirement(name string) ServiceInstanceRequirement + NewLoginRequirement() Requirement + NewSpaceRequirement(name string) SpaceRequirement + NewTargetedSpaceRequirement() Requirement + NewTargetedOrgRequirement() TargetedOrgRequirement + NewOrganizationRequirement(name string) OrganizationRequirement + NewDomainRequirement(name string) DomainRequirement + NewUserRequirement(username string) UserRequirement + NewBuildpackRequirement(buildpack string) BuildpackRequirement + NewApiEndpointRequirement() Requirement + NewMinCCApiVersionRequirement(commandName string, major, minor, patch int) Requirement +} + +type apiRequirementFactory struct { + ui terminal.UI + config core_config.Reader + repoLocator api.RepositoryLocator +} + +func NewFactory(ui terminal.UI, config core_config.Reader, repoLocator api.RepositoryLocator) (factory apiRequirementFactory) { + return apiRequirementFactory{ui, config, repoLocator} +} + +func (f apiRequirementFactory) NewApplicationRequirement(name string) ApplicationRequirement { + return NewApplicationRequirement( + name, + f.ui, + f.repoLocator.GetApplicationRepository(), + ) +} + +func (f apiRequirementFactory) NewServiceInstanceRequirement(name string) ServiceInstanceRequirement { + return NewServiceInstanceRequirement( + name, + f.ui, + f.repoLocator.GetServiceRepository(), + ) +} + +func (f apiRequirementFactory) NewLoginRequirement() Requirement { + return NewLoginRequirement( + f.ui, + f.config, + ) +} + +func (f apiRequirementFactory) NewSpaceRequirement(name string) SpaceRequirement { + return NewSpaceRequirement( + name, + f.ui, + f.repoLocator.GetSpaceRepository(), + ) +} + +func (f apiRequirementFactory) NewTargetedSpaceRequirement() Requirement { + return NewTargetedSpaceRequirement( + f.ui, + f.config, + ) +} + +func (f apiRequirementFactory) NewTargetedOrgRequirement() TargetedOrgRequirement { + return NewTargetedOrgRequirement( + f.ui, + f.config, + ) +} + +func (f apiRequirementFactory) NewOrganizationRequirement(name string) OrganizationRequirement { + return NewOrganizationRequirement( + name, + f.ui, + f.repoLocator.GetOrganizationRepository(), + ) +} + +func (f apiRequirementFactory) NewDomainRequirement(name string) DomainRequirement { + return NewDomainRequirement( + name, + f.ui, + f.config, + f.repoLocator.GetDomainRepository(), + ) +} + +func (f apiRequirementFactory) NewUserRequirement(username string) UserRequirement { + return NewUserRequirement( + username, + f.ui, + f.repoLocator.GetUserRepository(), + ) +} + +func (f apiRequirementFactory) NewBuildpackRequirement(buildpack string) BuildpackRequirement { + return NewBuildpackRequirement( + buildpack, + f.ui, + f.repoLocator.GetBuildpackRepository(), + ) +} + +func (f apiRequirementFactory) NewApiEndpointRequirement() Requirement { + return NewApiEndpointRequirement( + f.ui, + f.config, + ) +} + +func (f apiRequirementFactory) NewMinCCApiVersionRequirement(commandName string, major, minor, patch int) Requirement { + return NewCCApiVersionRequirement( + f.ui, + f.config, + commandName, + major, + minor, + patch, + ) +} diff --git a/cf/requirements/login.go b/cf/requirements/login.go new file mode 100644 index 00000000000..afb7dd64721 --- /dev/null +++ b/cf/requirements/login.go @@ -0,0 +1,29 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type LoginRequirement struct { + ui terminal.UI + config core_config.Reader + apiEndpointRequirement ApiEndpointRequirement +} + +func NewLoginRequirement(ui terminal.UI, config core_config.Reader) LoginRequirement { + return LoginRequirement{ui, config, ApiEndpointRequirement{ui, config}} +} + +func (req LoginRequirement) Execute() (success bool) { + if !req.apiEndpointRequirement.Execute() { + return false + } + + if !req.config.IsLoggedIn() { + req.ui.Say(terminal.NotLoggedInText()) + return false + } + + return true +} diff --git a/cf/requirements/login_test.go b/cf/requirements/login_test.go new file mode 100644 index 00000000000..349dbedd339 --- /dev/null +++ b/cf/requirements/login_test.go @@ -0,0 +1,49 @@ +package requirements_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/requirements" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("LoginRequirement", func() { + var ui *testterm.FakeUI + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + It("succeeds when given a config with an API endpoint and authentication", func() { + config := testconfig.NewRepositoryWithAccessToken(core_config.TokenInfo{Username: "my-user"}) + config.SetApiEndpoint("api.example.com") + req := NewLoginRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeTrue()) + }) + + It("fails when given a config with only an API endpoint", func() { + config := testconfig.NewRepository() + config.SetApiEndpoint("api.example.com") + req := NewLoginRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeFalse()) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"Not logged in."})) + }) + + It("fails when given a config with neither an API endpoint nor authentication", func() { + config := testconfig.NewRepository() + req := NewLoginRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeFalse()) + + Expect(ui.Outputs).To(ContainSubstrings([]string{"No API endpoint"})) + Expect(ui.Outputs).ToNot(ContainSubstrings([]string{"Not logged in."})) + }) +}) diff --git a/cf/requirements/organization.go b/cf/requirements/organization.go new file mode 100644 index 00000000000..8e187c94024 --- /dev/null +++ b/cf/requirements/organization.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api/organizations" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type OrganizationRequirement interface { + Requirement + GetOrganization() models.Organization +} + +type organizationApiRequirement struct { + name string + ui terminal.UI + orgRepo organizations.OrganizationRepository + org models.Organization +} + +func NewOrganizationRequirement(name string, ui terminal.UI, sR organizations.OrganizationRepository) (req *organizationApiRequirement) { + req = new(organizationApiRequirement) + req.name = name + req.ui = ui + req.orgRepo = sR + return +} + +func (req *organizationApiRequirement) Execute() (success bool) { + var apiErr error + req.org, apiErr = req.orgRepo.FindByName(req.name) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *organizationApiRequirement) GetOrganization() models.Organization { + return req.org +} diff --git a/cf/requirements/organization_test.go b/cf/requirements/organization_test.go new file mode 100644 index 00000000000..e6b85e82a89 --- /dev/null +++ b/cf/requirements/organization_test.go @@ -0,0 +1,50 @@ +package requirements_test + +import ( + "errors" + + test_org "github.com/cloudfoundry/cli/cf/api/organizations/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("OrganizationRequirement", func() { + var ( + ui *testterm.FakeUI + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + Context("when an org with the given name exists", func() { + It("succeeds", func() { + org := models.Organization{} + org.Name = "my-org-name" + org.Guid = "my-org-guid" + orgRepo := &test_org.FakeOrganizationRepository{} + orgReq := NewOrganizationRequirement("my-org-name", ui, orgRepo) + + orgRepo.ListOrgsReturns([]models.Organization{org}, nil) + orgRepo.FindByNameReturns(org, nil) + + Expect(orgReq.Execute()).To(BeTrue()) + Expect(orgRepo.FindByNameArgsForCall(0)).To(Equal("my-org-name")) + Expect(orgReq.GetOrganization()).To(Equal(org)) + }) + }) + + It("fails when the org with the given name does not exist", func() { + orgRepo := &test_org.FakeOrganizationRepository{} + + orgRepo.FindByNameReturns(models.Organization{}, errors.New("not found")) + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewOrganizationRequirement("foo", ui, orgRepo).Execute() + }) + }) +}) diff --git a/cf/requirements/requirements_suite_test.go b/cf/requirements/requirements_suite_test.go new file mode 100644 index 00000000000..082d466af0a --- /dev/null +++ b/cf/requirements/requirements_suite_test.go @@ -0,0 +1,19 @@ +package requirements_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRequirements(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Requirements Suite") +} diff --git a/cf/requirements/service_instance.go b/cf/requirements/service_instance.go new file mode 100644 index 00000000000..8122b7c9ffa --- /dev/null +++ b/cf/requirements/service_instance.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type ServiceInstanceRequirement interface { + Requirement + GetServiceInstance() models.ServiceInstance +} + +type serviceInstanceApiRequirement struct { + name string + ui terminal.UI + serviceRepo api.ServiceRepository + serviceInstance models.ServiceInstance +} + +func NewServiceInstanceRequirement(name string, ui terminal.UI, sR api.ServiceRepository) (req *serviceInstanceApiRequirement) { + req = new(serviceInstanceApiRequirement) + req.name = name + req.ui = ui + req.serviceRepo = sR + return +} + +func (req *serviceInstanceApiRequirement) Execute() (success bool) { + var apiErr error + req.serviceInstance, apiErr = req.serviceRepo.FindInstanceByName(req.name) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *serviceInstanceApiRequirement) GetServiceInstance() models.ServiceInstance { + return req.serviceInstance +} diff --git a/cf/requirements/service_instance_test.go b/cf/requirements/service_instance_test.go new file mode 100644 index 00000000000..478b1e9d34c --- /dev/null +++ b/cf/requirements/service_instance_test.go @@ -0,0 +1,45 @@ +package requirements_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServiceInstanceRequirement", func() { + var ( + ui *testterm.FakeUI + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + Context("when a service instance with the given name can be found", func() { + It("succeeds", func() { + instance := models.ServiceInstance{} + instance.Name = "my-service" + instance.Guid = "my-service-guid" + repo := &testapi.FakeServiceRepo{FindInstanceByNameServiceInstance: instance} + + req := NewServiceInstanceRequirement("my-service", ui, repo) + + Expect(req.Execute()).To(BeTrue()) + Expect(repo.FindInstanceByNameName).To(Equal("my-service")) + Expect(req.GetServiceInstance()).To(Equal(instance)) + }) + }) + + Context("when a service instance with the given name can't be found", func() { + It("fails", func() { + repo := &testapi.FakeServiceRepo{FindInstanceByNameNotFound: true} + testassert.AssertPanic(testterm.QuietPanic, func() { + NewServiceInstanceRequirement("foo", ui, repo).Execute() + }) + }) + }) +}) diff --git a/cf/requirements/space.go b/cf/requirements/space.go new file mode 100644 index 00000000000..ddbe9ad5507 --- /dev/null +++ b/cf/requirements/space.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api/spaces" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type SpaceRequirement interface { + Requirement + GetSpace() models.Space +} + +type spaceApiRequirement struct { + name string + ui terminal.UI + spaceRepo spaces.SpaceRepository + space models.Space +} + +func NewSpaceRequirement(name string, ui terminal.UI, sR spaces.SpaceRepository) (req *spaceApiRequirement) { + req = new(spaceApiRequirement) + req.name = name + req.ui = ui + req.spaceRepo = sR + return +} + +func (req *spaceApiRequirement) Execute() (success bool) { + var apiErr error + req.space, apiErr = req.spaceRepo.FindByName(req.name) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *spaceApiRequirement) GetSpace() models.Space { + return req.space +} diff --git a/cf/requirements/space_test.go b/cf/requirements/space_test.go new file mode 100644 index 00000000000..9fbb379ec82 --- /dev/null +++ b/cf/requirements/space_test.go @@ -0,0 +1,45 @@ +package requirements_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SpaceRequirement", func() { + var ( + ui *testterm.FakeUI + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + }) + + Context("when a space with the given name exists", func() { + It("succeeds", func() { + space := models.Space{} + space.Name = "awesome-sauce-space" + space.Guid = "my-space-guid" + spaceRepo := &testapi.FakeSpaceRepository{Spaces: []models.Space{space}} + + spaceReq := NewSpaceRequirement("awesome-sauce-space", ui, spaceRepo) + + Expect(spaceReq.Execute()).To(BeTrue()) + Expect(spaceRepo.FindByNameName).To(Equal("awesome-sauce-space")) + Expect(spaceReq.GetSpace()).To(Equal(space)) + }) + }) + + Context("when a space with the given name does not exist", func() { + It("fails", func() { + spaceRepo := &testapi.FakeSpaceRepository{FindByNameNotFound: true} + testassert.AssertPanic(testterm.QuietPanic, func() { + NewSpaceRequirement("foo", ui, spaceRepo).Execute() + }) + }) + }) +}) diff --git a/cf/requirements/targeted_organization.go b/cf/requirements/targeted_organization.go new file mode 100644 index 00000000000..41aebd67d5e --- /dev/null +++ b/cf/requirements/targeted_organization.go @@ -0,0 +1,38 @@ +package requirements + +import ( + "fmt" + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type TargetedOrgRequirement interface { + Requirement + GetOrganizationFields() models.OrganizationFields +} + +type targetedOrgApiRequirement struct { + ui terminal.UI + config core_config.Reader +} + +func NewTargetedOrgRequirement(ui terminal.UI, config core_config.Reader) TargetedOrgRequirement { + return targetedOrgApiRequirement{ui, config} +} + +func (req targetedOrgApiRequirement) Execute() (success bool) { + if !req.config.HasOrganization() { + message := fmt.Sprintf(T("No org targeted, use '{{.Command}}' to target an org.", map[string]interface{}{"Command": terminal.CommandColor(cf.Name() + " target -o ORG")})) + req.ui.Failed(message) + return false + } + + return true +} + +func (req targetedOrgApiRequirement) GetOrganizationFields() (org models.OrganizationFields) { + return req.config.OrganizationFields() +} diff --git a/cf/requirements/targeted_organization_test.go b/cf/requirements/targeted_organization_test.go new file mode 100644 index 00000000000..1cbeebf6362 --- /dev/null +++ b/cf/requirements/targeted_organization_test.go @@ -0,0 +1,50 @@ +package requirements_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + + . "github.com/cloudfoundry/cli/cf/requirements" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("TargetedOrganizationRequirement", func() { + var ( + ui *testterm.FakeUI + config core_config.ReadWriter + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + config = testconfig.NewRepositoryWithDefaults() + }) + + Context("when the user has an org targeted", func() { + It("succeeds", func() { + req := NewTargetedOrgRequirement(ui, config) + success := req.Execute() + Expect(success).To(BeTrue()) + }) + }) + + Context("when the user does not have an org targeted", func() { + It("fails", func() { + config.SetOrganizationFields(models.OrganizationFields{}) + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewTargetedOrgRequirement(ui, config).Execute() + }) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"No org targeted"}, + )) + }) + }) +}) diff --git a/cf/requirements/targeted_space.go b/cf/requirements/targeted_space.go new file mode 100644 index 00000000000..0de476adf8c --- /dev/null +++ b/cf/requirements/targeted_space.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "fmt" + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type TargetedSpaceRequirement struct { + ui terminal.UI + config core_config.Reader +} + +func NewTargetedSpaceRequirement(ui terminal.UI, config core_config.Reader) TargetedSpaceRequirement { + return TargetedSpaceRequirement{ui, config} +} + +func (req TargetedSpaceRequirement) Execute() (success bool) { + if !req.config.HasOrganization() { + message := fmt.Sprintf(T("No org and space targeted, use '{{.Command}}' to target an org and space", map[string]interface{}{"Command": terminal.CommandColor(cf.Name() + " target -o ORG -s SPACE")})) + req.ui.Failed(message) + return false + } + + if !req.config.HasSpace() { + message := fmt.Sprintf(T("No space targeted, use '{{.Command}}' to target a space", map[string]interface{}{"Command": terminal.CommandColor("cf target -s")})) + req.ui.Failed(message) + return false + } + + return true +} diff --git a/cf/requirements/targeted_space_test.go b/cf/requirements/targeted_space_test.go new file mode 100644 index 00000000000..61b5307c02f --- /dev/null +++ b/cf/requirements/targeted_space_test.go @@ -0,0 +1,48 @@ +package requirements_test + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("TargetedSpaceRequirement", func() { + var ( + ui *testterm.FakeUI + config core_config.ReadWriter + ) + + BeforeEach(func() { + ui = new(testterm.FakeUI) + config = testconfig.NewRepositoryWithDefaults() + }) + + Context("when the user has targeted a space", func() { + It("succeeds", func() { + req := NewTargetedSpaceRequirement(ui, config) + Expect(req.Execute()).To(BeTrue()) + }) + }) + + Context("when the user does not have a space targeted", func() { + It("fails", func() { + config.SetSpaceFields(models.SpaceFields{}) + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewTargetedSpaceRequirement(ui, config).Execute() + }) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"No space targeted"}, + )) + }) + }) +}) diff --git a/cf/requirements/user.go b/cf/requirements/user.go new file mode 100644 index 00000000000..89440cc1ce7 --- /dev/null +++ b/cf/requirements/user.go @@ -0,0 +1,43 @@ +package requirements + +import ( + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +type UserRequirement interface { + Requirement + GetUser() models.UserFields +} + +type userApiRequirement struct { + username string + ui terminal.UI + userRepo api.UserRepository + user models.UserFields +} + +func NewUserRequirement(username string, ui terminal.UI, userRepo api.UserRepository) (req *userApiRequirement) { + req = new(userApiRequirement) + req.username = username + req.ui = ui + req.userRepo = userRepo + return +} + +func (req *userApiRequirement) Execute() (success bool) { + var apiErr error + req.user, apiErr = req.userRepo.FindByUsername(req.username) + + if apiErr != nil { + req.ui.Failed(apiErr.Error()) + return false + } + + return true +} + +func (req *userApiRequirement) GetUser() models.UserFields { + return req.user +} diff --git a/cf/requirements/user_test.go b/cf/requirements/user_test.go new file mode 100644 index 00000000000..1a8db161791 --- /dev/null +++ b/cf/requirements/user_test.go @@ -0,0 +1,49 @@ +package requirements_test + +import ( + testapi "github.com/cloudfoundry/cli/cf/api/fakes" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/cloudfoundry/cli/cf/requirements" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/cli/testhelpers/matchers" +) + +var _ = Describe("UserRequirement", func() { + Context("when a user with the given name can be found", func() { + It("returns the user model", func() { + user := models.UserFields{} + user.Username = "my-user" + user.Guid = "my-user-guid" + + userRepo := &testapi.FakeUserRepository{FindByUsernameUserFields: user} + ui := new(testterm.FakeUI) + + userReq := NewUserRequirement("foo", ui, userRepo) + success := userReq.Execute() + + Expect(success).To(BeTrue()) + Expect(userRepo.FindByUsernameUsername).To(Equal("foo")) + Expect(userReq.GetUser()).To(Equal(user)) + }) + }) + + Context("when a user with the given name cannot be found", func() { + It("panics and prints a failure message", func() { + userRepo := &testapi.FakeUserRepository{FindByUsernameNotFound: true} + ui := new(testterm.FakeUI) + + testassert.AssertPanic(testterm.QuietPanic, func() { + NewUserRequirement("foo", ui, userRepo).Execute() + }) + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"FAILED"}, + []string{"not found"}, + )) + }) + }) +}) diff --git a/cf/terminal/color.go b/cf/terminal/color.go new file mode 100644 index 00000000000..b23741b9e3e --- /dev/null +++ b/cf/terminal/color.go @@ -0,0 +1,137 @@ +package terminal + +import ( + "fmt" + "os" + "regexp" + "runtime" + + "code.google.com/p/go.crypto/ssh/terminal" +) + +type Color uint + +const ( + red Color = 31 + green = 32 + yellow = 33 + // blue = 34 + magenta = 35 + cyan = 36 + grey = 37 + white = 38 +) + +var ( + colorize func(message string, color Color, bold int) string + OsSupportsColors = runtime.GOOS != "windows" + TerminalSupportsColors = isTerminal() + UserAskedForColors = "" +) + +func init() { + InitColorSupport() +} + +func InitColorSupport() { + if colorsEnabled() { + colorize = func(message string, color Color, bold int) string { + return fmt.Sprintf("\033[%d;%dm%s\033[0m", bold, color, message) + } + } else { + colorize = func(message string, _ Color, _ int) string { + return message + } + } +} + +func colorsEnabled() bool { + return userDidNotDisableColor() && + (userEnabledColors() || (TerminalSupportsColors && OsSupportsColors)) +} + +func userEnabledColors() bool { + return UserAskedForColors == "true" || os.Getenv("CF_COLOR") == "true" +} + +func userDidNotDisableColor() bool { + return os.Getenv("CF_COLOR") != "false" && (UserAskedForColors != "false" || os.Getenv("CF_COLOR") == "true") +} + +func Colorize(message string, color Color) string { + return colorize(message, color, 0) +} + +func ColorizeBold(message string, color Color) string { + return colorize(message, color, 1) +} + +var decolorizerRegex = regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`) + +func Decolorize(message string) string { + return string(decolorizerRegex.ReplaceAll([]byte(message), []byte(""))) +} + +func HeaderColor(message string) string { + return ColorizeBold(message, white) +} + +func CommandColor(message string) string { + return ColorizeBold(message, yellow) +} + +func StoppedColor(message string) string { + return ColorizeBold(message, grey) +} + +func AdvisoryColor(message string) string { + return ColorizeBold(message, yellow) +} + +func CrashedColor(message string) string { + return ColorizeBold(message, red) +} + +func FailureColor(message string) string { + return ColorizeBold(message, red) +} + +func SuccessColor(message string) string { + return ColorizeBold(message, green) +} + +func EntityNameColor(message string) string { + return ColorizeBold(message, cyan) +} + +func PromptColor(message string) string { + return ColorizeBold(message, cyan) +} + +func TableContentHeaderColor(message string) string { + return ColorizeBold(message, cyan) +} + +func WarningColor(message string) string { + return ColorizeBold(message, magenta) +} + +func LogStdoutColor(message string) string { + return Colorize(message, white) +} + +func LogStderrColor(message string) string { + return Colorize(message, red) +} + +func LogAppHeaderColor(message string) string { + return ColorizeBold(message, yellow) +} + +func LogSysHeaderColor(message string) string { + return ColorizeBold(message, cyan) +} + +func isTerminal() bool { + return terminal.IsTerminal(1) +} diff --git a/cf/terminal/color_test.go b/cf/terminal/color_test.go new file mode 100644 index 00000000000..ea0332fb155 --- /dev/null +++ b/cf/terminal/color_test.go @@ -0,0 +1,153 @@ +package terminal_test + +import ( + . "github.com/cloudfoundry/cli/cf/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "os" + "runtime" +) + +var _ = Describe("Terminal colors", func() { + BeforeEach(func() { + UserAskedForColors = "" + }) + + JustBeforeEach(func() { + InitColorSupport() + }) + + Describe("CF_COLOR", func() { + Context("On OSes that don't support colors", func() { + BeforeEach(func() { OsSupportsColors = false }) + + Context("When the CF_COLOR env variable is specified", func() { + BeforeEach(func() { os.Setenv("CF_COLOR", "true") }) + itColorizes() + }) + + Context("When the CF_COLOR env variable is not specified", func() { + BeforeEach(func() { os.Setenv("CF_COLOR", "") }) + itDoesntColorize() + + Context("when the user DOES ask for colors", func() { + BeforeEach(func() { UserAskedForColors = "true" }) + itColorizes() + }) + }) + }) + + Context("On OSes that support colors", func() { + BeforeEach(func() { OsSupportsColors = true }) + + Context("When the CF_COLOR env variable is not specified", func() { + BeforeEach(func() { os.Setenv("CF_COLOR", "") }) + + Context("And the terminal supports colors", func() { + BeforeEach(func() { TerminalSupportsColors = true }) + itColorizes() + + Context("And user does not ask for color", func() { + BeforeEach(func() { UserAskedForColors = "false" }) + itDoesntColorize() + }) + + Context("And user does ask for color", func() { + BeforeEach(func() { UserAskedForColors = "true" }) + itColorizes() + }) + }) + + Context("And the terminal doesn't support colors", func() { + BeforeEach(func() { TerminalSupportsColors = false }) + itDoesntColorize() + + Context("And user asked for color", func() { + BeforeEach(func() { UserAskedForColors = "true" }) + itColorizes() + }) + }) + }) + + Context("When the CF_COLOR env variable is set to 'true'", func() { + BeforeEach(func() { os.Setenv("CF_COLOR", "true") }) + + Context("And the terminal supports colors", func() { + BeforeEach(func() { TerminalSupportsColors = true }) + itColorizes() + + Context("and the user asked for colors", func() { + BeforeEach(func() { UserAskedForColors = "true" }) + itColorizes() + }) + + Context("and the user did not ask for colors", func() { + BeforeEach(func() { UserAskedForColors = "false" }) + itColorizes() + }) + }) + + Context("Even if the terminal doesn't support colors", func() { + BeforeEach(func() { TerminalSupportsColors = false }) + itColorizes() + }) + }) + + Context("When the CF_COLOR env variable is set to 'false', even if the terminal supports colors", func() { + BeforeEach(func() { + os.Setenv("CF_COLOR", "false") + TerminalSupportsColors = true + }) + + itDoesntColorize() + + Context("and the user asked for colors", func() { + BeforeEach(func() { UserAskedForColors = "true" }) + itDoesntColorize() + }) + }) + }) + }) + + Describe("OsSupportsColors", func() { + It("Returns false on windows, and true otherwise", func() { + if runtime.GOOS == "windows" { + Expect(OsSupportsColors).To(BeFalse()) + } else { + Expect(OsSupportsColors).To(BeTrue()) + } + }) + }) + + var ( + originalOsSupportsColors bool + originalTerminalSupportsColors bool + ) + + BeforeEach(func() { + originalOsSupportsColors = OsSupportsColors + originalTerminalSupportsColors = TerminalSupportsColors + }) + + AfterEach(func() { + OsSupportsColors = originalOsSupportsColors + TerminalSupportsColors = originalTerminalSupportsColors + os.Setenv("CF_COLOR", "false") + }) +}) + +func itColorizes() { + It("colorizes", func() { + text := "Hello World" + colorizedText := ColorizeBold(text, 31) + Expect(colorizedText).To(Equal("\033[1;31mHello World\033[0m")) + }) +} + +func itDoesntColorize() { + It("doesn't colorize", func() { + text := "Hello World" + colorizedText := ColorizeBold(text, 31) + Expect(colorizedText).To(Equal("Hello World")) + }) +} diff --git a/cf/terminal/debug_printer.go b/cf/terminal/debug_printer.go new file mode 100644 index 00000000000..371892b5099 --- /dev/null +++ b/cf/terminal/debug_printer.go @@ -0,0 +1,13 @@ +package terminal + +import ( + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/trace" + "time" +) + +type DebugPrinter struct{} + +func (DebugPrinter) Print(title, dump string) { + trace.Logger.Printf("\n%s [%s]\n%s\n", HeaderColor(T(title)), time.Now().Format(time.RFC3339), trace.Sanitize(dump)) +} diff --git a/cf/terminal/fakes/fake_output_capture.go b/cf/terminal/fakes/fake_output_capture.go new file mode 100644 index 00000000000..4af5ae9be65 --- /dev/null +++ b/cf/terminal/fakes/fake_output_capture.go @@ -0,0 +1,41 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/terminal" + "sync" +) + +type FakeOutputCapture struct { + GetOutputAndResetStub func() []string + getOutputAndResetMutex sync.RWMutex + getOutputAndResetArgsForCall []struct{} + getOutputAndResetReturns struct { + result1 []string + } +} + +func (fake *FakeOutputCapture) GetOutputAndReset() []string { + fake.getOutputAndResetMutex.Lock() + defer fake.getOutputAndResetMutex.Unlock() + fake.getOutputAndResetArgsForCall = append(fake.getOutputAndResetArgsForCall, struct{}{}) + if fake.GetOutputAndResetStub != nil { + return fake.GetOutputAndResetStub() + } else { + return fake.getOutputAndResetReturns.result1 + } +} + +func (fake *FakeOutputCapture) GetOutputAndResetCallCount() int { + fake.getOutputAndResetMutex.RLock() + defer fake.getOutputAndResetMutex.RUnlock() + return len(fake.getOutputAndResetArgsForCall) +} + +func (fake *FakeOutputCapture) GetOutputAndResetReturns(result1 []string) { + fake.getOutputAndResetReturns = struct { + result1 []string + }{result1} +} + +var _ OutputCapture = new(FakeOutputCapture) diff --git a/cf/terminal/fakes/fake_printer.go b/cf/terminal/fakes/fake_printer.go new file mode 100644 index 00000000000..e09a769d88f --- /dev/null +++ b/cf/terminal/fakes/fake_printer.go @@ -0,0 +1,263 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + . "github.com/cloudfoundry/cli/cf/terminal" +) + +type FakePrinter struct { + PrintStub func(a ...interface{}) (n int, err error) + printMutex sync.RWMutex + printArgsForCall []struct { + a []interface{} + } + printReturns struct { + result1 int + result2 error + } + PrintfStub func(format string, a ...interface{}) (n int, err error) + printfMutex sync.RWMutex + printfArgsForCall []struct { + format string + a []interface{} + } + printfReturns struct { + result1 int + result2 error + } + PrintlnStub func(a ...interface{}) (n int, err error) + printlnMutex sync.RWMutex + printlnArgsForCall []struct { + a []interface{} + } + printlnReturns struct { + result1 int + result2 error + } + ForcePrintStub func(a ...interface{}) (n int, err error) + forcePrintMutex sync.RWMutex + forcePrintArgsForCall []struct { + a []interface{} + } + forcePrintReturns struct { + result1 int + result2 error + } + ForcePrintfStub func(format string, a ...interface{}) (n int, err error) + forcePrintfMutex sync.RWMutex + forcePrintfArgsForCall []struct { + format string + a []interface{} + } + forcePrintfReturns struct { + result1 int + result2 error + } + ForcePrintlnStub func(a ...interface{}) (n int, err error) + forcePrintlnMutex sync.RWMutex + forcePrintlnArgsForCall []struct { + a []interface{} + } + forcePrintlnReturns struct { + result1 int + result2 error + } +} + +func (fake *FakePrinter) Print(a ...interface{}) (n int, err error) { + fake.printMutex.Lock() + defer fake.printMutex.Unlock() + fake.printArgsForCall = append(fake.printArgsForCall, struct { + a []interface{} + }{a}) + if fake.PrintStub != nil { + return fake.PrintStub(a) + } else { + return fake.printReturns.result1, fake.printReturns.result2 + } +} + +func (fake *FakePrinter) PrintCallCount() int { + fake.printMutex.RLock() + defer fake.printMutex.RUnlock() + return len(fake.printArgsForCall) +} + +func (fake *FakePrinter) PrintArgsForCall(i int) []interface{} { + fake.printMutex.RLock() + defer fake.printMutex.RUnlock() + return fake.printArgsForCall[i].a +} + +func (fake *FakePrinter) PrintReturns(result1 int, result2 error) { + fake.printReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *FakePrinter) Printf(format string, a ...interface{}) (n int, err error) { + fake.printfMutex.Lock() + defer fake.printfMutex.Unlock() + fake.printfArgsForCall = append(fake.printfArgsForCall, struct { + format string + a []interface{} + }{format, a}) + if fake.PrintfStub != nil { + return fake.PrintfStub(format, a...) + } else { + return fake.printfReturns.result1, fake.printfReturns.result2 + } +} + +func (fake *FakePrinter) PrintfCallCount() int { + fake.printfMutex.RLock() + defer fake.printfMutex.RUnlock() + return len(fake.printfArgsForCall) +} + +func (fake *FakePrinter) PrintfArgsForCall(i int) (string, []interface{}) { + fake.printfMutex.RLock() + defer fake.printfMutex.RUnlock() + return fake.printfArgsForCall[i].format, fake.printfArgsForCall[i].a +} + +func (fake *FakePrinter) PrintfReturns(result1 int, result2 error) { + fake.printfReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *FakePrinter) Println(a ...interface{}) (n int, err error) { + fake.printlnMutex.Lock() + defer fake.printlnMutex.Unlock() + fake.printlnArgsForCall = append(fake.printlnArgsForCall, struct { + a []interface{} + }{a}) + if fake.PrintlnStub != nil { + return fake.PrintlnStub(a) + } else { + return fake.printlnReturns.result1, fake.printlnReturns.result2 + } +} + +func (fake *FakePrinter) PrintlnCallCount() int { + fake.printlnMutex.RLock() + defer fake.printlnMutex.RUnlock() + return len(fake.printlnArgsForCall) +} + +func (fake *FakePrinter) PrintlnArgsForCall(i int) []interface{} { + fake.printlnMutex.RLock() + defer fake.printlnMutex.RUnlock() + return fake.printlnArgsForCall[i].a +} + +func (fake *FakePrinter) PrintlnReturns(result1 int, result2 error) { + fake.printlnReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *FakePrinter) ForcePrint(a ...interface{}) (n int, err error) { + fake.forcePrintMutex.Lock() + defer fake.forcePrintMutex.Unlock() + fake.forcePrintArgsForCall = append(fake.forcePrintArgsForCall, struct { + a []interface{} + }{a}) + if fake.ForcePrintStub != nil { + return fake.ForcePrintStub(a) + } else { + return fake.forcePrintReturns.result1, fake.forcePrintReturns.result2 + } +} + +func (fake *FakePrinter) ForcePrintCallCount() int { + fake.forcePrintMutex.RLock() + defer fake.forcePrintMutex.RUnlock() + return len(fake.forcePrintArgsForCall) +} + +func (fake *FakePrinter) ForcePrintArgsForCall(i int) []interface{} { + fake.forcePrintMutex.RLock() + defer fake.forcePrintMutex.RUnlock() + return fake.forcePrintArgsForCall[i].a +} + +func (fake *FakePrinter) ForcePrintReturns(result1 int, result2 error) { + fake.forcePrintReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *FakePrinter) ForcePrintf(format string, a ...interface{}) (n int, err error) { + fake.forcePrintfMutex.Lock() + defer fake.forcePrintfMutex.Unlock() + fake.forcePrintfArgsForCall = append(fake.forcePrintfArgsForCall, struct { + format string + a []interface{} + }{format, a}) + if fake.ForcePrintfStub != nil { + return fake.ForcePrintfStub(format, a...) + } else { + return fake.forcePrintfReturns.result1, fake.forcePrintfReturns.result2 + } +} + +func (fake *FakePrinter) ForcePrintfCallCount() int { + fake.forcePrintfMutex.RLock() + defer fake.forcePrintfMutex.RUnlock() + return len(fake.forcePrintfArgsForCall) +} + +func (fake *FakePrinter) ForcePrintfArgsForCall(i int) (string, []interface{}) { + fake.forcePrintfMutex.RLock() + defer fake.forcePrintfMutex.RUnlock() + return fake.forcePrintfArgsForCall[i].format, fake.forcePrintfArgsForCall[i].a +} + +func (fake *FakePrinter) ForcePrintfReturns(result1 int, result2 error) { + fake.forcePrintfReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *FakePrinter) ForcePrintln(a ...interface{}) (n int, err error) { + fake.forcePrintlnMutex.Lock() + defer fake.forcePrintlnMutex.Unlock() + fake.forcePrintlnArgsForCall = append(fake.forcePrintlnArgsForCall, struct { + a []interface{} + }{a}) + if fake.ForcePrintlnStub != nil { + return fake.ForcePrintlnStub(a) + } else { + return fake.forcePrintlnReturns.result1, fake.forcePrintlnReturns.result2 + } +} + +func (fake *FakePrinter) ForcePrintlnCallCount() int { + fake.forcePrintlnMutex.RLock() + defer fake.forcePrintlnMutex.RUnlock() + return len(fake.forcePrintlnArgsForCall) +} + +func (fake *FakePrinter) ForcePrintlnArgsForCall(i int) []interface{} { + fake.forcePrintlnMutex.RLock() + defer fake.forcePrintlnMutex.RUnlock() + return fake.forcePrintlnArgsForCall[i].a +} + +func (fake *FakePrinter) ForcePrintlnReturns(result1 int, result2 error) { + fake.forcePrintlnReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +var _ Printer = new(FakePrinter) diff --git a/cf/terminal/fakes/fake_terminal_output_switch.go b/cf/terminal/fakes/fake_terminal_output_switch.go new file mode 100644 index 00000000000..3463379f0fb --- /dev/null +++ b/cf/terminal/fakes/fake_terminal_output_switch.go @@ -0,0 +1,40 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/terminal" + "sync" +) + +type FakeTerminalOutputSwitch struct { + DisableTerminalOutputStub func(bool) + disableTerminalOutputMutex sync.RWMutex + disableTerminalOutputArgsForCall []struct { + arg1 bool + } +} + +func (fake *FakeTerminalOutputSwitch) DisableTerminalOutput(arg1 bool) { + fake.disableTerminalOutputMutex.Lock() + defer fake.disableTerminalOutputMutex.Unlock() + fake.disableTerminalOutputArgsForCall = append(fake.disableTerminalOutputArgsForCall, struct { + arg1 bool + }{arg1}) + if fake.DisableTerminalOutputStub != nil { + fake.DisableTerminalOutputStub(arg1) + } +} + +func (fake *FakeTerminalOutputSwitch) DisableTerminalOutputCallCount() int { + fake.disableTerminalOutputMutex.RLock() + defer fake.disableTerminalOutputMutex.RUnlock() + return len(fake.disableTerminalOutputArgsForCall) +} + +func (fake *FakeTerminalOutputSwitch) DisableTerminalOutputArgsForCall(i int) bool { + fake.disableTerminalOutputMutex.RLock() + defer fake.disableTerminalOutputMutex.RUnlock() + return fake.disableTerminalOutputArgsForCall[i].arg1 +} + +var _ TerminalOutputSwitch = new(FakeTerminalOutputSwitch) diff --git a/cf/terminal/table.go b/cf/terminal/table.go new file mode 100644 index 00000000000..42d8c5a85b2 --- /dev/null +++ b/cf/terminal/table.go @@ -0,0 +1,86 @@ +package terminal + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +type Table interface { + Add(row ...string) + Print() +} + +type PrintableTable struct { + ui UI + headers []string + headerPrinted bool + maxSizes []int + rows [][]string +} + +func NewTable(ui UI, headers []string) Table { + return &PrintableTable{ + ui: ui, + headers: headers, + maxSizes: make([]int, len(headers)), + } +} + +func (t *PrintableTable) Add(row ...string) { + t.rows = append(t.rows, row) +} + +func (t *PrintableTable) Print() { + for _, row := range append(t.rows, t.headers) { + t.calculateMaxSize(row) + } + + if t.headerPrinted == false { + t.printHeader() + t.headerPrinted = true + } + + for _, line := range t.rows { + t.printRow(line) + } + + t.rows = [][]string{} +} + +func (t *PrintableTable) calculateMaxSize(row []string) { + for index, value := range row { + cellLength := utf8.RuneCountInString(Decolorize(value)) + if t.maxSizes[index] < cellLength { + t.maxSizes[index] = cellLength + } + } +} + +func (t *PrintableTable) printHeader() { + output := "" + for col, value := range t.headers { + output = output + t.cellValue(col, HeaderColor(value)) + } + t.ui.Say(output) +} + +func (t *PrintableTable) printRow(row []string) { + output := "" + for columnIndex, value := range row { + if columnIndex == 0 { + value = TableContentHeaderColor(value) + } + + output = output + t.cellValue(columnIndex, value) + } + t.ui.Say("%s", output) +} + +func (t *PrintableTable) cellValue(col int, value string) string { + padding := "" + if col < len(t.headers)-1 { + padding = strings.Repeat(" ", t.maxSizes[col]-utf8.RuneCountInString(Decolorize(value))) + } + return fmt.Sprintf("%s%s ", value, padding) +} diff --git a/cf/terminal/table_test.go b/cf/terminal/table_test.go new file mode 100644 index 00000000000..10561d9ccb8 --- /dev/null +++ b/cf/terminal/table_test.go @@ -0,0 +1,115 @@ +package terminal_test + +import ( + . "github.com/cloudfoundry/cli/cf/terminal" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + testterm "github.com/cloudfoundry/cli/testhelpers/terminal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Table", func() { + var ( + ui *testterm.FakeUI + table Table + ) + + BeforeEach(func() { + ui = &testterm.FakeUI{} + table = NewTable(ui, []string{"watashi", "no", "atama!"}) + }) + + It("prints the header", func() { + table.Print() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"watashi", "no", "atama!"}, + )) + }) + + It("prints format string literals as strings", func() { + table.Add("cloak %s", "and", "dagger") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"cloak %s", "and", "dagger"}, + )) + }) + + It("prints all the rows you give it", func() { + table.Add("something", "and", "nothing") + table.Print() + Expect(ui.Outputs).To(ContainSubstrings( + []string{"something", "and", "nothing"}, + )) + }) + + Describe("adding rows to be printed later", func() { + It("prints them when you call Print()", func() { + table.Add("a", "b", "c") + table.Add("passed", "to", "print") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"a", "b", "c"}, + )) + }) + + It("flushes previously added rows and then outputs passed rows", func() { + table.Add("a", "b", "c") + table.Add("passed", "to", "print") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"watashi", "no", "atama!"}, + []string{"a", "b", "c"}, + []string{"passed", "to", "print"}, + )) + }) + + It("flushes the buffer of rows when you call print", func() { + table.Add("a", "b", "c") + table.Add("passed", "to", "print") + table.Print() + ui.ClearOutputs() + + table.Print() + Expect(ui.Outputs).To(BeEmpty()) + }) + }) + + Describe("aligning columns", func() { + It("aligns rows to the header when the header is longest", func() { + table.Add("a", "b", "c") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"watashi no atama!"}, + []string{"a b c"}, + )) + }) + + It("aligns rows to the longest row provided", func() { + table.Add("x", "y", "z") + table.Add("something", "something", "darkside") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"watashi no atama!"}, + []string{"x y z"}, + []string{"something something darkside"}, + )) + }) + + It("aligns rows to the longest row provided when there are multibyte characters present", func() { + table.Add("x", "ÿ", "z") + table.Add("something", "something", "darkside") + table.Print() + + Expect(ui.Outputs).To(ContainSubstrings( + []string{"watashi no atama!"}, + []string{"x ÿ z"}, + []string{"something something darkside"}, + )) + }) + }) +}) diff --git a/cf/terminal/tee_printer.go b/cf/terminal/tee_printer.go new file mode 100644 index 00000000000..28202511a92 --- /dev/null +++ b/cf/terminal/tee_printer.go @@ -0,0 +1,88 @@ +package terminal + +import ( + "fmt" +) + +type Printer interface { + Print(a ...interface{}) (n int, err error) + Printf(format string, a ...interface{}) (n int, err error) + Println(a ...interface{}) (n int, err error) + ForcePrint(a ...interface{}) (n int, err error) + ForcePrintf(format string, a ...interface{}) (n int, err error) + ForcePrintln(a ...interface{}) (n int, err error) +} + +type OutputCapture interface { + GetOutputAndReset() []string +} + +type TerminalOutputSwitch interface { + DisableTerminalOutput(bool) +} + +type TeePrinter struct { + disableTerminalOutput bool + output []string +} + +func NewTeePrinter() *TeePrinter { + return &TeePrinter{ + output: []string{}, + } +} + +func (t *TeePrinter) GetOutputAndReset() []string { + currentOutput := t.output + t.output = []string{} + return currentOutput +} + +func (t *TeePrinter) Print(values ...interface{}) (n int, err error) { + str := fmt.Sprint(values...) + t.output = append(t.output, Decolorize(str)) + if !t.disableTerminalOutput { + return fmt.Print(str) + } + return +} + +func (t *TeePrinter) Printf(format string, a ...interface{}) (n int, err error) { + str := fmt.Sprintf(format, a...) + t.output = append(t.output, Decolorize(str)) + if !t.disableTerminalOutput { + return fmt.Print(str) + } + return +} + +func (t *TeePrinter) Println(values ...interface{}) (n int, err error) { + str := fmt.Sprint(values...) + t.output = append(t.output, Decolorize(str)) + if !t.disableTerminalOutput { + return fmt.Println(str) + } + return +} + +func (t *TeePrinter) ForcePrint(values ...interface{}) (n int, err error) { + str := fmt.Sprint(values...) + t.output = append(t.output, Decolorize(str)) + return fmt.Print(str) +} + +func (t *TeePrinter) ForcePrintf(format string, a ...interface{}) (n int, err error) { + str := fmt.Sprintf(format, a...) + t.output = append(t.output, Decolorize(str)) + return fmt.Print(str) +} + +func (t *TeePrinter) ForcePrintln(values ...interface{}) (n int, err error) { + str := fmt.Sprint(values...) + t.output = append(t.output, Decolorize(str)) + return fmt.Println(str) +} + +func (t *TeePrinter) DisableTerminalOutput(disable bool) { + t.disableTerminalOutput = disable +} diff --git a/cf/terminal/tee_printer_test.go b/cf/terminal/tee_printer_test.go new file mode 100644 index 00000000000..72cd5ae077a --- /dev/null +++ b/cf/terminal/tee_printer_test.go @@ -0,0 +1,216 @@ +package terminal_test + +import ( + . "github.com/cloudfoundry/cli/cf/terminal" + + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("TeePrinter", func() { + var ( + output []string + printer *TeePrinter + ) + + Describe(".Print", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Print("Hello ") + printer.Print("Mom!") + }) + }) + + It("should delegate to fmt.Print", func() { + Expect(output[0]).To(Equal("Hello Mom!")) + }) + + It("should save the output to the slice", func() { + outputs := printer.GetOutputAndReset() + Expect(outputs[0]).To(Equal("Hello ")) + Expect(outputs[1]).To(Equal("Mom!")) + }) + + It("should decolorize text", func() { + io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Print("hi " + EntityNameColor("foo")) + }) + + output = printer.GetOutputAndReset() + Expect(output[0]).To(Equal("hi foo")) + }) + }) + + Describe(".Printf", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Printf("Hello %s", "everybody") + }) + }) + + It("should delegate to fmt.Printf", func() { + Expect(output[0]).To(Equal("Hello everybody")) + }) + + It("should save the output to the slice", func() { + Expect(printer.GetOutputAndReset()[0]).To(Equal("Hello everybody")) + }) + + It("should decolorize text", func() { + io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Printf("hi %s", EntityNameColor("foo")) + }) + + output = printer.GetOutputAndReset() + Expect(output[0]).To(Equal("hi foo")) + }) + }) + + Describe(".Println", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Println("Hello ", "everybody") + }) + }) + + It("should delegate to fmt.Printf", func() { + Expect(output[0]).To(Equal("Hello everybody")) + }) + + It("should save the output to the slice", func() { + Expect(printer.GetOutputAndReset()[0]).To(Equal("Hello everybody")) + }) + + It("should decolorize text", func() { + io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Println("hi " + EntityNameColor("foo")) + }) + + output = printer.GetOutputAndReset() + Expect(output[0]).To(Equal("hi foo")) + }) + }) + + Describe(".ForcePrintf", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.ForcePrintf("Hello %s", "everybody") + }) + }) + + It("should delegate to fmt.Printf", func() { + Expect(output[0]).To(Equal("Hello everybody")) + }) + + It("should save the output to the slice", func() { + Expect(printer.GetOutputAndReset()[0]).To(Equal("Hello everybody")) + }) + + It("should decolorize text", func() { + io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Printf("hi %s", EntityNameColor("foo")) + }) + + output = printer.GetOutputAndReset() + Expect(output[0]).To(Equal("hi foo")) + }) + }) + + Describe(".ForcePrintln", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.ForcePrintln("Hello ", "everybody") + }) + }) + + It("should delegate to fmt.Printf", func() { + Expect(output[0]).To(Equal("Hello everybody")) + }) + + It("should save the output to the slice", func() { + Expect(printer.GetOutputAndReset()[0]).To(Equal("Hello everybody")) + }) + + It("should decolorize text", func() { + io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Println("hi " + EntityNameColor("foo")) + }) + + output = printer.GetOutputAndReset() + Expect(output[0]).To(Equal("hi foo")) + }) + }) + + Describe(".GetOutputAndReset", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.Print("Hello") + printer.Print("Mom!") + }) + }) + + It("should clear the slice after access", func() { + printer.GetOutputAndReset() + Expect(printer.GetOutputAndReset()).To(BeEmpty()) + }) + }) + + Describe("Pausing Output", func() { + BeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + printer = NewTeePrinter() + printer.DisableTerminalOutput(true) + printer.Print("Hello") + printer.Println("Mom!") + printer.Printf("Dad!") + printer.ForcePrint("Forced Hello") + printer.ForcePrintln("Forced Mom") + printer.ForcePrintf("Forced Dad") + }) + }) + + It("should print only forced terminal output", func() { + Expect(output).To(Equal([]string{"Forced HelloForced Mom", "Forced Dad"})) + }) + + It("should still capture all output", func() { + Expect(printer.GetOutputAndReset()).To(Equal([]string{"Hello", "Mom!", "Dad!", "Forced Hello", "Forced Mom", "Forced Dad"})) + }) + + Describe(".ResumeOutput", func() { + BeforeEach(func() { + printer.GetOutputAndReset() + output = io_helpers.CaptureOutput(func() { + printer.DisableTerminalOutput(false) + printer.Print("Hello") + printer.Println("Mom!") + printer.Printf("Dad!") + printer.Println("Grandpa!") + printer.ForcePrint("ForcePrint") + printer.ForcePrintln("ForcePrintln") + printer.ForcePrintf("ForcePrintf") + }) + }) + + It("should print all output", func() { + Expect(output).To(Equal([]string{"HelloMom!", "Dad!Grandpa!", "ForcePrintForcePrintln", "ForcePrintf"})) + }) + + It("should capture all output", func() { + Expect(printer.GetOutputAndReset()).To(Equal([]string{"Hello", "Mom!", "Dad!", "Grandpa!", "ForcePrint", "ForcePrintln", "ForcePrintf"})) + }) + }) + }) +}) diff --git a/cf/terminal/terminal_suite_test.go b/cf/terminal/terminal_suite_test.go new file mode 100644 index 00000000000..b4039a1f640 --- /dev/null +++ b/cf/terminal/terminal_suite_test.go @@ -0,0 +1,19 @@ +package terminal_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTerminal(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Terminal Suite") +} diff --git a/cf/terminal/ui.go b/cf/terminal/ui.go new file mode 100644 index 00000000000..f1d8375140e --- /dev/null +++ b/cf/terminal/ui.go @@ -0,0 +1,252 @@ +package terminal + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + "time" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/trace" + "github.com/codegangsta/cli" +) + +type ColoringFunction func(value string, row int, col int) string + +func NotLoggedInText() string { + return fmt.Sprintf(T("Not logged in. Use '{{.CFLoginCommand}}' to log in.", map[string]interface{}{"CFLoginCommand": CommandColor(cf.Name() + " " + "login")})) +} + +type UI interface { + PrintPaginator(rows []string, err error) + Say(message string, args ...interface{}) + PrintCapturingNoOutput(message string, args ...interface{}) + Warn(message string, args ...interface{}) + Ask(prompt string, args ...interface{}) (answer string) + AskForPassword(prompt string, args ...interface{}) (answer string) + Confirm(message string, args ...interface{}) bool + ConfirmDelete(modelType, modelName string) bool + ConfirmDeleteWithAssociations(modelType, modelName string) bool + Ok() + Failed(message string, args ...interface{}) + FailWithUsage(context *cli.Context) + PanicQuietly() + ShowConfiguration(core_config.Reader) + LoadingIndication() + Wait(duration time.Duration) + Table(headers []string) Table +} + +type terminalUI struct { + stdin io.Reader + printer Printer +} + +func NewUI(r io.Reader, printer Printer) UI { + return &terminalUI{ + stdin: r, + printer: printer, + } +} + +func (c *terminalUI) PrintPaginator(rows []string, err error) { + if err != nil { + c.Failed(err.Error()) + return + } + + for _, row := range rows { + c.Say(row) + } +} + +func (c *terminalUI) PrintCapturingNoOutput(message string, args ...interface{}) { + if len(args) == 0 { + fmt.Printf("%s", message) + } else { + fmt.Printf(message, args...) + } +} + +func (c *terminalUI) Say(message string, args ...interface{}) { + if len(args) == 0 { + c.printer.Printf("%s\n", message) + } else { + c.printer.Printf(message+"\n", args...) + } +} + +func (c *terminalUI) Warn(message string, args ...interface{}) { + message = fmt.Sprintf(message, args...) + c.Say(WarningColor(message)) + return +} + +func (c *terminalUI) ConfirmDeleteWithAssociations(modelType, modelName string) bool { + return c.confirmDelete(T("Really delete the {{.ModelType}} {{.ModelName}} and everything associated with it?", + map[string]interface{}{ + "ModelType": modelType, + "ModelName": EntityNameColor(modelName), + })) +} + +func (c *terminalUI) ConfirmDelete(modelType, modelName string) bool { + return c.confirmDelete(T("Really delete the {{.ModelType}} {{.ModelName}}?", + map[string]interface{}{ + "ModelType": modelType, + "ModelName": EntityNameColor(modelName), + })) +} + +func (c *terminalUI) confirmDelete(message string) bool { + result := c.Confirm(message) + + if !result { + c.Warn(T("Delete cancelled")) + } + + return result +} + +func (c *terminalUI) Confirm(message string, args ...interface{}) bool { + response := c.Ask(message, args...) + switch strings.ToLower(response) { + case "y", T("yes"): + return true + } + return false +} + +func (c *terminalUI) Ask(prompt string, args ...interface{}) (answer string) { + fmt.Println("") + fmt.Printf(prompt+PromptColor(">")+" ", args...) + + rd := bufio.NewReader(c.stdin) + line, err := rd.ReadString('\n') + if err == nil { + return strings.TrimSpace(line) + } + return "" +} + +func (c *terminalUI) Ok() { + c.Say(SuccessColor(T("OK"))) +} + +const QuietPanic = "This shouldn't print anything" + +func (c *terminalUI) Failed(message string, args ...interface{}) { + message = fmt.Sprintf(message, args...) + + if T == nil { + c.Say(FailureColor("FAILED")) + c.Say(message) + + trace.Logger.Print("FAILED") + trace.Logger.Print(message) + c.PanicQuietly() + } else { + c.Say(FailureColor(T("FAILED"))) + c.Say(message) + + trace.Logger.Print(T("FAILED")) + trace.Logger.Print(message) + c.PanicQuietly() + } +} + +func (c *terminalUI) PanicQuietly() { + panic(QuietPanic) +} + +func (c *terminalUI) FailWithUsage(context *cli.Context) { + c.Say(FailureColor(T("FAILED"))) + c.Say(T("Incorrect Usage.\n")) + cli.ShowCommandHelp(context, context.Command.Name) + c.Say("") + os.Exit(1) +} + +func (ui *terminalUI) ShowConfiguration(config core_config.Reader) { + table := NewTable(ui, []string{"", ""}) + + if config.HasAPIEndpoint() { + table.Add( + T("API endpoint:"), + T("{{.ApiEndpoint}} (API version: {{.ApiVersionString}})", + map[string]interface{}{ + "ApiEndpoint": EntityNameColor(config.ApiEndpoint()), + "ApiVersionString": EntityNameColor(config.ApiVersion()), + }), + ) + } + + if !config.IsLoggedIn() { + table.Print() + ui.Say(NotLoggedInText()) + return + } else { + table.Add( + T("User:"), + EntityNameColor(config.UserEmail()), + ) + } + + if !config.HasOrganization() && !config.HasSpace() { + table.Print() + command := fmt.Sprintf("%s target -o ORG -s SPACE", cf.Name()) + ui.Say(T("No org or space targeted, use '{{.CFTargetCommand}}'", + map[string]interface{}{ + "CFTargetCommand": CommandColor(command), + })) + return + } + + if config.HasOrganization() { + table.Add( + T("Org:"), + EntityNameColor(config.OrganizationFields().Name), + ) + } else { + command := fmt.Sprintf("%s target -o Org", cf.Name()) + table.Add( + T("Org:"), + T("No org targeted, use '{{.CFTargetCommand}}'", + map[string]interface{}{ + "CFTargetCommand": CommandColor(command), + }), + ) + } + + if config.HasSpace() { + table.Add( + T("Space:"), + EntityNameColor(config.SpaceFields().Name), + ) + } else { + command := fmt.Sprintf("%s target -s SPACE", cf.Name()) + table.Add( + T("Space:"), + T("No space targeted, use '{{.CFTargetCommand}}'", map[string]interface{}{"CFTargetCommand": CommandColor(command)}), + ) + } + + table.Print() +} + +func (c *terminalUI) LoadingIndication() { + c.printer.Print(".") +} + +func (c *terminalUI) Wait(duration time.Duration) { + time.Sleep(duration) +} + +func (ui *terminalUI) Table(headers []string) Table { + return NewTable(ui, headers) +} diff --git a/cf/terminal/ui_test.go b/cf/terminal/ui_test.go new file mode 100644 index 00000000000..9fa7a3633f0 --- /dev/null +++ b/cf/terminal/ui_test.go @@ -0,0 +1,329 @@ +package terminal_test + +import ( + "io" + "os" + "strings" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/models" + testassert "github.com/cloudfoundry/cli/testhelpers/assert" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + + . "github.com/cloudfoundry/cli/cf/terminal" + . "github.com/cloudfoundry/cli/testhelpers/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("UI", func() { + + Describe("Printing message to stdout with PrintCapturingNoOutput", func() { + It("prints strings without using the TeePrinter", func() { + printer := NewTeePrinter() + io_helpers.SimulateStdin("", func(reader io.Reader) { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, printer) + ui.PrintCapturingNoOutput("Hello") + }) + + Expect("Hello").To(Equal(strings.Join(output, ""))) + Expect(len(printer.GetOutputAndReset())).To(Equal(0)) + }) + }) + }) + + Describe("Printing message to stdout with Say", func() { + It("prints strings", func() { + io_helpers.SimulateStdin("", func(reader io.Reader) { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + ui.Say("Hello") + }) + + Expect("Hello").To(Equal(strings.Join(output, ""))) + }) + }) + + It("prints formatted strings", func() { + io_helpers.SimulateStdin("", func(reader io.Reader) { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + ui.Say("Hello %s", "World!") + }) + + Expect("Hello World!").To(Equal(strings.Join(output, ""))) + }) + }) + + It("does not format strings when provided no args", func() { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.Say("Hello %s World!") // whoops + }) + + Expect(strings.Join(output, "")).To(Equal("Hello %s World!")) + }) + }) + + Describe("Asking user for input", func() { + It("allows string with whitespaces", func() { + io_helpers.CaptureOutput(func() { + io_helpers.SimulateStdin("foo bar\n", func(reader io.Reader) { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.Ask("?")).To(Equal("foo bar")) + }) + }) + }) + + It("returns empty string if an error occured while reading string", func() { + io_helpers.CaptureOutput(func() { + io_helpers.SimulateStdin("string without expected delimiter", func(reader io.Reader) { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.Ask("?")).To(Equal("")) + }) + }) + }) + + It("always outputs the prompt, even when output is disabled", func() { + output := io_helpers.CaptureOutput(func() { + io_helpers.SimulateStdin("things are great\n", func(reader io.Reader) { + printer := NewTeePrinter() + printer.DisableTerminalOutput(true) + ui := NewUI(reader, printer) + ui.Ask("You like things?") + }) + }) + Expect(strings.Join(output, "")).To(ContainSubstring("You like things?")) + }) + }) + + Describe("Confirming user input", func() { + It("treats 'y' as an affirmative confirmation", func() { + io_helpers.SimulateStdin("y\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.Confirm("Hello %s", "World?")).To(BeTrue()) + }) + + Expect(out).To(ContainSubstrings([]string{"Hello World?"})) + }) + }) + + It("treats 'yes' as an affirmative confirmation", func() { + io_helpers.SimulateStdin("yes\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.Confirm("Hello %s", "World?")).To(BeTrue()) + }) + + Expect(out).To(ContainSubstrings([]string{"Hello World?"})) + }) + }) + + It("treats other input as a negative confirmation", func() { + io_helpers.SimulateStdin("wat\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.Confirm("Hello %s", "World?")).To(BeFalse()) + }) + + Expect(out).To(ContainSubstrings([]string{"Hello World?"})) + }) + }) + }) + + Describe("Confirming deletion", func() { + It("formats a nice output string with exactly one prompt", func() { + io_helpers.SimulateStdin("y\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.ConfirmDelete("fizzbuzz", "bizzbump")).To(BeTrue()) + }) + + Expect(out).To(ContainSubstrings([]string{ + "Really delete the fizzbuzz", + "bizzbump", + "?> ", + })) + }) + }) + + It("treats 'yes' as an affirmative confirmation", func() { + io_helpers.SimulateStdin("yes\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.ConfirmDelete("modelType", "modelName")).To(BeTrue()) + }) + + Expect(out).To(ContainSubstrings([]string{"modelType modelName"})) + }) + }) + + It("treats other input as a negative confirmation and warns the user", func() { + io_helpers.SimulateStdin("wat\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.ConfirmDelete("modelType", "modelName")).To(BeFalse()) + }) + + Expect(out).To(ContainSubstrings([]string{"Delete cancelled"})) + }) + }) + }) + + Describe("Confirming deletion with associations", func() { + It("warns the user that associated objects will also be deleted", func() { + io_helpers.SimulateStdin("wat\n", func(reader io.Reader) { + out := io_helpers.CaptureOutput(func() { + ui := NewUI(reader, NewTeePrinter()) + Expect(ui.ConfirmDeleteWithAssociations("modelType", "modelName")).To(BeFalse()) + }) + + Expect(out).To(ContainSubstrings([]string{"Delete cancelled"})) + }) + }) + }) + + Context("when user is not logged in", func() { + var config core_config.Reader + + BeforeEach(func() { + config = testconfig.NewRepository() + }) + + It("prompts the user to login", func() { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.ShowConfiguration(config) + }) + + Expect(output).ToNot(ContainSubstrings([]string{"API endpoint:"})) + Expect(output).To(ContainSubstrings([]string{"Not logged in", "Use", "log in"})) + }) + }) + + Context("when an api endpoint is set and the user logged in", func() { + var config core_config.ReadWriter + + BeforeEach(func() { + accessToken := core_config.TokenInfo{ + UserGuid: "my-user-guid", + Username: "my-user", + Email: "my-user-email", + } + config = testconfig.NewRepositoryWithAccessToken(accessToken) + config.SetApiEndpoint("https://test.example.org") + config.SetApiVersion("☃☃☃") + }) + + Describe("tells the user what is set in the config", func() { + var output []string + + JustBeforeEach(func() { + output = io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.ShowConfiguration(config) + }) + }) + + It("tells the user which api endpoint is set", func() { + Expect(output).To(ContainSubstrings([]string{"API endpoint:", "https://test.example.org"})) + }) + + It("tells the user the api version", func() { + Expect(output).To(ContainSubstrings([]string{"API version:", "☃☃☃"})) + }) + + It("tells the user which user is logged in", func() { + Expect(output).To(ContainSubstrings([]string{"User:", "my-user-email"})) + }) + + Context("when an org is targeted", func() { + BeforeEach(func() { + config.SetOrganizationFields(models.OrganizationFields{ + Name: "org-name", + Guid: "org-guid", + }) + }) + + It("tells the user which org is targeted", func() { + Expect(output).To(ContainSubstrings([]string{"Org:", "org-name"})) + }) + }) + + Context("when a space is targeted", func() { + BeforeEach(func() { + config.SetSpaceFields(models.SpaceFields{ + Name: "my-space", + Guid: "space-guid", + }) + }) + + It("tells the user which space is targeted", func() { + Expect(output).To(ContainSubstrings([]string{"Space:", "my-space"})) + }) + }) + }) + + It("prompts the user to target an org and space when no org or space is targeted", func() { + output := io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.ShowConfiguration(config) + }) + + Expect(output).To(ContainSubstrings([]string{"No", "org", "space", "targeted", "-o ORG", "-s SPACE"})) + }) + + It("prompts the user to target an org when no org is targeted", func() { + sf := models.SpaceFields{} + sf.Guid = "guid" + sf.Name = "name" + + output := io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.ShowConfiguration(config) + }) + + Expect(output).To(ContainSubstrings([]string{"No", "org", "targeted", "-o ORG"})) + }) + + It("prompts the user to target a space when no space is targeted", func() { + of := models.OrganizationFields{} + of.Guid = "of-guid" + of.Name = "of-name" + + output := io_helpers.CaptureOutput(func() { + ui := NewUI(os.Stdin, NewTeePrinter()) + ui.ShowConfiguration(config) + }) + + Expect(output).To(ContainSubstrings([]string{"No", "space", "targeted", "-s SPACE"})) + }) + }) + + Describe("failing", func() { + It("panics with a specific string", func() { + io_helpers.CaptureOutput(func() { + testassert.AssertPanic(QuietPanic, func() { + NewUI(os.Stdin, NewTeePrinter()).Failed("uh oh") + }) + }) + }) + + It("does not use 'T' func to translate when it is not initialized", func() { + t := i18n.T + i18n.T = nil + + io_helpers.CaptureOutput(func() { + testassert.AssertPanic(QuietPanic, func() { + NewUI(os.Stdin, NewTeePrinter()).Failed("uh oh") + }) + }) + + i18n.T = t + }) + }) +}) diff --git a/src/cf/terminal/ui_unix.go b/cf/terminal/ui_unix.go similarity index 88% rename from src/cf/terminal/ui_unix.go rename to cf/terminal/ui_unix.go index aae58c103e8..aae69e9fb7f 100644 --- a/src/cf/terminal/ui_unix.go +++ b/cf/terminal/ui_unix.go @@ -7,6 +7,7 @@ package terminal import ( "bufio" "fmt" + . "github.com/cloudfoundry/cli/cf/i18n" "os" "os/signal" "strings" @@ -31,7 +32,7 @@ func (ui terminalUI) AskForPassword(prompt string, args ...interface{}) (passwd // Display the prompt. fmt.Println("") - fmt.Printf(prompt+" ", args...) + fmt.Printf(prompt+PromptColor(">")+" ", args...) // File descriptors for stdin, stdout, and stderr. fd := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} @@ -52,7 +53,7 @@ func (ui terminalUI) AskForPassword(prompt string, args ...interface{}) (passwd passwd = readPassword(pid) - // Carraige return after the user input. + // Carriage return after the user input. fmt.Println("") return @@ -73,7 +74,7 @@ func echoOff(fd []uintptr) (int, error) { pid, err := syscall.ForkExec(sttyArg0, sttyArgvEOff, &syscall.ProcAttr{Dir: exec_cwdir, Files: fd}) if err != nil { - return 0, fmt.Errorf("failed turning off console echo for password entry:\n\t%s", err) + return 0, fmt.Errorf(T("failed turning off console echo for password entry:\n{{.ErrorDescription}}", map[string]interface{}{"ErrorDescription": err})) } return pid, nil diff --git a/src/cf/terminal/ui_windows.go b/cf/terminal/ui_windows.go similarity index 100% rename from src/cf/terminal/ui_windows.go rename to cf/terminal/ui_windows.go diff --git a/cf/trace/trace.go b/cf/trace/trace.go new file mode 100644 index 00000000000..ac2a92d9d83 --- /dev/null +++ b/cf/trace/trace.go @@ -0,0 +1,98 @@ +package trace + +import ( + "fmt" + "io" + "log" + "os" + "regexp" + + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/gofileutils/fileutils" +) + +const CF_TRACE = "CF_TRACE" + +type Printer interface { + Print(v ...interface{}) + Printf(format string, v ...interface{}) + Println(v ...interface{}) +} + +type nullLogger struct{} + +func (*nullLogger) Print(v ...interface{}) {} +func (*nullLogger) Printf(format string, v ...interface{}) {} +func (*nullLogger) Println(v ...interface{}) {} + +var stdOut io.Writer = os.Stdout +var Logger Printer + +func init() { + Logger = NewLogger("") +} + +func EnableTrace() { + Logger = newStdoutLogger() +} + +func DisableTrace() { + Logger = new(nullLogger) +} + +func SetStdout(s io.Writer) { + stdOut = s +} + +func NewLogger(cf_trace string) Printer { + switch cf_trace { + case "", "false": + Logger = new(nullLogger) + case "true": + Logger = newStdoutLogger() + default: + Logger = newFileLogger(cf_trace) + } + + return Logger +} + +func newStdoutLogger() Printer { + return log.New(stdOut, "", 0) +} + +func newFileLogger(path string) Printer { + file, err := fileutils.Open(path) + if err != nil { + logger := newStdoutLogger() + logger.Printf(T("CF_TRACE ERROR CREATING LOG FILE {{.Path}}:\n{{.Err}}", + map[string]interface{}{"Path": path, "Err": err})) + return logger + } + + return log.New(file, "", 0) +} + +func Sanitize(input string) (sanitized string) { + var sanitizeJson = func(propertyName string, json string) string { + regex := regexp.MustCompile(fmt.Sprintf(`"%s":\s*"[^"]*"`, propertyName)) + return regex.ReplaceAllString(json, fmt.Sprintf(`"%s":"%s"`, propertyName, PRIVATE_DATA_PLACEHOLDER())) + } + + re := regexp.MustCompile(`(?m)^Authorization: .*`) + sanitized = re.ReplaceAllString(input, "Authorization: "+PRIVATE_DATA_PLACEHOLDER()) + re = regexp.MustCompile(`password=[^&]*&`) + sanitized = re.ReplaceAllString(sanitized, "password="+PRIVATE_DATA_PLACEHOLDER()+"&") + + sanitized = sanitizeJson("access_token", sanitized) + sanitized = sanitizeJson("refresh_token", sanitized) + sanitized = sanitizeJson("token", sanitized) + sanitized = sanitizeJson("password", sanitized) + sanitized = sanitizeJson("oldPassword", sanitized) + + return +} + +func PRIVATE_DATA_PLACEHOLDER() string { + return T("[PRIVATE DATA HIDDEN]") +} diff --git a/cf/trace/trace_suite_test.go b/cf/trace/trace_suite_test.go new file mode 100644 index 00000000000..b6fa5ef803b --- /dev/null +++ b/cf/trace/trace_suite_test.go @@ -0,0 +1,19 @@ +package trace_test + +import ( + "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/testhelpers/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTrace(t *testing.T) { + config := configuration.NewRepositoryWithDefaults() + i18n.T = i18n.Init(config, &detection.JibberJabberDetector{}) + + RegisterFailHandler(Fail) + RunSpecs(t, "Trace Suite") +} diff --git a/cf/trace/trace_test.go b/cf/trace/trace_test.go new file mode 100644 index 00000000000..f45579d4da4 --- /dev/null +++ b/cf/trace/trace_test.go @@ -0,0 +1,247 @@ +package trace_test + +import ( + "bytes" + "github.com/cloudfoundry/gofileutils/fileutils" + "io/ioutil" + "os" + "runtime" + + . "github.com/cloudfoundry/cli/cf/trace" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("trace logger", func() { + Describe("a new, better API", func() { + var ( + stdout *bytes.Buffer + ) + + BeforeEach(func() { + stdout = bytes.NewBuffer([]byte{}) + SetStdout(stdout) + }) + + It("assumes it should write to stdout", func() { + logger := NewLogger("true") + logger.Print("hello whirled") + + result, err := ioutil.ReadAll(stdout) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(ContainSubstring("hello whirled")) + }) + + It("prints to nothing when given false", func() { + logger := NewLogger("false") + logger.Print("hello whirled") + + result, err := ioutil.ReadAll(stdout) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + + It("prints to a file when given a string", func() { + fileutils.TempFile("trace_test", func(file *os.File, err error) { + Expect(err).NotTo(HaveOccurred()) + file.Write([]byte("pre-existing content")) + + logger := NewLogger(file.Name()) + logger.Print("hello world") + + file.Seek(0, os.SEEK_SET) + result, err := ioutil.ReadAll(file) + Expect(err).NotTo(HaveOccurred()) + + byteString := string(result) + Expect(byteString).To(ContainSubstring("pre-existing content")) + Expect(byteString).To(ContainSubstring("hello world")) + + result, _ = ioutil.ReadAll(stdout) + Expect(string(result)).To(BeEmpty()) + }) + }) + + Context("when CF_TRACE is set to a file path that cannot be opened", func() { + It("defaults to printing to its out pipe", func() { + if runtime.GOOS != "windows" { + stdOut := bytes.NewBuffer([]byte{}) + SetStdout(stdOut) + + logger := NewLogger("/dev/null/whoops") + logger.Print("hello world") + + result, _ := ioutil.ReadAll(stdOut) + Expect(string(result)).To(ContainSubstring("hello world")) + } + }) + }) + }) + + Describe("Sanitize", func() { + It("hides the authorization token header", func() { + request := ` +REQUEST: +GET /v2/organizations HTTP/1.1 +Host: api.run.pivotal.io +Accept: application/json +Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI3NDRkNWQ1My0xODkxLTQzZjktYjNiMy1mMTQxNDZkYzQ4ZmUiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJlbWFpbCI6Im1nZWhhcmQrY2xpQHBpdm90YWxsYWJzLmNvbSIsImlhdCI6MTM3ODI0NzgxNiwiZXhwIjoxMzc4MjkxMDE2LCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.LL_QLO0SztGRENmU-9KA2WouOyPkKVENGQoUtjqrGR-UIekXMClH6fmKELzHtB69z3n9x7_jYJbvv32D-dX1J7p1CMWIDLOzXUnIUDK7cU5Q2yuYszf4v5anKiJtrKWU0_Pg87cQTZ_lWXAhdsi-bhLVR_pITxehfz7DKChjC8gh-FiuDvH5qHxxPqYHUl9jPso5OQ0y0fqZpLt8Yq23DKWaFAZehLnrhFltdQ_jSLy1QAYYZVD_HpQDf9NozKXruIvXhyIuwGj99QmUs3LSyNWecy822VqOoBtPYS6CLegMuWWlO64TJNrnZuh5YsOuW8SudJONx2wwEqARysJIHw +This is the body. Please don't get rid of me even though I contain Authorization: and some other text + ` + + expected := ` +REQUEST: +GET /v2/organizations HTTP/1.1 +Host: api.run.pivotal.io +Accept: application/json +Authorization: [PRIVATE DATA HIDDEN] +This is the body. Please don't get rid of me even though I contain Authorization: and some other text + ` + + Expect(Sanitize(request)).To(Equal(expected)) + }) + + Describe("hiding passwords in the body of requests", func() { + It("hides passwords in query args", func() { + request := ` +POST /oauth/token HTTP/1.1 +Host: login.run.pivotal.io +Accept: application/json +Authorization: [PRIVATE DATA HIDDEN] +Content-Type: application/x-www-form-urlencoded + +grant_type=password&password=password&scope=&username=mgehard%2Bcli%40pivotallabs.com +` + + expected := ` +POST /oauth/token HTTP/1.1 +Host: login.run.pivotal.io +Accept: application/json +Authorization: [PRIVATE DATA HIDDEN] +Content-Type: application/x-www-form-urlencoded + +grant_type=password&password=[PRIVATE DATA HIDDEN]&scope=&username=mgehard%2Bcli%40pivotallabs.com +` + Expect(Sanitize(request)).To(Equal(expected)) + }) + + It("hides paswords in the JSON-formatted request body", func() { + request := ` +REQUEST: [2014-03-07T10:53:36-08:00] +PUT /Users/user-guid-goes-here/password HTTP/1.1 + +{"password":"stanleysPasswordIsCool","oldPassword":"stanleypassword!"} +` + + expected := ` +REQUEST: [2014-03-07T10:53:36-08:00] +PUT /Users/user-guid-goes-here/password HTTP/1.1 + +{"password":"[PRIVATE DATA HIDDEN]","oldPassword":"[PRIVATE DATA HIDDEN]"} +` + + Expect(Sanitize(request)).To(Equal(expected)) + }) + + It("hides create-user passwords", func() { + request := ` +REQUEST: [2014-03-07T12:15:08-08:00] +POST /Users HTTP/1.1 +{ + "userName": "jiro", + "emails": [{"value":"jiro"}], + "password": "leansushi", + "name": {"givenName":"jiro", "familyName":"jiro"} +} +` + expected := ` +REQUEST: [2014-03-07T12:15:08-08:00] +POST /Users HTTP/1.1 +{ + "userName": "jiro", + "emails": [{"value":"jiro"}], + "password":"[PRIVATE DATA HIDDEN]", + "name": {"givenName":"jiro", "familyName":"jiro"} +} +` + Expect(Sanitize(request)).To(Equal(expected)) + }) + }) + + It("hides oauth tokens in the body of requests", func() { + response := ` +HTTP/1.1 200 OK +Content-Length: 2132 +Cache-Control: no-cache +Cache-Control: no-store +Cache-Control: no-store +Connection: keep-alive +Content-Type: application/json;charset=UTF-8 +Date: Thu, 05 Sep 2013 16:31:43 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Pragma: no-cache +Pragma: no-cache +Server: Apache-Coyote/1.1 + +{"access_token":"eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNmE3YzEzNi02NDk3LTRmYWYtODc5OS00YzQyZTFmM2M2ZjUiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJlbWFpbCI6Im1nZWhhcmQrY2xpQHBpdm90YWxsYWJzLmNvbSIsImlhdCI6MTM3ODM5ODcwMywiZXhwIjoxMzc4NDQxOTAzLCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.VZErs4AnXgAzEirSY1A0yV0xQItXiPqaMfpO__MBwCihEpMEtMKemvlUPn3HEKyOGINk9YzhPV30ILrBb0oPt9plCD42BLEtyr_cbeo-1zap6QuhN8YjAAKQgjNYKORSvgi9x13JrXtCGByviHVEBP39Zeum2ZoehZfClWS7YP9lUfqaIBWUDLLBQtT6AZRlbzLwH-MJ5GkH1DOkIXzuWBk0OXp4VNm38kxzLQMnOJ3aJTcWv3YBxJeIgasoQLadTPaEPLxDGeC7V6SqhGJdyyZVnGTOKLt5ict-fxDoX6CxFnT_ZuMvseSocPfS2Or0HR_FICHAv2_C_6yv_4aI7w","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjMjM2M2E3Yi04M2MwLTRiN2ItYjg0Zi1mNTM3MTA4ZGExZmEiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiaWF0IjoxMzc4Mzk4NzAzLCJleHAiOjEzODA5OTA3MDMsImNpZCI6ImNmIiwiaXNzIjoiaHR0cHM6Ly91YWEucnVuLnBpdm90YWwuaW8vb2F1dGgvdG9rZW4iLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlci5yZWFkIiwiY2xvdWRfY29udHJvbGxlci53cml0ZSIsIm9wZW5pZCIsInBhc3N3b3JkLndyaXRlIl19.G8K9hVy2TGvxWEHMmVT86iQ5szMjnN0pWog2ASawpDiV8A4QODn9lJQq0G08LjjElV6wKQywAxM6eU8p32byW6RU9Tu-0iz9lW96aWSppTjsb4itbPLxsdMXLSRKOow0vuuGhwaTYx9OZIMpzNbXJVwbRRyWlhty6LVrEZp3hG37HO-N7g2oJdFZwxATaE63iL5ZnikcvKrPkBTKUGZ8OIAvsAlHQiEnbB8mfaw6Bh74ciTjOl0DYbHlZoEMQazXkLnY3INgCyErRcjtNkjRQGe6fOV4v1Wx3PAZ05gaBsAOaThgifz4Rmaf--hnrhtYI5F3g17tDmht6udZv1_C6A","expires_in":43199,"scope":"cloud_controller.read cloud_controller.write openid password.write","jti":"c6a7c136-6497-4faf-8799-4c42e1f3c6f5"} +` + + expected := ` +HTTP/1.1 200 OK +Content-Length: 2132 +Cache-Control: no-cache +Cache-Control: no-store +Cache-Control: no-store +Connection: keep-alive +Content-Type: application/json;charset=UTF-8 +Date: Thu, 05 Sep 2013 16:31:43 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Pragma: no-cache +Pragma: no-cache +Server: Apache-Coyote/1.1 + +{"access_token":"[PRIVATE DATA HIDDEN]","token_type":"bearer","refresh_token":"[PRIVATE DATA HIDDEN]","expires_in":43199,"scope":"cloud_controller.read cloud_controller.write openid password.write","jti":"c6a7c136-6497-4faf-8799-4c42e1f3c6f5"} +` + + Expect(Sanitize(response)).To(Equal(expected)) + }) + + It("hides service auth tokens in the request body", func() { + response := ` +HTTP/1.1 200 OK +Content-Length: 2132 +Cache-Control: no-cache +Cache-Control: no-store +Cache-Control: no-store +Connection: keep-alive +Content-Type: application/json;charset=UTF-8 +Date: Thu, 05 Sep 2013 16:31:43 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Pragma: no-cache +Pragma: no-cache +Server: Apache-Coyote/1.1 + +{"label":"some label","provider":"some provider","token":"some-token-with-stuff-in-it"} +` + + expected := ` +HTTP/1.1 200 OK +Content-Length: 2132 +Cache-Control: no-cache +Cache-Control: no-store +Cache-Control: no-store +Connection: keep-alive +Content-Type: application/json;charset=UTF-8 +Date: Thu, 05 Sep 2013 16:31:43 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Pragma: no-cache +Pragma: no-cache +Server: Apache-Coyote/1.1 + +{"label":"some label","provider":"some provider","token":"[PRIVATE DATA HIDDEN]"} +` + + Expect(Sanitize(response)).To(Equal(expected)) + }) + }) +}) diff --git a/cf/ui_helpers/logs.go b/cf/ui_helpers/logs.go new file mode 100644 index 00000000000..d650916b22a --- /dev/null +++ b/cf/ui_helpers/logs.go @@ -0,0 +1,71 @@ +package ui_helpers + +import ( + "fmt" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/loggregatorlib/logmessage" +) + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func ExtractLogHeader(msg *logmessage.LogMessage, loc *time.Location) (logHeader, coloredLogHeader string) { + logMsg := msg + sourceName := logMsg.GetSourceName() + sourceID := logMsg.GetSourceId() + t := time.Unix(0, logMsg.GetTimestamp()) + timeFormat := "2006-01-02T15:04:05.00-0700" + timeString := t.In(loc).Format(timeFormat) + + logHeader = fmt.Sprintf("%s [%s]", timeString, sourceName) + coloredLogHeader = terminal.LogSysHeaderColor(logHeader) + + if sourceName == "App" { + logHeader = fmt.Sprintf("%s [%s/%s]", timeString, sourceName, sourceID) + coloredLogHeader = terminal.LogAppHeaderColor(logHeader) + } + + // Calculate padding + longestHeader := fmt.Sprintf("%s [App/0] ", timeFormat) + expectedHeaderLength := utf8.RuneCountInString(longestHeader) + padding := strings.Repeat(" ", max(0, expectedHeaderLength-utf8.RuneCountInString(logHeader))) + + logHeader = logHeader + padding + coloredLogHeader = coloredLogHeader + padding + + return +} + +var newLinesPattern = regexp.MustCompile("[\n\r]+$") + +func ExtractLogContent(logMsg *logmessage.LogMessage, logHeader string) (logContent string) { + msgText := string(logMsg.GetMessage()) + msgText = newLinesPattern.ReplaceAllString(msgText, "") + + msgLines := strings.Split(msgText, "\n") + padding := strings.Repeat(" ", utf8.RuneCountInString(logHeader)) + coloringFunc := terminal.LogStdoutColor + logType := "OUT" + + if logMsg.GetMessageType() == logmessage.LogMessage_ERR { + coloringFunc = terminal.LogStderrColor + logType = "ERR" + } + + logContent = fmt.Sprintf("%s %s", logType, msgLines[0]) + for _, msgLine := range msgLines[1:] { + logContent = fmt.Sprintf("%s\n%s%s", logContent, padding, msgLine) + } + logContent = coloringFunc(logContent) + + return +} diff --git a/cf/ui_helpers/ui.go b/cf/ui_helpers/ui.go new file mode 100644 index 00000000000..33cedc50f7d --- /dev/null +++ b/cf/ui_helpers/ui.go @@ -0,0 +1,71 @@ +package ui_helpers + +import ( + "fmt" + "strings" + + . "github.com/cloudfoundry/cli/cf/i18n" + + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal" +) + +func ColoredAppState(app models.ApplicationFields) string { + appState := strings.ToLower(app.State) + + if app.RunningInstances == 0 { + if appState == "stopped" { + return appState + } else { + return terminal.CrashedColor(appState) + } + } + + if app.RunningInstances < app.InstanceCount { + return terminal.WarningColor(appState) + } + + return appState +} + +func ColoredAppInstances(app models.ApplicationFields) string { + healthString := fmt.Sprintf("%d/%d", app.RunningInstances, app.InstanceCount) + + if app.RunningInstances < 0 { + healthString = fmt.Sprintf("?/%d", app.InstanceCount) + } + + if app.RunningInstances == 0 { + if strings.ToLower(app.State) == "stopped" { + return healthString + } else { + return terminal.CrashedColor(healthString) + } + } + + if app.RunningInstances < app.InstanceCount { + return terminal.WarningColor(healthString) + } + + return healthString +} + +func ColoredInstanceState(instance models.AppInstanceFields) (colored string) { + state := string(instance.State) + switch state { + case "started", "running": + colored = T("running") + case "stopped": + colored = terminal.StoppedColor(T("stopped")) + case "flapping": + colored = terminal.CrashedColor(T("crashing")) + case "down": + colored = terminal.CrashedColor(T("down")) + case "starting": + colored = terminal.AdvisoryColor(T("starting")) + default: + colored = terminal.WarningColor(state) + } + + return +} diff --git a/cf_commands_excluded.json b/cf_commands_excluded.json new file mode 100644 index 00000000000..8de703ccd74 --- /dev/null +++ b/cf_commands_excluded.json @@ -0,0 +1,200 @@ +{ + "excludedStrings": [ + ".", + "\\", + "help", + ".go", + "", + "/", + "false", + "true", + + "%.1f", + ".0", + "(?i)^(-?\\d+)([KMGT])B?$", + + "buildpacks", + "CF_NAME buildpacks", + "enable", + "disable", + "buildpack", + "lock", + "unlock", + "true", + "false", + "CF_COLOR", + "\u001b[%d;%dm%s\u001b[0m", + "windows", + "\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", + "login", + "QuietPanic", + "%s t -o Org", + ".", + "%s\n", + "/bin/stty", + "stty", + "-echo", + "echo", + "\u003e", + "kernel32", + "SetConsoleMode", + "%s target -s SPACE", + "%s target -o ORG -s SPACE", + " target -o ", + "spaces", + + "invalid_token", + "Authorization", + "Authorization: ", + "(?m)^Authorization: .*", + "multipart/form-data", + "GET", + "PUT", + "POST", + "DELETE", + "Content-Type", + "access_token", + "refresh_token", + "token", + "password", + "oldPassword", + "password=", + "password=[^\u0026]*\u0026", + "\"%s\":\\s*\"[^\"]*\"", + "\u0026", + "go-cli ", + "application/json", + "accept", + " / ", + "/jobs/", + "X-Cf-Warnings", + "content-type", + "User-Agent", + + "api", + "apps", + "auth", + + "buildpacks", + + "config", + + "create-buildpack", + "create-domain", + "create-org", + "create-service", + "create-service-auth-token", + "create-service-broker", + "create-user", + "create-user-provided-service", + + "curl", + + "delete", + "delete-buildpack", + "delete-domain", + "delete-shared-domain", + "delete-org", + "delete-orphaned-routes", + "delete-route", + "delete-service", + "delete-service-auth-token", + "delete-service-broker", + "delete-space", + "delete-user", + + "domains", + + "env", + + "events", + + "files", + + "login", + + "logout", + + "logs", + + "marketplace", + + "org", + "org-users", + "orgs", + + "passwd", + + "purge-service-offering", + + "quotas", + "quota", + + "create-quota", + "update-quota", + "delete-quota", + + "rename", + "rename-buildpack", + "rename-org", + "rename-service", + "rename-service-broker", + "rename-space", + + "routes", + + "service", + "service-auth-tokens", + "service-brokers", + "services", + + "migrate-service-instances", + + "set-env", + "set-org-role", + "set-quota", + + "create-shared-domain", + + "space", + "space-users", + "spaces", + + "stacks", + + "target", + + "unbind-service", + + "unset-env", + "unset-org-role", + "unset-space-role", + + "update-buildpack", + "update-service-broker", + "update-service-auth-token", + "update-user-provided-service", + + "create-route", + "map-route", + + "unmap-route", + + "app", + "bind-service", + "scale", + "start", + "stop", + "restart", + "restage", + "push" + ], + "excludedRegexps": [ + "^(?:\\w+-)+(?:role|user|users|buildpack|space|auth-tokens?)$", + "^(?:[\\W]*%(?:[#v]|[%EGUTXbcdefgopqstvx]))+[\\W]*$", + "^\\d+$", + "^\\s*-\\w\\s*$", + "^\\w$", + "^json:" + ] +} diff --git a/ci/scripts/build-and-release-gocd b/ci/scripts/build-and-release-gocd new file mode 100755 index 00000000000..eacab1848b4 --- /dev/null +++ b/ci/scripts/build-and-release-gocd @@ -0,0 +1,13 @@ +#!/bin/bash + +chmod +x out/cf-darwin-amd64 +chmod +x out/cf-linux-386 +chmod +x out/cf-linux-amd64 +chmod +x out/cf-windows-amd64.exe +chmod +x out/cf-windows-386.exe + +ci/scripts/build-installers-gocd +ci/scripts/tar-executables +ci/scripts/upload-binaries-gocd + +#( /bin/bash --login -c "rvm use 1.9 && bin/pivotal-tracker-deliver"; exit 0 ) diff --git a/ci/scripts/build-installers-gocd b/ci/scripts/build-installers-gocd new file mode 100755 index 00000000000..5ec161ca857 --- /dev/null +++ b/ci/scripts/build-installers-gocd @@ -0,0 +1,109 @@ +#!/bin/bash + +set -e + +ROOT_DIR=$(pwd) +OUT_DIR=${ROOT_DIR}/out +RELEASE_DIR=${ROOT_DIR}/release +INSTALLERS_DIR=${ROOT_DIR}/installers +VERSION=$(${OUT_DIR}/cf-linux-386 -v | cut -d' ' -f 3 | cut -d'-' -f 1) + + +# Instructions for installing iscc: +# https://katastrophos.net/andre/blog/2009/03/16/setting-up-the-inno-setup-compiler-on-debian/ +# +# forward X11 ports when installing 'Inno Setup' on the linux vm where the gocd agent runs +# $ ssh -X -i id_rsa.pem ubuntu@x.x.x.x' + +echo "building windows-386 installer" +( + cd ${INSTALLERS_DIR}/windows + cp ${OUT_DIR}/cf-windows-386.exe cf.exe + + sed -i -e "s/VERSION/${VERSION}/" ${ROOT_DIR}/ci/scripts/windows-installer.iss + + # Change the Unix file path to a Windows file path for the Inno Setup script. + sed -i -e "s/CF_SOURCE/$(echo "z:$(pwd)/cf.exe" | sed 's,/,\\\\,g')/" ${ROOT_DIR}/ci/scripts/windows-installer.iss + + ${ROOT_DIR}/ci/scripts/iscc ${ROOT_DIR}/ci/scripts/windows-installer.iss + mv ${ROOT_DIR}/ci/scripts/Output/setup.exe cf_installer.exe + zip ${ROOT_DIR}/release/installer-windows-386.zip cf_installer.exe + rm cf_installer.exe cf.exe +) + +echo "building windows-amd64 installer" +( + cd ${INSTALLERS_DIR}/windows + cp ${OUT_DIR}/cf-windows-amd64.exe cf.exe + ${ROOT_DIR}/ci/scripts/iscc ${ROOT_DIR}/ci/scripts/windows-installer.iss + mv ${ROOT_DIR}/ci/scripts/Output/setup.exe cf_installer.exe + zip ${RELEASE_DIR}/installer-windows-amd64.zip cf_installer.exe + rm cf_installer.exe cf.exe +) + +echo "building i386 DEB package" +( + cd ${INSTALLERS_DIR}/deb + mkdir -p cf/usr/bin + cp ${OUT_DIR}/cf-linux-386 cf/usr/bin/cf + cp control.template cf/DEBIAN/control + echo "Version: ${VERSION}" >> cf/DEBIAN/control + echo "Architecture: i386" >> cf/DEBIAN/control + fakeroot dpkg --build cf cf-cli_i386.deb + mv cf-cli_i386.deb ${RELEASE_DIR}/ + rm -rf cf/usr/bin cf/DEBIAN/control +) + +echo "building amd64 DEB package" +( + cd ${INSTALLERS_DIR}/deb + mkdir -p cf/usr/bin + cp ${OUT_DIR}/cf-linux-amd64 cf/usr/bin/cf + cp control.template cf/DEBIAN/control + echo "Version: ${VERSION}" >> cf/DEBIAN/control + echo "Architecture: amd64" >> cf/DEBIAN/control + fakeroot dpkg --build cf cf-cli_amd64.deb + mv cf-cli_amd64.deb ${RELEASE_DIR}/ + rm -rf cf/usr/bin cf/DEBIAN/control +) + +echo "building i386 RPM package" +( + cd ${INSTALLERS_DIR}/rpm + cp ${OUT_DIR}/cf-linux-386 cf + RPM_VERSION=$(echo $VERSION | sed 's/-/_/') + echo "Version: ${RPM_VERSION}" > cf-cli.spec + cat cf-cli.spec.template >> cf-cli.spec + rpmbuild --target i386 --define "_topdir $(pwd)/build" -bb cf-cli.spec + mv build/RPMS/i386/cf-cli*.rpm ${RELEASE_DIR}/cf-cli_i386.rpm + rm -rf build cf cf-cli.spec +) + +echo "building amd64 RPM package" +( + cd ${INSTALLERS_DIR}/rpm + cp ${OUT_DIR}/cf-linux-amd64 cf + RPM_VERSION=$(echo $VERSION | sed 's/-/_/') + echo "Version: ${RPM_VERSION}" > cf-cli.spec + cat cf-cli.spec.template >> cf-cli.spec + rpmbuild --target x86_64 --define "_topdir $(pwd)/build" -bb cf-cli.spec + mv build/RPMS/x86_64/cf-cli*.rpm ${RELEASE_DIR}/cf-cli_amd64.rpm + rm -rf build cf cf-cli.spec +) + +echo "building OS X installer" +( + cd ${INSTALLERS_DIR}/osx + mkdir -p cf-cli/usr/local/bin + mkdir -p cf-cli/usr/local/share/doc/cf-cli + cp ${OUT_DIR}/cf-darwin-amd64 cf-cli/usr/local/bin/cf + cp COPYING cf-cli/usr/local/share/doc/cf-cli + chmod -R go-w cf-cli + ( cd cf-cli && find usr | cpio -o --format=odc | gzip -c > ../Payload ) + ls4mkbom cf-cli | sed 's/1000\/1000/0\/80/' > bom_list + mkbom -i bom_list Bom + mv Bom Payload com.cloudfoundry.cli.pkg + xar -c --compression none -f installer-osx-amd64.pkg com.cloudfoundry.cli.pkg Distribution + mv installer-osx-amd64.pkg ${RELEASE_DIR}/ + rm -rf cf-cli com.cloudfoundry.cli.pkg/Payload com.cloudfoundry.cli.pkg/Bom bom_list +) diff --git a/ci/scripts/iscc b/ci/scripts/iscc new file mode 100755 index 00000000000..59dfdc14d30 --- /dev/null +++ b/ci/scripts/iscc @@ -0,0 +1,28 @@ +#!/bin/sh + +set -e + +SCRIPTNAME=$1 +INNO_BIN="Inno Setup 5/ISCC.exe" + +# Check if variable is set +[ -z "$SCRIPTNAME" ] && { echo "Usage: $0 "; echo; exit 1; } + +# Check if filename exist +[ ! -f "$SCRIPTNAME" ] && { echo "File not found. Aborting."; echo; exit 1; } + +# Check if wine is present +command -v wine >/dev/null 2>&1 || { echo >&2 "I require wine but it's not installed. Aborting."; echo; exit 1; } + +# Get inno setup path +INNO_PATH="${WINE_DIR}/.wine/dosdevices/c:/Program Files/${INNO_BIN}" +echo $INNO_PATH + +# Translate unix script path to windows path +SCRIPTNAME=$(winepath -w "$SCRIPTNAME" 2> /dev/null) + +# Check if Inno Setup is installed into wine +#[ ! -f "$INNO_PATH" ] && { echo "Install Inno Setup 5 Quickstart before running this script."; echo; exit 1; } + +# Compile! +wine "$INNO_PATH" "$SCRIPTNAME" diff --git a/ci/scripts/tar-executables b/ci/scripts/tar-executables new file mode 100755 index 00000000000..8f0d441ccbd --- /dev/null +++ b/ci/scripts/tar-executables @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p out release +( + cd out + + for BIN in cf-[ld]*; do + cp ${BIN} cf + tar cvzf ../release/${BIN}.tgz cf + rm cf + done + + for BIN in cf-windows*; do + cp ${BIN} cf.exe + zip ../release/${BIN%exe}zip cf.exe + rm cf.exe + done +) diff --git a/ci/scripts/upload-binaries-gocd b/ci/scripts/upload-binaries-gocd new file mode 100755 index 00000000000..3634992cf2e --- /dev/null +++ b/ci/scripts/upload-binaries-gocd @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -e + +if [ -z "$AWS_ACCESS_KEY_ID" ]; then + echo "Need to set AWS_ACCESS_KEY_ID" + exit 1 +fi + +if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + echo "Need to set AWS_SECRET_ACCESS_KEY" + exit 1 +fi + +s3_config_file=$(pwd)/../../../../cli-ci/ci/s3cfg + +function upload_artifacts { + s3_path_prefix=$1 + + for file in $(ls release) + do + echo s3cmd --config=$s3_config_file put release/$file s3://go-cli/$s3_path_prefix/$file + s3cmd --config=$s3_config_file put release/$file s3://go-cli/$s3_path_prefix/$file + done +} + +release_tags=$(git show-ref --tags -d | grep $(git rev-parse HEAD) | cut -d'/' -f3 | egrep 'v[0-9]'; exit 0) +latest_release_tag=$(git tag | egrep 'v[0-9]' | sort | tail -n 1; exit 0) + +for tag in $release_tags +do + echo "Uploading artifacts for release" $tag + upload_artifacts "releases/$tag" +done + +# Only upload to the 'latest' bucket if we're building some +# commit *after* the latest release + +# this tries to avoid uploading a release to the "latest" bucket if we're +# actually building an older tag, which would result in us overwriting the +# edge build with an older version. +git merge-base --is-ancestor $latest_release_tag HEAD +if [ $? -eq 0 ] +then + echo "Uploading master artifacts" + upload_artifacts "master" +fi diff --git a/ci/scripts/windows-installer.iss b/ci/scripts/windows-installer.iss new file mode 100755 index 00000000000..8cab0573d7b --- /dev/null +++ b/ci/scripts/windows-installer.iss @@ -0,0 +1,30 @@ +[Setup] +ChangesEnvironment=yes +AppName=Cloud Foundry CLI +AppVersion=VERSION +AppVerName=VERSION +DefaultDirName={pf}\CloudFoundry + +[Registry] +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Check: NeedsAddPath(ExpandConstant('{app}')) + +[Files] +Source: CF_SOURCE; DestDir: "{app}" + +[Code] + +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', OrigPath) + then begin + Result := True; + exit; + end; + // look for the path with leading and trailing semicolon + // Pos() returns 0 if not found + Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; +end; diff --git a/excluded.json b/excluded.json new file mode 100644 index 00000000000..d7d14dadee3 --- /dev/null +++ b/excluded.json @@ -0,0 +1,158 @@ +{ + "excludedStrings": [ + ".", + "\\", + "help", + ".go", + "", + "/", + "false", + "true", + "%.1f", + ".0", + "(?i)^(-?\\d+)([KMGT])B?$", + "buildpacks", + "CF_NAME buildpacks", + "enable", + "disable", + "buildpack", + "lock", + "unlock", + "true", + "false", + "CF_COLOR", + "\u001b[%d;%dm%s\u001b[0m", + "windows", + "\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", + "login", + "QuietPanic", + "%s t -o Org", + ".", + "%s\n", + "/bin/stty", + "stty", + "-echo", + "echo", + "\u003e", + "kernel32", + "SetConsoleMode", + "%s target -s SPACE", + "%s target -o ORG -s SPACE", + " target -o ", + "spaces", + "invalid_token", + "Authorization", + "Authorization: ", + "(?m)^Authorization: .*", + "multipart/form-data", + "GET", + "PUT", + "POST", + "DELETE", + "Content-Type", + "access_token", + "refresh_token", + "token", + "password", + "oldPassword", + "password=", + "password=[^\u0026]*\u0026", + "\"%s\":\\s*\"[^\"]*\"", + "\u0026", + "go-cli ", + "application/json", + "accept", + " / ", + "/jobs/", + "X-Cf-Warnings", + "content-type", + "User-Agent", + "push", + "cups", + "marketplace", + "uups", + "buildpacks", + "CF_NAME buildpacks", + "enable", + "disable", + "buildpack", + "lock", + "unlock", + "true", + "false", + "CF_COLOR", + "\u001b[%d;%dm%s\u001b[0m", + "windows", + "\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", + "login", + "QuietPanic", + "%s t -o Org", + ".", + "%s\n", + "/bin/stty", + "stty", + "-echo", + "echo", + "\u003e", + "kernel32", + "SetConsoleMode", + "%s target -s SPACE", + "%s target -o ORG -s SPACE", + " target -o ", + "spaces", + + "invalid_token", + "Authorization", + "Authorization: ", + "(?m)^Authorization: .*", + "multipart/form-data", + "GET", + "PUT", + "POST", + "DELETE", + "Content-Type", + "access_token", + "refresh_token", + "token", + "password", + "oldPassword", + "password=", + "password=[^\u0026]*\u0026", + "\"%s\":\\s*\"[^\"]*\"", + "\u0026", + "go-cli ", + "application/json", + "accept", + " / ", + "/jobs/", + "X-Cf-Warnings", + "content-type", + "User-Agent", + + "private_domains", + "shared_domains", + + "%s logs %s --recent", + "2006-01-02T15:04:05.00-0700", + "2006-01-02 03:04:05 PM", + + "CF_STAGING_TIMEOUT", + "CF_STARTUP_TIMEOUT", + + "manifest.yml", + "manifest.yaml", + "inherit" + + ], + "excludedRegexps": [ + "^CF_NAME (?:[\\w-]+)$", + "^(?:\\w+-)*(?:role|users?|buildpack|space|auth-tokens?|services?|security-groups?|quotas?)(?:-\\w+)*$", + "^(?:[\\W]*%(?:[#v]|[%EGUTXbcdefgopqstvx]))+[\\W]*$", + "^\\d+$", + "^\\s*-\\w\\s*$", + "^\\w$", + "^\\w{2}$", + "^json", + "^\\W+$" + ] +} diff --git a/fileutils/file_utils_notwin.go b/fileutils/file_utils_notwin.go new file mode 100644 index 00000000000..13bd3ab5310 --- /dev/null +++ b/fileutils/file_utils_notwin.go @@ -0,0 +1,13 @@ +// + +// +build !windows + +package fileutils + +import ( + "os" +) + +func IsRegular(f os.FileInfo) bool { + return f.Mode().IsRegular() +} diff --git a/fileutils/file_utils_windows.go b/fileutils/file_utils_windows.go new file mode 100644 index 00000000000..118f286779f --- /dev/null +++ b/fileutils/file_utils_windows.go @@ -0,0 +1,19 @@ +package fileutils + +import ( + "os" + "syscall" +) + +const ( + FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 +) + +func IsRegular(f os.FileInfo) bool { + if fileattrs, ok := f.Sys().(*syscall.Win32FileAttributeData); ok { + if fileattrs.FileAttributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return false + } + } + return f.Mode().IsRegular() +} diff --git a/fileutils/fileutils_suite_test.go b/fileutils/fileutils_suite_test.go new file mode 100644 index 00000000000..71fd2a45a7f --- /dev/null +++ b/fileutils/fileutils_suite_test.go @@ -0,0 +1,13 @@ +package fileutils_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFileutils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fileutils Suite") +} diff --git a/fileutils/iocopy.go b/fileutils/iocopy.go new file mode 100644 index 00000000000..f329686005a --- /dev/null +++ b/fileutils/iocopy.go @@ -0,0 +1,44 @@ +package fileutils + +import ( + "io" + "os" + "runtime" +) + +func CopyFile(dst, src string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + err = out.Close() + if err != nil { + return err + } + + fileInfo, err := os.Stat(src) + if err != nil { + return err + } + + if runtime.GOOS != "windows" { + err = os.Chmod(dst, fileInfo.Mode()) + if err != nil { + return err + } + } + + return nil +} diff --git a/fileutils/iocopy_test.go b/fileutils/iocopy_test.go new file mode 100644 index 00000000000..79f592e65d4 --- /dev/null +++ b/fileutils/iocopy_test.go @@ -0,0 +1,31 @@ +// +build darwin freebsd linux netbsd openbsd + +package fileutils_test + +import ( + . "github.com/cloudfoundry/cli/fileutils" + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Iocopy", func() { + Describe(".CopyFile", func() { + It("copies a file with correct permisions", func() { + someFile, err := ioutil.TempFile("", "blarg") + err = os.Chmod(someFile.Name(), 0731) + Expect(err).ToNot(HaveOccurred()) + + newDir, _ := ioutil.TempDir("", "") + + err = CopyFile(filepath.Join(newDir, "baz"), someFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + fileStat, _ := os.Stat(filepath.Join(newDir, "baz")) + Expect(int(fileStat.Mode())).To(Equal(0731)) + }) + }) +}) diff --git a/fileutils/tmp_utils.go b/fileutils/tmp_utils.go new file mode 100644 index 00000000000..4961a8d40b7 --- /dev/null +++ b/fileutils/tmp_utils.go @@ -0,0 +1,27 @@ +package fileutils + +import ( + "io/ioutil" + "os" +) + +func TempDir(namePrefix string, cb func(tmpDir string, err error)) { + tmpDir, err := ioutil.TempDir("", namePrefix) + + defer func() { + os.RemoveAll(tmpDir) + }() + + cb(tmpDir, err) +} + +func TempFile(namePrefix string, cb func(tmpFile *os.File, err error)) { + tmpFile, err := ioutil.TempFile("", namePrefix) + + defer func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }() + + cb(tmpFile, err) +} diff --git a/fixtures/applications/app-copy-test/dir1/child-dir/file2.txt b/fixtures/applications/app-copy-test/dir1/child-dir/file2.txt new file mode 100644 index 00000000000..2e7e0ee18bd --- /dev/null +++ b/fixtures/applications/app-copy-test/dir1/child-dir/file2.txt @@ -0,0 +1 @@ +file2-content diff --git a/fixtures/applications/app-copy-test/dir1/child-dir/file3.txt b/fixtures/applications/app-copy-test/dir1/child-dir/file3.txt new file mode 100644 index 00000000000..328cb297377 --- /dev/null +++ b/fixtures/applications/app-copy-test/dir1/child-dir/file3.txt @@ -0,0 +1 @@ +file3-content diff --git a/fixtures/applications/app-copy-test/dir1/file1.txt b/fixtures/applications/app-copy-test/dir1/file1.txt new file mode 100644 index 00000000000..08d64e17937 --- /dev/null +++ b/fixtures/applications/app-copy-test/dir1/file1.txt @@ -0,0 +1 @@ +file1-content diff --git a/fixtures/applications/app-copy-test/dir2/child-dir2/grandchild-dir2/file4.txt b/fixtures/applications/app-copy-test/dir2/child-dir2/grandchild-dir2/file4.txt new file mode 100644 index 00000000000..a96cae0c0e9 --- /dev/null +++ b/fixtures/applications/app-copy-test/dir2/child-dir2/grandchild-dir2/file4.txt @@ -0,0 +1 @@ +file4-content diff --git a/fixtures/applications/app-with-cfignore/.cfignore b/fixtures/applications/app-with-cfignore/.cfignore new file mode 100644 index 00000000000..7301d991da5 --- /dev/null +++ b/fixtures/applications/app-with-cfignore/.cfignore @@ -0,0 +1,4 @@ +dir1/**/* +!dir1/file1.txt +!dir1/child-dir/file3.txt +dir2/**/* diff --git a/fixtures/applications/app-with-cfignore/dir1/child-dir/file2.txt b/fixtures/applications/app-with-cfignore/dir1/child-dir/file2.txt new file mode 100644 index 00000000000..2e7e0ee18bd --- /dev/null +++ b/fixtures/applications/app-with-cfignore/dir1/child-dir/file2.txt @@ -0,0 +1 @@ +file2-content diff --git a/fixtures/applications/app-with-cfignore/dir1/child-dir/file3.txt b/fixtures/applications/app-with-cfignore/dir1/child-dir/file3.txt new file mode 100644 index 00000000000..328cb297377 --- /dev/null +++ b/fixtures/applications/app-with-cfignore/dir1/child-dir/file3.txt @@ -0,0 +1 @@ +file3-content diff --git a/fixtures/applications/app-with-cfignore/dir1/file1.txt b/fixtures/applications/app-with-cfignore/dir1/file1.txt new file mode 100644 index 00000000000..08d64e17937 --- /dev/null +++ b/fixtures/applications/app-with-cfignore/dir1/file1.txt @@ -0,0 +1 @@ +file1-content diff --git a/fixtures/applications/app-with-cfignore/dir2/child-dir2/grandchild-dir2/file4.txt b/fixtures/applications/app-with-cfignore/dir2/child-dir2/grandchild-dir2/file4.txt new file mode 100644 index 00000000000..a96cae0c0e9 --- /dev/null +++ b/fixtures/applications/app-with-cfignore/dir2/child-dir2/grandchild-dir2/file4.txt @@ -0,0 +1 @@ +file4-content diff --git a/fixtures/applications/empty-dir/.gitignore b/fixtures/applications/empty-dir/.gitignore new file mode 100644 index 00000000000..d6b7ef32c84 --- /dev/null +++ b/fixtures/applications/empty-dir/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/fixtures/applications/example-app.azip b/fixtures/applications/example-app.azip new file mode 100644 index 00000000000..53863171c9b Binary files /dev/null and b/fixtures/applications/example-app.azip differ diff --git a/fixtures/applications/example-app.zip b/fixtures/applications/example-app.zip new file mode 100644 index 00000000000..906c12e3eaa Binary files /dev/null and b/fixtures/applications/example-app.zip differ diff --git a/fixtures/applications/example-app/.cfignore b/fixtures/applications/example-app/.cfignore new file mode 100644 index 00000000000..285078c9dd8 --- /dev/null +++ b/fixtures/applications/example-app/.cfignore @@ -0,0 +1,2 @@ +*.yml +ignore-me diff --git a/src/fixtures/example-app/Gemfile b/fixtures/applications/example-app/Gemfile old mode 100755 new mode 100644 similarity index 100% rename from src/fixtures/example-app/Gemfile rename to fixtures/applications/example-app/Gemfile diff --git a/src/fixtures/example-app/Gemfile.lock b/fixtures/applications/example-app/Gemfile.lock similarity index 100% rename from src/fixtures/example-app/Gemfile.lock rename to fixtures/applications/example-app/Gemfile.lock diff --git a/src/fixtures/example-app/app.rb b/fixtures/applications/example-app/app.rb similarity index 100% rename from src/fixtures/example-app/app.rb rename to fixtures/applications/example-app/app.rb diff --git a/src/fixtures/example-app/config.ru b/fixtures/applications/example-app/config.ru similarity index 100% rename from src/fixtures/example-app/config.ru rename to fixtures/applications/example-app/config.ru diff --git a/src/code.google.com/p/go.net/.hg/store/undo.phaseroots b/fixtures/applications/example-app/ignore-me similarity index 100% rename from src/code.google.com/p/go.net/.hg/store/undo.phaseroots rename to fixtures/applications/example-app/ignore-me diff --git a/src/fixtures/example-app/manifest.yml b/fixtures/applications/example-app/manifest.yml similarity index 100% rename from src/fixtures/example-app/manifest.yml rename to fixtures/applications/example-app/manifest.yml diff --git a/fixtures/applications/exclude-a-default-cfignore/.cfignore b/fixtures/applications/exclude-a-default-cfignore/.cfignore new file mode 100644 index 00000000000..71992e664cf --- /dev/null +++ b/fixtures/applications/exclude-a-default-cfignore/.cfignore @@ -0,0 +1,2 @@ +!.svn +!_darcs \ No newline at end of file diff --git a/fixtures/applications/exclude-a-default-cfignore/.svn/test b/fixtures/applications/exclude-a-default-cfignore/.svn/test new file mode 100644 index 00000000000..ce616e98b43 --- /dev/null +++ b/fixtures/applications/exclude-a-default-cfignore/.svn/test @@ -0,0 +1 @@ +a test file diff --git a/fixtures/applications/exclude-a-default-cfignore/_darcs b/fixtures/applications/exclude-a-default-cfignore/_darcs new file mode 100644 index 00000000000..30d74d25844 --- /dev/null +++ b/fixtures/applications/exclude-a-default-cfignore/_darcs @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/fixtures/applications/ignored_and_resource_matched_example_app.zip b/fixtures/applications/ignored_and_resource_matched_example_app.zip new file mode 100644 index 00000000000..0d07cc2d7ac Binary files /dev/null and b/fixtures/applications/ignored_and_resource_matched_example_app.zip differ diff --git a/fixtures/applications/test/.cfignore b/fixtures/applications/test/.cfignore new file mode 100644 index 00000000000..285078c9dd8 --- /dev/null +++ b/fixtures/applications/test/.cfignore @@ -0,0 +1,2 @@ +*.yml +ignore-me diff --git a/fixtures/applications/test/Gemfile b/fixtures/applications/test/Gemfile new file mode 100644 index 00000000000..efd542d96d4 --- /dev/null +++ b/fixtures/applications/test/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +ruby "1.9.3" + +gem "sinatra" diff --git a/fixtures/applications/test/Gemfile.lock b/fixtures/applications/test/Gemfile.lock new file mode 100644 index 00000000000..725fd367873 --- /dev/null +++ b/fixtures/applications/test/Gemfile.lock @@ -0,0 +1,16 @@ +GEM + specs: + rack (1.5.2) + rack-protection (1.5.0) + rack + sinatra (1.4.3) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + tilt (1.4.1) + +PLATFORMS + ruby + +DEPENDENCIES + sinatra diff --git a/fixtures/applications/test/app.rb b/fixtures/applications/test/app.rb new file mode 100755 index 00000000000..2bea8a22afb --- /dev/null +++ b/fixtures/applications/test/app.rb @@ -0,0 +1,5 @@ +require "sinatra" + +get "/" do + "Hello world!" +end diff --git a/fixtures/applications/test/config.ru b/fixtures/applications/test/config.ru new file mode 100644 index 00000000000..ca38dbf092c --- /dev/null +++ b/fixtures/applications/test/config.ru @@ -0,0 +1,2 @@ +require File.expand_path("../app", __FILE__) +run Sinatra::Application diff --git a/src/code.google.com/p/go.net/.hg/undo.bookmarks b/fixtures/applications/test/ignore-me similarity index 100% rename from src/code.google.com/p/go.net/.hg/undo.bookmarks rename to fixtures/applications/test/ignore-me diff --git a/fixtures/applications/test/manifest.yml b/fixtures/applications/test/manifest.yml new file mode 100644 index 00000000000..2ba7e121f7a --- /dev/null +++ b/fixtures/applications/test/manifest.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: hello + memory: 128M + instances: 1 + host: hello + domain: cli.cf-app.com + path: . diff --git a/fixtures/buildpacks/bad-buildpack.zip b/fixtures/buildpacks/bad-buildpack.zip new file mode 100644 index 00000000000..340a1db55e9 Binary files /dev/null and b/fixtures/buildpacks/bad-buildpack.zip differ diff --git a/fixtures/buildpacks/example-buildpack-in-dir.zip b/fixtures/buildpacks/example-buildpack-in-dir.zip new file mode 100644 index 00000000000..f6aa582e5a4 Binary files /dev/null and b/fixtures/buildpacks/example-buildpack-in-dir.zip differ diff --git a/fixtures/buildpacks/example-buildpack.zip b/fixtures/buildpacks/example-buildpack.zip new file mode 100644 index 00000000000..407c147d5ac Binary files /dev/null and b/fixtures/buildpacks/example-buildpack.zip differ diff --git a/fixtures/buildpacks/example-buildpack/bin/compile b/fixtures/buildpacks/example-buildpack/bin/compile new file mode 100755 index 00000000000..e44405ccbde --- /dev/null +++ b/fixtures/buildpacks/example-buildpack/bin/compile @@ -0,0 +1 @@ +the-compile-script diff --git a/fixtures/buildpacks/example-buildpack/bin/detect b/fixtures/buildpacks/example-buildpack/bin/detect new file mode 100755 index 00000000000..73cdb9fadd6 --- /dev/null +++ b/fixtures/buildpacks/example-buildpack/bin/detect @@ -0,0 +1 @@ +the-detect-script diff --git a/fixtures/buildpacks/example-buildpack/bin/release b/fixtures/buildpacks/example-buildpack/bin/release new file mode 100755 index 00000000000..369d3025639 --- /dev/null +++ b/fixtures/buildpacks/example-buildpack/bin/release @@ -0,0 +1 @@ +the-release-script diff --git a/fixtures/buildpacks/example-buildpack/lib/helper b/fixtures/buildpacks/example-buildpack/lib/helper new file mode 100644 index 00000000000..7b26c06eceb --- /dev/null +++ b/fixtures/buildpacks/example-buildpack/lib/helper @@ -0,0 +1 @@ +the-helper-script diff --git a/fixtures/config/help-plugin-test-config/.cf/plugins/config.json b/fixtures/config/help-plugin-test-config/.cf/plugins/config.json new file mode 100644 index 00000000000..b18771fd5c6 --- /dev/null +++ b/fixtures/config/help-plugin-test-config/.cf/plugins/config.json @@ -0,0 +1,19 @@ +{ + "Plugins": { + "Test1":{ + "Location":"../../fixtures/plugins/test_1.exe", + "Commands":[ + {"Name":"test1_cmd1","HelpText":"help text for test1 cmd1"}, + {"Name":"test1_cmd2","HelpText":"help text for test1 cmd2"} + ] + }, + "Test2":{ + "Location":"../../fixtures/plugins/test_2.exe", + "Commands":[ + {"Name":"test2_cmd1","HelpText":"help text for test2 cmd1"}, + {"Name":"test2_cmd2","HelpText":"help text for test2 cmd2"} + ] + } + } +} + diff --git a/fixtures/config/main-plugin-test-config/.cf/config.json b/fixtures/config/main-plugin-test-config/.cf/config.json new file mode 100644 index 00000000000..0dbd11a67e9 --- /dev/null +++ b/fixtures/config/main-plugin-test-config/.cf/config.json @@ -0,0 +1,30 @@ +{ + "ConfigVersion": 3, + "Target": "", + "ApiVersion": "", + "AuthorizationEndpoint": "", + "LoggregatorEndPoint": "", + "UaaEndpoint": "", + "AccessToken": "", + "RefreshToken": "", + "OrganizationFields": { + "Guid": "", + "Name": "", + "QuotaDefinition": { + "name": "", + "memory_limit": 0, + "total_routes": 0, + "total_services": 0, + "non_basic_services_allowed": false + } + }, + "SpaceFields": { + "Guid": "", + "Name": "" + }, + "SSLDisabled": false, + "AsyncTimeout": 0, + "Trace": "", + "ColorEnabled": "", + "Locale": "" +} \ No newline at end of file diff --git a/fixtures/config/main-plugin-test-config/.cf/plugins/config.json b/fixtures/config/main-plugin-test-config/.cf/plugins/config.json new file mode 100644 index 00000000000..0435a0a5b75 --- /dev/null +++ b/fixtures/config/main-plugin-test-config/.cf/plugins/config.json @@ -0,0 +1,58 @@ +{ + "Plugins": { + "Test1":{ + "Location":"../fixtures/plugins/test_1.exe", + "Commands":[ + {"Name":"test_1_cmd1","HelpText":"help text for test1 cmd1"}, + {"Name":"test_1_cmd2","HelpText":"help text for test1 cmd2"} + ] + }, + "Test2":{ + "Location":"../fixtures/plugins/test_2.exe", + "Commands":[ + {"Name":"test_2_cmd1","HelpText":"help text for test2 cmd1"}, + {"Name":"test_2_cmd2","HelpText":"help text for test2 cmd2"} + ] + }, + "TestWithPush":{ + "Location":"../fixtures/plugins/test_with_push.exe", + "Commands":[ + {"Name":"push","HelpText":"push text for test_with_push"} + ] + }, + "TestWithPushShortName":{ + "Location":"../fixtures/plugins/test_with_push_short_name.exe", + "Commands":[ + {"Name":"p","HelpText":"plugin short name p"} + ] + }, + "TestWithHelp":{ + "Location":"../fixtures/plugins/test_with_help.exe", + "Commands":[ + {"Name":"help","HelpText":"help text for test_with_help"} + ] + }, + "MySay":{ + "Location":"../fixtures/plugins/my_say.exe", + "Commands":[ + {"Name":"my-say","HelpText":"Help text for saying stuff"} + ] + }, + "Input":{ + "Location":"../fixtures/plugins/input.exe", + "Commands":[ + {"Name":"input","HelpText":"help text for input"} + ] + }, + "CoreCmd":{ + "Location":"../fixtures/plugins/call_core_cmd.exe", + "Commands":[ + {"Name":"awesomeness","HelpText":"the most awesomeness command you have ever seen"}, + {"Name":"core-command","HelpText":"runs core commands and dumps the output from the cli process"}, + {"Name":"core-command-quiet","HelpText":"runs core commands quietly and dumps the output from the cli process"} + ] + } + } +} + + diff --git a/fixtures/config/outdated-config/.cf/config.json b/fixtures/config/outdated-config/.cf/config.json new file mode 100644 index 00000000000..0dbd11a67e9 --- /dev/null +++ b/fixtures/config/outdated-config/.cf/config.json @@ -0,0 +1,30 @@ +{ + "ConfigVersion": 3, + "Target": "", + "ApiVersion": "", + "AuthorizationEndpoint": "", + "LoggregatorEndPoint": "", + "UaaEndpoint": "", + "AccessToken": "", + "RefreshToken": "", + "OrganizationFields": { + "Guid": "", + "Name": "", + "QuotaDefinition": { + "name": "", + "memory_limit": 0, + "total_routes": 0, + "total_services": 0, + "non_basic_services_allowed": false + } + }, + "SpaceFields": { + "Guid": "", + "Name": "" + }, + "SSLDisabled": false, + "AsyncTimeout": 0, + "Trace": "", + "ColorEnabled": "", + "Locale": "" +} \ No newline at end of file diff --git a/fixtures/config/plugin-config/.cf/config.json b/fixtures/config/plugin-config/.cf/config.json new file mode 100644 index 00000000000..085f2885628 --- /dev/null +++ b/fixtures/config/plugin-config/.cf/config.json @@ -0,0 +1,30 @@ +{ + "ConfigVersion": 3, + "Target": "", + "ApiVersion": "", + "AuthorizationEndpoint": "", + "LoggregatorEndpoint": "", + "UaaEndpoint": "", + "AccessToken": "", + "RefreshToken": "", + "OrganizationFields": { + "Guid": "", + "Name": "", + "QuotaDefinition": { + "name": "", + "memory_limit": 0, + "total_routes": 0, + "total_services": 0, + "non_basic_services_allowed": false + } + }, + "SpaceFields": { + "Guid": "", + "Name": "" + }, + "SSLDisabled": false, + "AsyncTimeout": 0, + "Trace": "", + "ColorEnabled": "", + "Locale": "", +} diff --git a/fixtures/config/plugin-config/.cf/plugins/config.json b/fixtures/config/plugin-config/.cf/plugins/config.json new file mode 100644 index 00000000000..b039625b481 --- /dev/null +++ b/fixtures/config/plugin-config/.cf/plugins/config.json @@ -0,0 +1,18 @@ +{ + "Plugins": { + "Test1":{ + "Location":"../../../fixtures/plugins/test_1.exe", + "Commands":[ + {"Name":"test_1_cmd1","HelpText":"help text for test1 cmd1"}, + {"Name":"test_1_cmd2","HelpText":"help text for test1 cmd2"} + ] + }, + "Test2":{ + "Location":"../../../fixtures/plugins/test_2.exe", + "Commands":[ + {"Name":"test_2_cmd1","HelpText":"help text for test2 cmd1"}, + {"Name":"test_2_cmd2","HelpText":"help text for test2 cmd2"} + ] + } + } +} diff --git a/fixtures/config/versioned-config/.cf/config.json b/fixtures/config/versioned-config/.cf/config.json new file mode 100644 index 00000000000..8d38a1ab7a6 --- /dev/null +++ b/fixtures/config/versioned-config/.cf/config.json @@ -0,0 +1 @@ +{"ConfigVersion":9001,"Target":"","ApiVersion":"","AuthorizationEndpoint":"","AccessToken":"","RefreshToken":"","OrganizationFields":{"Guid":"","Name":"","QuotaDefinition":{"Guid":"","Name":"","MemoryLimit":0}},"SpaceFields":{"Guid":"","Name":""},"ApplicationStartTimeout":30} diff --git a/src/fixtures/hello_world.txt b/fixtures/hello_world.txt similarity index 100% rename from src/fixtures/hello_world.txt rename to fixtures/hello_world.txt diff --git a/fixtures/manifests/base-manifest.yml b/fixtures/manifests/base-manifest.yml new file mode 100644 index 00000000000..9c457932891 --- /dev/null +++ b/fixtures/manifests/base-manifest.yml @@ -0,0 +1,8 @@ +--- +env: + foo: bar + will-be-overridden: baz +services: + - base-service +applications: + - name: base-app diff --git a/fixtures/manifests/both_yaml_yml/manifest.yaml b/fixtures/manifests/both_yaml_yml/manifest.yaml new file mode 100644 index 00000000000..cd4380ec225 --- /dev/null +++ b/fixtures/manifests/both_yaml_yml/manifest.yaml @@ -0,0 +1,4 @@ +--- +applications: +- name: yaml-yaml-yaml + memory: 256mb diff --git a/fixtures/manifests/both_yaml_yml/manifest.yml b/fixtures/manifests/both_yaml_yml/manifest.yml new file mode 100644 index 00000000000..c19488de829 --- /dev/null +++ b/fixtures/manifests/both_yaml_yml/manifest.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: yml-extension + memory: 256mb diff --git a/fixtures/manifests/different-manifest.yml b/fixtures/manifests/different-manifest.yml new file mode 100644 index 00000000000..837e69555f0 --- /dev/null +++ b/fixtures/manifests/different-manifest.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: from-different-manifest + memory: 128M + instances: 1 + host: hello + domain: cli.cf-app.com + path: . + env: + LD_LIBRARY_PATH: /usr/lib/somewhere diff --git a/src/code.google.com/p/go.net/.hg/undo.dirstate b/fixtures/manifests/empty-manifest.yml similarity index 100% rename from src/code.google.com/p/go.net/.hg/undo.dirstate rename to fixtures/manifests/empty-manifest.yml diff --git a/fixtures/manifests/inherited-manifest.yml b/fixtures/manifests/inherited-manifest.yml new file mode 100644 index 00000000000..a8db2ae6e71 --- /dev/null +++ b/fixtures/manifests/inherited-manifest.yml @@ -0,0 +1,8 @@ +--- +inherit: base-manifest.yml +env: + will-be-overridden: my-value +applications: + - name: my-app + services: + - foo-service diff --git a/fixtures/manifests/manifest.yml b/fixtures/manifests/manifest.yml new file mode 100644 index 00000000000..a369ee958e1 --- /dev/null +++ b/fixtures/manifests/manifest.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: from-default-manifest + memory: 256mb diff --git a/fixtures/manifests/only_yaml/manifest.yaml b/fixtures/manifests/only_yaml/manifest.yaml new file mode 100644 index 00000000000..a369ee958e1 --- /dev/null +++ b/fixtures/manifests/only_yaml/manifest.yaml @@ -0,0 +1,4 @@ +--- +applications: +- name: from-default-manifest + memory: 256mb diff --git a/fixtures/plugins/call_core_cmd.go b/fixtures/plugins/call_core_cmd.go new file mode 100644 index 00000000000..54ddb116651 --- /dev/null +++ b/fixtures/plugins/call_core_cmd.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type CoreCmd struct{} + +func (c *CoreCmd) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "CoreCmd", + Commands: []plugin.Command{ + { + Name: "awesomeness", + HelpText: "the most awesomeness command you have ever seen", + }, + { + Name: "core-command", + HelpText: "command to call core command. It passes all text through to command", + }, + { + Name: "core-command-quiet", + HelpText: "command to call core command, disabling output to the terminal. It passes all text through to command", + }, + }, + } +} + +func main() { + plugin.Start(new(CoreCmd)) +} + +func dumpOutput(output []string) { + fmt.Println("") + fmt.Println("---------- Command output from the plugin ----------") + for index, val := range output { + fmt.Print("#", index, " value: ", val) + } + fmt.Println("---------- FIN -----------") +} + +func (c *CoreCmd) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "core-command" { + output, err := cliConnection.CliCommand(args[1:]...) + if err != nil { + fmt.Println("PLUGIN ERROR: Error from CliCommand: ", err) + } + dumpOutput(output) + } else if args[0] == "core-command-quiet" { + output, err := cliConnection.CliCommandWithoutTerminalOutput(args[1:]...) + if err != nil { + fmt.Println("PLUGIN ERROR: Error from CliCommand: ", err) + } + dumpOutput(output) + } else if args[0] == "awesomeness" { + cliConnection.CliCommand("plugins") + } else if len(args) == 2 && args[0] == "awesomeness" && args[1] == "easter_egg" { + fmt.Println(` +ZZZZ$Z$$$ZZ$$$$$77$777777777777777777777I77II7I?III?IIII???????+++????????++++++=++++++++++++++++++=++======+======+++++ +ZZZZZZZZ$$ZZ$77777777777III777777I7777IIIIIIIII??IIIIII???????++++???????++++++++++++?????++++++++++========+===~+=+++++ +ZZZZZZ$$$$$$77777$$77I77777777III77IIIIII7IIIII????II????I7$ZZZ$$7I???????++?+++++++++++++++++++++++==+=============++++ +$ZZ77$7$$$$$777777777777I7777IIII77I777III?III??????IDNNMNNNNNDDDNNNDDO7????+++++=++=++++++++=++++====+========~~===++++ +ZZ$$$7777$$$$$777777777777IIIIIIII7IIIIIIIIII?I??+IZNND8NNNMMMMNNNDDDNND8?+??+?++++++++++++++==+=+===++=======~~~====+++ +$$$$$$$$Z$$$$7777777777I7I?II??IIIIII?III???????$DMNN8O8NNNNNNNNMMMMND8DNMD$+++?++==++++++++=======~===========~~======+ +$$$$Z$$$$$$$7777I777IIIIII?IIIIIIIII?????+????ZNND8DDZZDDDDDDDNNNNNNMMMMNNNNNZ?++=====+++++++=====~~==~===~==~=~=~====== +$$$7$$$7$777III777IIIIIIIIIII??III??III???++?$MNNDDD8$O88DDDNMNNNNNNMMMMMMDNMMN7+=====+++++==========~~~~~~~~~~=~======+ +77777777777777IIIII7IIIII????????II?????++++$DN8Z7?+++=++???I$ODNNNMMMMMNNNNNMMM+=+===~==========~==~~~~~~~~~~~~=~====== +$$77777777777I77IIIIIIII????II?III?????++++I8N$I?+======++???I$O8DDNNNMMNNNMNMNMZ+=+==~===~~====~==~~~~~~~~~~~~~~~====== +$$77777777III77IIIIIIII??????I?I????+++++?I8NOI?=~~~~~~~~~==+?I7$ZDNNNNMMMMNNNMNMO+===~~===~~~~~~~~~~~~~~~~~~::~~======= +$$$$$$77II77IIIIIIIIIII?????????????+++++7NM87+=~:~~:~~~~~~==++I7$8DNNNMNMMMMMNMNMO=~=======~~~~~~~:~~~~~~:::::::~~===== +77777$$7IIII?II??II??????????++++??++===$NMNZI+=~~~~~~~~~~~==+?I7$OO8DNMNNNMNNMMNMM$==+++===~===~~~~~~~~:::::::::~~~==== +7II?I7I77III???+????++???????++????+==~+$DMD$I+=======~=====+???I7$ZO8DDNNMMNNMMMMMD???+++++++==~=~~~~=~~~~~:::::~~~==+= +777I777$7II???????++++??????+++???++==+IONM8$I+============+++??II7$ZZODNNNMMMMMMMMM7?I??????+=====~~~~~~~~~~::::~~~===+ +$7777$$ZZ7I7I??++?I?++?++???+++++++===7$DNNO7I++===~~=======++++++?I7$Z8NNNNNMNMNMMM8I7IIII7I+=====~+=~~::::~~~::~~~===+ +ZZZOOZ8OO$7III?II77I?+++++++++=====+=?78DMMZI?=+===~=~=~==++=+++???II7Z8NMMMMMMMNMNMD7$$$I77I????+++?=~~~~~~~::::::~==++ +O8888ZZZ88Z$$$$7777$7?I7?++=+=++====+7$8NMMOI?????+=+===?7$7IIIII77$ZZODNNMMMMMMNNMNN$OZ$7IIII?I??++?+?+=~~:~~:::::===++ +O8DD8OOOOOOOZ$$$$$Z$$$Z$7I++++++====?$Z8MMM87?I77$I++++?$ZOZZZZOZ$OOOOO8NNMMMMMMNNNMMOZ$ZZ$$777I??+??+?+=~~:~~~::::~==== +DDDD88OOOZZZZOOOZZOOOOZ$ZZ7+?+=++=++IOO8DNMDDDI777$ZI+?ZDDZ7$OOO8DMNO$ZODMMMNNNNMMMNMDOZZZZZ$III?I7$7I??=~~~~~~~:~==++++ +888888888OOOO8OZO8OOOZO$7$Z+?+=+++??IZDNNMMOZDIZNDO7I=I8O$I?IOZZZZ???I7O8MMNNMMMMMMMMNOOZZZZZ7$$$77$77I++~:::::~:~====+? +DD8D8O8OOO888OOOZ8D8O88ZZ8+???IZO$???ONNMMMZIIII$Z7?+=7ZZ7??I77$7I++?I7ODMMMMMMMMNNNMMO8OOOZ$7$$$77$777I?=~~~~~~~===+++? +8D88OO88OOO8888OODD888OOO8OZ7$OO$8Z7ZO8NMMN7?+=====+?+I77I?=~~~~~=??I7Z8DMMMMNMMMMMNNMZZZZZ$$777$$$Z$$$7I+==~==~=====++? +8DDDO88888888DDD8DD88DD8O888OZOZ$DO$OO8NMMM$?+====++++?II?+==~~~==?I7$Z8NMMMMNMMMMNNNMZZZZZZ$$Z$$$$ZZZ$7I?==========++?? +DD888D8OO8DDD8ODNDD88DDD8D8D8O8OODOO88DNNMM8I+=~==+++=?II?++====++I7$ZODNMMMNMMNMMNNNM77777I??I???????????++++++???????I +NMMNNNNNDDDND8D88DD8DDDDDD8DD8D8D8DNNNNNNMMD7+++==++++II7I?++==++?7$ZO8NNMMMNNNMMMMMNN88OOOOO$$$$$$$7I??I???++??III7$$7I +NMMNDDNNNDDDD88O88888D8DN8DDDDDDNNNMMNNNMMMMZ7++??I?=?777ZI++=+??I$ZOODNNMMMMMMMMMMMMNDD8O8OOOZZZZZZZ$ZZ$7I7777I7I7$$ZZZ +NNNNNDDNNDDDNDNDD888DD88DDDDDDDDDNNNNNMMMMMMD$II?++?$8NMND7++??II7ZO88DNMMMMMMMMMMMMMMNDDD888OOOOOO8OZ$Z$$$ZZZ$$$$$$$$ZZ +MMNMNDDNNNDDNDDDNDD8DD888D88DDDDNNNMNNNMMMMMNZ7II?==?NNN8ZI??III7$$ZO8DNMMMMMMMMMMMMMMN8OOOOOOOOZOOOZOZZZZOZZZZ$$$$$$ZZ$ +NNNMMNNDDNDDNDDDNND8DDDDDD88DDDDDNDNMMMMNMMMMO7I77?++I7$$77Z7IIII7ZZO8DNNMMMMMMMMMMMMNND888ZOOOZZZOOOOZZZZ$$77777$$ZZOO8 +MNNMNDDDNNNNNDDDDDD8DDNDDDDDDNNDNDDDNMMMNNMMMN7II7I?77$ZOOZ7I??II7ZO8DNNNMMMNNMMMMMMMNNN8OOZOZZZZZZZZZZZZ$$$$$$$$$$ZOOOO +NMMNDNNNNNDDDDDDDNNNNNMNDDDNDNNDD88DNNNNMMMMMMO7I?++IZZZZZ$77II7$$O8DDNNMMMMMMMMMMMMMNND8OOOOZZZOOZZZZZZZZZZZZZOOOZZZZZO +NMMNDNNNNNNDNDDDDNDNNNNNNNNDDDDDD88DNNMMMMMMMMNO$II??$$Z$77$777$ZZ8DDDNNMMMMMMMMMMMMMMDD8ZOO8OZZZ$ZZOOOOZZZZOOOOOOZOOZ$Z +NMMNNNNNNNNNNNNNDNNDDDDDD8D88D8D88O8NDDNMMMMMMMMNO7?+===?I777$ZO88DDDNMMMMMMMMMMMMMMMMND8O$ZZZ$$$$$ZOZOOZZZ$$$Z$$ZZ$ZOZZ +MNNMNNMNNNNNNNNNNNNNNNDDD888888888O8NNNMNNNMMNMMMMNOI++?I77$O88DDDDNNMMMMMMMMMMMMMMMMNNNDOZZZZ$$$$$$ZZZZOZ$$ZZOOO$$ZOZZO +NNMMMNMMMDDNNNNNNNNNDNNDDDDD88O8OOZ$8NNNMMMMMMMMMMMMNZZOO88DDDDDDNMMMNNNMMMNMMMMMMMMNMNNDZOZO8ZOOZZZOOZO8O8OOOOO8OZZZZ$$ +MMMNNNMMNNNDNDNNNNNNNDNNDDDDDD88OZZ$OMNMMMMMMMMMMMMMMMMMMMNNNMMMMMNNNNNNNNNMMMMMMMMMMNNNND88O8OOOOOZOOOO8OZO888888OZZZO8 +NMNNNMNMMMNDNNNNNNNND88DOO8OOZOOZZZ$ZDNMMMMMMMMMMMMMMMMNNMMMMMMNNNNNNDNDDNNMMMNMMMMMMNMMND88OOZZZZZ$OOO88OO8888O8O888OOO +MMMMMMMNMNNNNDDDDDDD8888D8OZZZZZZOZZO8NMMMMMMMMMMMMMMMN$ZO88DNNNDDD88OOO8DNNMMNMMMMNNNNNN8O8O8OOOO8OOOO8OZZZO88OOZOZZZO8 +MMMMMMMMMMNDNNNDD888OOO8OZ8OOOZOZZZZOOONMMNNMMMMMMMMMMMZI7$$OOO888OOZZZZZ8DNMNNNMMMMNNNMMDD8Z$77II7$77$$$$$$$77$7I??7$ZZ +NNMMMMMMMNDDND88D888O8OO8O88OOOOOOOO888NMMMNMMMMMMMMMMMD7II77$ZZZZZZ$$77$Z8NNDNMNNMNDNNMMMN8DD8O8O8OZZ7I77Z$77777$$ZOZ7$ +NNNNNNNNNNNND88DNNDDNNNNNOOOOOOOZOZONDDMMMMMMMMMMMMMMMN8$7III777$$777I7I7$Z8NNNNNNNN88MMMMMNMND888DNZOO8DNZ77II777ZZ$7$O +NNNNNNDNDDD88888D8DNNNNNMDD8O8DD8DNDNNMMMMMMMMMMMMMMMDOZ$7II?I7777IIIII?7$$ODNDDDDDN88MNNMMNNND8OO8NODD8DN8O7II77$Z$$7$O +NNNNNNNNDDDDD888D8NDDDO8NMNMNNNMNNMMNNNMMMMMMMMMMMNDO7I?I????????I?++????I7Z8DDNN88N8DMNMNNMNDDNNNND88ZZOOZZ7II?IZ$ZZ7$$ +NNNNNNNNNDDDD8DDNNDDDND8NMMMMMMNMNNMMMMMMMMNMMMNNZ$$7???++?+????+++++++++?I$O8NDDZ8D8DNNNNMMNDD8OZZZ7III???III7ZZ$ZOZZZ8 +MMNDDNMMNNND8DNDNNNDD8DDNMNDNMMMMNMMMMMMMMMMMM8$7I???=++=++++++++==+==+++I7ZDDDDOZ8D8NNNNNNNNMND8888O7+?I?7$$77$$$$ZOOOO +NMMNNNNMNNNDDDNDDDD8888NMMNDNMMMMMMMMMMMMMMMMMZIII++++++==+=+++++==+++????7Z888DOZ8DDNNNNNNNNMNND88D8$?IIII777$$Z$$$Z$$$ +NNNNNNMNNDDDNDDD8O8O8DDNMNNNNMMMMNNNMMMMMMMNNN7I?+++========++========++??I$8OO88O8DNMNNNNMDDNNMNDDDOO$I$7I??II??I777777 +NNNMNNNND8DDD8DDO8D8DNDNDDNNDDNMNNNMMMMMMMNNNOI+++++=~~=====+====~=====+?I$$8DO88O8NNMNNNDD88DDDNNNN8OO8Z$77Z7?7O777$O$$ +NDNNNDNNND8888O8OO8O8DDDNNDDDNNDNNMMMMMMNDDDO$?+==============~====+++++?I$8O888888MMMNN8ZZZ88DDNMMNND8Z$$IIIIII?IIII??? +NNNMNDMMNM8Z8$ZOOO8OODDDDD888DNNMMMMMMMM8OZZO7++======~~~=======~~==++?+?I$ZOO8D88DND888OZOZ7ZZZO8DNMMMND88888Z77777I?I? +NNNNNDMMNMDDN$ZOZZOOZO88DN888DMMMMMMMMMMOZZ$$?=+===========~=====~===+++?I$ZOO8DDD8OOZ$$$7$$$8DDDDNMNNMMMD8O88OZ$$$OO77$ +NNNNNNMMNNNDNMN8OZZO88O8D88DNMMMMMMMMMMNZ7II?+==========~======~=~===++??7ZOO888O$7$$$$I?IIII$O8DNMMMMMMMMNDD8ODD88888DO +DDNNDDNDDNNNNNNDNDOOOOOOO88MMMMMMMMMMMN8$III+===~=========~~=~~~==~==++?I$$ZZZZOOOZ7I?7$O888DNNNNNNMMMMMMMMNNDD88DNNDO$I +NNNNDDDNNNDNNNNNN88DDD8OZ$MMMMMMMMMMMMND7?++=======+===~~~~~~~~=~~~~~=+??7$$ZOO$7$ZO8DDNNNMMMMMMMMMMMMMMMMMMMD88DDD8888D +DNNNNNNNDDDDDNDDDDD888OZ$8MMMMMMMMMMMNDD87?++=~~=++++=~~~~~~~:~~~~~~===+?7ZZ$ZZODNNMNNDDNNNNMMMMMMMMMMMMMMMMMNMD8888DND8 +DNNNNNNNNND88D8888OZ$ZZZ8NMMMMMMMMMMNNDDDO$I?+===+?++==~~~~~~~~~~~~====?7$ZOO8DDNNNNDDMMMMMMMMMMMMMMMMMMMMMNNNMN888888D8 +NNDDND888OOO888OOOOO88DMMMMMMMMMMMMMNDD8DOOOD87I?I?+=~~~~~~=~~~~~===+I$ZOO8DNNMNNNNMMMMMMMMNMMMMMMMMMMMMMMMNMNMNDOOZOZO8 +NDND888O8888DO888DDDDMMMMMMMMMMMMMMNND88D8DD8888DDD8O7III?++=+==++I$$Z88ODMMNNNMMMMNNMNDNNNNMMMMMMMMMNMMMMMMMNNMMOOOZZ88 +NDDD8D8O8DDDD88888D8MMMMMMMMMMMMMMNDDDDDNNNDD8D8888OOOOOOOOO8OO8OZO8OODMD8NMMMMMMMMNNN8NNNNNMMMMMMMMMMMMMMMMMMMNMDDNDNDD +NNDDDDDD8888OOZOOZZZMMMMMMMMMMMMMNDNDNNDMMD8DD8O8OO8D888888D888888ODMMNDNMNMMMMMMNNNN8NMNNNNMMMMMMMMMMMMMMMMMMMMNNDDDDD8 +MMNDDD888888O8OOOZ$$MMMMMMMMMMMMMNDDNNNDMMDDDNDD8OOO88888D8DDD8DD8DNNNNMMMMMMMMMNNNMD8NMDNNNMMNNDNNNMMMMMMMMMMMMNM8O88D8 +MMMNNNDDD8888888DDDDMMMMMMMMMMMMMNNNMNDNMNNMNMD8D88DDD88DND8NNNDDNDNMMMMMMMMMMMMNNNNDNNDDNN8OODNDDNDNNMMNMMMMNNNMMNOOO88 +MMMNNNNNNDNDDDDDD8DDMMMMMMMMMMMMMNNMMNMMMNMMMNDDDD8DD88NNMNMD8NNDNMMMMMMMMMMMMNNNMNNNMNND88O888NNNNNDNNMMMMMMMNMNNMDOZZZ +MMMMMNNNNNNNNDDDDDDDMMMMMMMMMMMMMMMMMMMMNMMMMMNNNNNMNNNNMMMNNMNNMMMMMMMMMMMMMMMMMMMMMMDNND8O8D8N88DNNMMMMMMMNNNMMMMMNNND +MMMMNNNDDNDNNDDDNNNMMMMMMMMMMMMMMMMMMMMMNMMMMNNNMNNMNNNNMNNNNNMNNMMMMMMMMMMMMMMMMMMMMNNNDDO888O888DNMMMMMMMMNNNNNMNMMMNN +MMMNDD8D8888OOOO8DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMNMMMMMNNNMNNNMMMMNMMMMMMMMMMMMMMMMNDND888888DNDNMMMNMMMMMMMNNNNNNMMD88 +MMD8888OOOO88OO88DNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNMMMMMMMMMMMMMMMMMMMMMMNDDDDD8ODDDNMMMMMMMMMMMMMNNNDNMMMMN88 +MD8D888888OOOOOO8NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNMMMMMMMMMMMMMMMMMMMMMMNNNDNND888DMMMNMMMMMMMMMMMMNNNNNNNNMDO +88DDD88888OOO888MMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMNNMMMMMMMMMMMMMMMMMMMMNMNNNNDDNNDNNNMMNMMMMMMMMMMMMMMNNNNNMNNMN8 +DDDDDDDDDD888DDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMNNNNDNNNNNMMMMMMMMMMMMMMMMMMMNNNNNNNMMNN +NMMMMMMNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMNMMMMMMMMMMMMNNNNMMMNNNN +MMMNNMNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNMMNMMMMMMMMMMMMMMMMMMMMMMMNNNMMMMMMN +NMMMMMNNNNMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMNNMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNMMMN +NMMMNNMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMNNNMNNNMMMNNNM +NNMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMNMMMMMMMMMMMMMMMNNMMNNMMMNNNN +NMMMMMMMMMMMMMMMMMMMMMMMNNNMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMNMMMMMMNNN +NMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMNMMNMMMMNN +NMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNMMMMMMMMMMMMMMMNNMNN +NMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMNN +NMMMMMMMMMMMMMMNNMMMNDDDNMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMNMNNMNM +MMMMMMMMMMMMMNNND88NMMMMNDDMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNNMMMMNN +NMMMMMMMMMMNNNNNDDMMDDDNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMNMMNNNMNNNNNNNNN +MMMMMMMMMMMMMMNNNMNODNND88NODD8DMNMND8OODDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNNNNMNNN +NMMMMMMMMMMMMMDDNDOOMMND8N8DDODMN8O$III77$Z8DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNMNNNNNNMM +NMMMMMMMMMMMMN8NDNNNNDONMNNO8MDZ8IIIIIII77$$$7ZODNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMNNMNNN +MMMMMMMMMMMMMNMDDMNMODMM8DNMMNOD$777IIII777777$$$$77$DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNMMNNMNN +NMMMMMMMMMMMMNDNMNN8MMMNMNMNMD88$$777777777777777$$77$$8NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNMMMNNNNN +NMMMMMMMMMMMMMMMMMDNNMMNMDMNMNNDZ$$$$$$$$7777777777$$$$$ZZZZNMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMNNNMNNNNNNN +NMMMMMMMMMMMMMMMMNNNMMNNNDMMNNM8ZZZZZ$$Z$$777777$$77$$$$77$$Z8NMMMMMNMMMMMMMMMMMMMMMMMMMMMMNMMNMMMMMMMMNMMMMNNNNNNNNMMNN +NNNMMMMMMMMMMMMMMMMMMMNMMMMMNNMND8888OOZZ$$$$$$7777777777777$$ZO88MNMNMMMMMMMMNNNDD8O8NNMMNNNMMMNMMMMMMNNMNNMNNNNNNNNNNN +NMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMDDNDD88ZZZ$$$$$7$$$$77$ZZ$7III$8NDNNMN8ZO8O88NMND888NNND8O88NNMMMMMMMMNMMNNNNNNNNNNNNNN +NNNNMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMNN8ZZZZZ$$$$Z$$$$$ZODD8777$7$D88D8OZZO8DD8O88DND88DNNNNNNNMMMMMMMMMMMNMMNMNNNMMMNNN +NMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMD888OOZZZZZ$$ZZZZ$$ZZ8NNOZ$$$$DDDDDNDOODDDDDDDDDDD8DNNDNNNNNMMMMMMMMMMMMMNNNNNNNMMNN`) + } +} diff --git a/fixtures/plugins/empty_plugin.go b/fixtures/plugins/empty_plugin.go new file mode 100644 index 00000000000..536639ea2b0 --- /dev/null +++ b/fixtures/plugins/empty_plugin.go @@ -0,0 +1,18 @@ +package main + +import "github.com/cloudfoundry/cli/plugin" + +type EmptyPlugin struct{} + +func (c *EmptyPlugin) Run(cliConnection plugin.CliConnection, args []string) {} + +func (c *EmptyPlugin) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "EmptyPlugin", + Commands: []plugin.Command{}, + } +} + +func main() { + plugin.Start(new(EmptyPlugin)) +} diff --git a/fixtures/plugins/input.go b/fixtures/plugins/input.go new file mode 100644 index 00000000000..65a29e6b0c3 --- /dev/null +++ b/fixtures/plugins/input.go @@ -0,0 +1,42 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type Input struct { +} + +func (c *Input) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "input" { + var Echo string + fmt.Scanf("%s", &Echo) + + fmt.Println("THE WORD IS: ", Echo) + } +} + +func (c *Input) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Input", + Commands: []plugin.Command{ + { + Name: "input", + HelpText: "help text for input", + }, + }, + } +} + +func main() { + plugin.Start(new(Input)) +} diff --git a/fixtures/plugins/my_say.go b/fixtures/plugins/my_say.go new file mode 100644 index 00000000000..aff67a97f69 --- /dev/null +++ b/fixtures/plugins/my_say.go @@ -0,0 +1,44 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + "strings" + + "github.com/cloudfoundry/cli/plugin" +) + +type MySay struct { +} + +func (c *MySay) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "my-say" { + if len(args) == 3 && args[2] == "--loud" { + fmt.Println(strings.ToUpper(args[1])) + } + + fmt.Println(args[1]) + } +} + +func (c *MySay) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "MySay", + Commands: []plugin.Command{ + { + Name: "my-say", + HelpText: "Plugin to say things from the cli", + }, + }, + } +} + +func main() { + plugin.Start(new(MySay)) +} diff --git a/fixtures/plugins/panics.go b/fixtures/plugins/panics.go new file mode 100644 index 00000000000..8242fc82aac --- /dev/null +++ b/fixtures/plugins/panics.go @@ -0,0 +1,45 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "os" + + "github.com/cloudfoundry/cli/plugin" +) + +type Panics struct { +} + +func (c *Panics) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "panic" { + panic("OMG") + } else if args[0] == "exit1" { + os.Exit(1) + } +} + +func (c *Panics) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Panics", + Commands: []plugin.Command{ + { + Name: "panic", + HelpText: "omg panic", + }, + { + Name: "exit1", + HelpText: "omg exit1", + }, + }, + } +} + +func main() { + plugin.Start(new(Panics)) +} diff --git a/fixtures/plugins/test_1.go b/fixtures/plugins/test_1.go new file mode 100644 index 00000000000..835a3daa9b4 --- /dev/null +++ b/fixtures/plugins/test_1.go @@ -0,0 +1,53 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type Test1 struct { +} + +func (c *Test1) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "test_1_cmd1" { + theFirstCmd() + } else if args[0] == "test_1_cmd2" { + theSecondCmd() + } +} + +func (c *Test1) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Test1", + Commands: []plugin.Command{ + { + Name: "test_1_cmd1", + HelpText: "help text for test_1_cmd1", + }, + { + Name: "test_1_cmd2", + HelpText: "help text for test_1_cmd2", + }, + }, + } +} + +func theFirstCmd() { + fmt.Println("You called cmd1 in test_1") +} + +func theSecondCmd() { + fmt.Println("You called cmd2 in test_1") +} + +func main() { + plugin.Start(new(Test1)) +} diff --git a/fixtures/plugins/test_2.go b/fixtures/plugins/test_2.go new file mode 100644 index 00000000000..8228fed132b --- /dev/null +++ b/fixtures/plugins/test_2.go @@ -0,0 +1,52 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type Test2 struct{} + +func (c *Test2) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "test_2_cmd1" { + theFirstCmd() + } else if args[0] == "test_2_cmd2" { + theSecondCmd() + } +} + +func (c *Test2) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Test2", + Commands: []plugin.Command{ + { + Name: "test_2_cmd1", + HelpText: "help text for test_2_cmd1", + }, + { + Name: "test_2_cmd2", + HelpText: "help text for test_2_cmd2", + }, + }, + } +} + +func theFirstCmd() { + fmt.Println("You called cmd1 in test_2") +} + +func theSecondCmd() { + fmt.Println("You called cmd2 in test_2") +} + +func main() { + plugin.Start(new(Test2)) +} diff --git a/fixtures/plugins/test_with_help.go b/fixtures/plugins/test_with_help.go new file mode 100644 index 00000000000..4dc5d8c2e0f --- /dev/null +++ b/fixtures/plugins/test_with_help.go @@ -0,0 +1,43 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type TestWithHelp struct { +} + +func (c *TestWithHelp) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "help" { + theHelpCmd() + } +} + +func (c *TestWithHelp) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "TestWithHelp", + Commands: []plugin.Command{ + { + Name: "help", + HelpText: "help text for test_with_help", + }, + }, + } +} + +func theHelpCmd() { + fmt.Println("You called help in test_with_help") +} + +func main() { + plugin.Start(new(TestWithHelp)) +} diff --git a/fixtures/plugins/test_with_push.go b/fixtures/plugins/test_with_push.go new file mode 100644 index 00000000000..b8c7eeac0b7 --- /dev/null +++ b/fixtures/plugins/test_with_push.go @@ -0,0 +1,43 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type TestWithPush struct { +} + +func (c *TestWithPush) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "push" { + thePushCmd() + } +} + +func (c *TestWithPush) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "TestWithPush", + Commands: []plugin.Command{ + { + Name: "push", + HelpText: "push text for test_with_push", + }, + }, + } +} + +func thePushCmd() { + fmt.Println("You called push in test_with_push") +} + +func main() { + plugin.Start(new(TestWithPush)) +} diff --git a/fixtures/plugins/test_with_push_short_name.go b/fixtures/plugins/test_with_push_short_name.go new file mode 100644 index 00000000000..9f9a1c4ba5c --- /dev/null +++ b/fixtures/plugins/test_with_push_short_name.go @@ -0,0 +1,43 @@ +/** + * 1. Setup the server so cf can call it under main. + e.g. `cf my-plugin` creates the callable server. now we can call the Run command + * 2. Implement Run that is the actual code of the plugin! + * 3. Return an error +**/ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type TestWithPushShortName struct { +} + +func (c *TestWithPushShortName) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "p" { + thePushCmd() + } +} + +func (c *TestWithPushShortName) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "TestWithPushShortName", + Commands: []plugin.Command{ + { + Name: "p", + HelpText: "plugin short name p", + }, + }, + } +} + +func thePushCmd() { + fmt.Println("You called p within the plugin") +} + +func main() { + plugin.Start(new(TestWithPushShortName)) +} diff --git a/fixtures/test.file b/fixtures/test.file new file mode 100644 index 00000000000..52972ec9e05 Binary files /dev/null and b/fixtures/test.file differ diff --git a/fixtures/zip/.cfignore b/fixtures/zip/.cfignore new file mode 100644 index 00000000000..d00ca27ff03 --- /dev/null +++ b/fixtures/zip/.cfignore @@ -0,0 +1,6 @@ +*.log +/someDir/baz.txt +ignoredDir +/lastDir/* +/fooDir/**/baz.txt +/otherDir/ \ No newline at end of file diff --git a/src/fixtures/zip/ignoredDir/foo.txt b/fixtures/zip/.svn/foo.txt similarity index 100% rename from src/fixtures/zip/ignoredDir/foo.txt rename to fixtures/zip/.svn/foo.txt diff --git a/src/fixtures/zip/lastDir/foo.txt b/fixtures/zip/_darcs/foo.txt similarity index 100% rename from src/fixtures/zip/lastDir/foo.txt rename to fixtures/zip/_darcs/foo.txt diff --git a/src/fixtures/zip/foo.txt b/fixtures/zip/foo.txt similarity index 100% rename from src/fixtures/zip/foo.txt rename to fixtures/zip/foo.txt diff --git a/src/fixtures/zip/fooDir/bar/baz.txt b/fixtures/zip/fooDir/bar/baz.txt similarity index 100% rename from src/fixtures/zip/fooDir/bar/baz.txt rename to fixtures/zip/fooDir/bar/baz.txt diff --git a/src/fixtures/zip/ignoredDir/bar.txt b/fixtures/zip/ignoredDir/bar.txt similarity index 100% rename from src/fixtures/zip/ignoredDir/bar.txt rename to fixtures/zip/ignoredDir/bar.txt diff --git a/src/fixtures/zip/otherDir/ignoredDir/foo.txt b/fixtures/zip/ignoredDir/foo.txt similarity index 100% rename from src/fixtures/zip/otherDir/ignoredDir/foo.txt rename to fixtures/zip/ignoredDir/foo.txt diff --git a/src/fixtures/zip/otherDir/dev.log b/fixtures/zip/lastDir/foo.txt similarity index 100% rename from src/fixtures/zip/otherDir/dev.log rename to fixtures/zip/lastDir/foo.txt diff --git a/src/fixtures/zip/someDir/baz.txt b/fixtures/zip/otherDir/ignoredDir/foo.txt similarity index 100% rename from src/fixtures/zip/someDir/baz.txt rename to fixtures/zip/otherDir/ignoredDir/foo.txt diff --git a/src/fixtures/zip/subDir/bar.txt b/fixtures/zip/subDir/bar.txt old mode 100755 new mode 100644 similarity index 100% rename from src/fixtures/zip/subDir/bar.txt rename to fixtures/zip/subDir/bar.txt diff --git a/fixtures/zip/subDir/otherDir/file.txt b/fixtures/zip/subDir/otherDir/file.txt new file mode 100644 index 00000000000..1ac3aeabfdf --- /dev/null +++ b/fixtures/zip/subDir/otherDir/file.txt @@ -0,0 +1 @@ +This file should be present. \ No newline at end of file diff --git a/generic/generic_suite_test.go b/generic/generic_suite_test.go new file mode 100644 index 00000000000..da96edae237 --- /dev/null +++ b/generic/generic_suite_test.go @@ -0,0 +1,13 @@ +package generic_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGeneric(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Generic Suite") +} diff --git a/generic/map.go b/generic/map.go new file mode 100644 index 00000000000..9fb4f00dfed --- /dev/null +++ b/generic/map.go @@ -0,0 +1,154 @@ +package generic + +import "fmt" + +// interface declaration +type Map interface { + IsEmpty() bool + Count() int + Keys() []interface{} + Has(key interface{}) bool + Except(keys []interface{}) Map + IsNil(key interface{}) bool + NotNil(key interface{}) bool + Get(key interface{}) interface{} + Set(key interface{}, value interface{}) + Delete(key interface{}) + String() string +} + +// concrete map type +type ConcreteMap map[interface{}]interface{} + +// constructors +func newEmptyMap() Map { + return &ConcreteMap{} +} + +func NewMap(data ...interface{}) Map { + if len(data) == 0 { + return newEmptyMap() + } else if len(data) > 1 { + panic("NewMap called with more than one argument") + } + + switch data := data[0].(type) { + case Map: + return data + case map[string]string: + stringMap := newEmptyMap() + for key, val := range data { + stringMap.Set(key, val) + } + return stringMap + case map[string]interface{}: + stringToInterfaceMap := newEmptyMap() + for key, val := range data { + stringToInterfaceMap.Set(key, val) + } + return stringToInterfaceMap + case map[interface{}]interface{}: + mapp := ConcreteMap(data) + return &mapp + } + + fmt.Printf("\n\n map: %T", data) + panic("NewMap called with unexpected argument") +} + +// implementing interface methods +func (data *ConcreteMap) IsEmpty() bool { + return data.Count() == 0 +} + +func (data *ConcreteMap) Count() int { + return len(*data) +} + +func (data *ConcreteMap) Has(key interface{}) bool { + _, ok := (*data)[key] + return ok +} + +func (data *ConcreteMap) Except(keys []interface{}) Map { + otherMap := NewMap() + Each(data, func(key, value interface{}) { + if !Contains(keys, key) { + otherMap.Set(key, value) + } + }) + return otherMap +} + +func (data *ConcreteMap) IsNil(key interface{}) bool { + maybe, ok := (*data)[key] + return ok && maybe == nil +} + +func (data *ConcreteMap) NotNil(key interface{}) bool { + maybe, ok := (*data)[key] + return ok && maybe != nil +} + +func (data *ConcreteMap) Keys() (keys []interface{}) { + keys = make([]interface{}, 0, data.Count()) + for key := range *data { + keys = append(keys, key) + } + + return +} + +func (data *ConcreteMap) Get(key interface{}) interface{} { + return (*data)[key] +} + +func (data *ConcreteMap) Set(key, value interface{}) { + (*data)[key] = value +} + +func (data *ConcreteMap) Delete(key interface{}) { + delete(*data, key) +} + +func (data *ConcreteMap) String() string { + return fmt.Sprintf("% v", *data) +} + +// helper functions +func IsMappable(value interface{}) bool { + switch value.(type) { + case Map: + return true + case map[string]interface{}: + return true + case map[interface{}]interface{}: + return true + default: + return false + } +} + +type Iterator func(key, val interface{}) + +func Each(collection Map, cb Iterator) { + for _, key := range collection.Keys() { + cb(key, collection.Get(key)) + } +} + +func Contains(collection, item interface{}) bool { + switch collection := collection.(type) { + case Map: + return collection.Has(item) + case []interface{}: + for _, val := range collection { + if val == item { + return true + } + } + return false + } + + panic("unexpected type passed to Contains") +} diff --git a/generic/map_test.go b/generic/map_test.go new file mode 100644 index 00000000000..bfa83cdb457 --- /dev/null +++ b/generic/map_test.go @@ -0,0 +1,51 @@ +package generic_test + +import ( + . "github.com/cloudfoundry/cli/generic" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func init() { + Describe("generic maps", func() { + It("deep merges, with the last map taking precedence in conflicts", func() { + map1 := NewMap(map[interface{}]interface{}{ + "key1": "val1", + "key2": "val2", + "nest1": map[interface{}]interface{}{ + "nestKey1": "nest1Val1", + "nestKey2": "nest1Val2", + }, + "nest2": []interface{}{ + "nest2Val1", + }, + }) + + map2 := NewMap(map[interface{}]interface{}{ + "key1": "newVal1", + "nest1": map[interface{}]interface{}{ + "nestKey1": "newNest1Val1", + }, + "nest2": []interface{}{ + "something", + }, + }) + + expectedMap := NewMap(map[interface{}]interface{}{ + "key1": "newVal1", + "key2": "val2", + "nest1": NewMap(map[interface{}]interface{}{ + "nestKey1": "newNest1Val1", + "nestKey2": "nest1Val2", + }), + "nest2": []interface{}{ + "nest2Val1", + "something", + }, + }) + + mergedMap := DeepMerge(map1, map2) + Expect(mergedMap).To(Equal(expectedMap)) + }) + }) +} diff --git a/generic/merge_reduce.go b/generic/merge_reduce.go new file mode 100644 index 00000000000..e892b647908 --- /dev/null +++ b/generic/merge_reduce.go @@ -0,0 +1,52 @@ +package generic + +func Merge(collection, otherCollection Map) Map { + mergedMap := NewMap() + + iterator := func(key, value interface{}) { + mergedMap.Set(key, value) + } + + Each(collection, iterator) + Each(otherCollection, iterator) + + return mergedMap +} + +func DeepMerge(maps ...Map) Map { + mergedMap := NewMap() + return Reduce(maps, mergedMap, mergeReducer) +} + +func mergeReducer(key, val interface{}, reduced Map) Map { + switch { + case reduced.Has(key) == false: + reduced.Set(key, val) + return reduced + + case IsMappable(val): + maps := []Map{NewMap(reduced.Get(key)), NewMap(val)} + mergedMap := Reduce(maps, NewMap(), mergeReducer) + reduced.Set(key, mergedMap) + return reduced + + case IsSliceable(val): + reduced.Set(key, append(reduced.Get(key).([]interface{}), val.([]interface{})...)) + return reduced + + default: + reduced.Set(key, val) + return reduced + } +} + +type Reducer func(key, val interface{}, reducedVal Map) Map + +func Reduce(collections []Map, resultVal Map, cb Reducer) Map { + for _, collection := range collections { + for _, key := range collection.Keys() { + resultVal = cb(key, collection.Get(key), resultVal) + } + } + return resultVal +} diff --git a/generic/slice.go b/generic/slice.go new file mode 100644 index 00000000000..c142d88733a --- /dev/null +++ b/generic/slice.go @@ -0,0 +1,12 @@ +package generic + +func IsSliceable(value interface{}) bool { + switch value.(type) { + case []string: + return true + case []interface{}: + return true + default: + return false + } +} diff --git a/generic/slice_test.go b/generic/slice_test.go new file mode 100644 index 00000000000..aa184c476e3 --- /dev/null +++ b/generic/slice_test.go @@ -0,0 +1,23 @@ +package generic_test + +import ( + "github.com/cloudfoundry/cli/generic" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func init() { + Describe("IsSliceable", func() { + It("should return false when the type cannot be sliced", func() { + Expect(generic.IsSliceable("bad slicing")).To(BeFalse()) + }) + + It("should return true if the type can be sliced", func() { + Expect(generic.IsSliceable([]string{"a string"})).To(BeTrue()) + }) + + It("should return true if the type can be sliced", func() { + Expect(generic.IsSliceable([]interface{}{1, 2, 3})).To(BeTrue()) + }) + }) +} diff --git a/glob/glob.go b/glob/glob.go new file mode 100644 index 00000000000..659e0af005e --- /dev/null +++ b/glob/glob.go @@ -0,0 +1,108 @@ +package glob + +import ( + "regexp" + "strings" +) + +// Glob holds a Unix-style glob pattern in a compiled form for efficient +// matching against paths. +// +// Glob notation: +// - `?` matches a single char in a single path component +// - `*` matches zero or more chars in a single path component +// - `**` matches zero or more chars in zero or more components +// - any other sequence matches itself +type Glob struct { + pattern string // original glob pattern + regexp *regexp.Regexp // compiled regexp +} + +const charPat = `[^/]` + +func mustBuildRe(p string) *regexp.Regexp { + return regexp.MustCompile(`^/$|^(` + p + `+)?(/` + p + `+)*$`) +} + +var globRe = mustBuildRe(`(` + charPat + `|[\*\?])`) + +// Supports unix/ruby-style glob patterns: +// - `?` matches a single char in a single path component +// - `*` matches zero or more chars in a single path component +// - `**` matches zero or more chars in zero or more components +func translateGlob(pat string) (string, error) { + if !globRe.MatchString(pat) { + return "", GlobError(pat) + } + + outs := make([]string, len(pat)) + i, double := 0, false + for _, c := range pat { + switch c { + default: + outs[i] = string(c) + double = false + case '.', '+', '-', '^', '$', '[', ']', '(', ')': + outs[i] = `\` + string(c) + double = false + case '?': + outs[i] = `[^/]` + double = false + case '*': + if double { + outs[i-1] = `.*` + } else { + outs[i] = `[^/]*` + } + double = !double + } + i++ + } + outs = outs[0:i] + + return "^" + strings.Join(outs, "") + "$", nil +} + +// CompileGlob translates pat into a form more convenient for +// matching against paths in the store. +func CompileGlob(pat string) (glob Glob, err error) { + pat = toSlash(pat) + s, err := translateGlob(pat) + if err != nil { + return + } + r, err := regexp.Compile(s) + if err != nil { + return + } + glob = Glob{pat, r} + return +} + +// MustCompileGlob is like CompileGlob, but it panics if an error occurs, +// simplifying safe initialization of global variables holding glob patterns. +func MustCompileGlob(pat string) Glob { + g, err := CompileGlob(pat) + if err != nil { + panic(err) + } + return g +} + +func (g Glob) String() string { + return g.pattern +} + +func (g Glob) Match(path string) bool { + return g.regexp.MatchString(toSlash(path)) +} + +type GlobError string + +func (e GlobError) Error() string { + return "invalid glob pattern: " + string(e) +} + +func toSlash(path string) string { + return strings.Replace(path, "\\", "/", -1) +} diff --git a/glob/glob_test.go b/glob/glob_test.go new file mode 100644 index 00000000000..57be1584101 --- /dev/null +++ b/glob/glob_test.go @@ -0,0 +1,81 @@ +package glob + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "testing" +) + +var globs = [][]string{ + {"/", `^/$`}, + {"/a", `^/a$`}, + {"/a.b", `^/a\.b$`}, + {"/a-b", `^/a\-b$`}, + {"/a?", `^/a[^/]$`}, + {"/a/b", `^/a/b$`}, + {"/*", `^/[^/]*$`}, + {"/*/a", `^/[^/]*/a$`}, + {"/*a/b", `^/[^/]*a/b$`}, + {"/a*/b", `^/a[^/]*/b$`}, + {"/a*a/b", `^/a[^/]*a/b$`}, + {"/*a*/b", `^/[^/]*a[^/]*/b$`}, + {"/**", `^/.*$`}, + {"/**/a", `^/.*/a$`}, +} + +var matches = [][]string{ + {"/a/b", "/a/b"}, + {"/a?", "/ab", "/ac"}, + {"/a*", "/a", "/ab", "/abc"}, + {"/a**", "/a", "/ab", "/abc", "/a/", "/a/b", "/ab/c"}, + {`c:\a\b\.d`, `c:\a\b\.d`}, + {`c:\**\.d`, `c:\a\b\.d`}, +} + +var nonMatches = [][]string{ + {"/a/b", "/a/c", "/a/", "/a/b/", "/a/bc"}, + {"/a?", "/", "/abc", "/a", "/a/"}, + {"/a*", "/", "/a/", "/ba"}, + {"/a**", "/", "/ba"}, +} + +var _ = Describe("Glob", func() { + It("translates globs to regexes", func() { + for _, parts := range globs { + pat, exp := parts[0], parts[1] + got, err := translateGlob(pat) + + Expect(err).NotTo(HaveOccurred()) + Expect(exp).To(Equal(got), "expected %q, but got %q from %q", exp, got, pat) + } + }) + + It("creates regexes that match correct file paths", func() { + for _, parts := range matches { + pat, paths := parts[0], parts[1:] + glob, err := CompileGlob(pat) + + Expect(err).NotTo(HaveOccurred()) + for _, path := range paths { + Expect(glob.Match(path)).To(BeTrue(), "path %q should match %q", pat, path) + } + } + }) + + It("creates regexes that do not match file incorrect file paths", func() { + for _, parts := range nonMatches { + pat, paths := parts[0], parts[1:] + glob, err := CompileGlob(pat) + + Expect(err).NotTo(HaveOccurred()) + for _, path := range paths { + Expect(glob.Match(path)).To(BeFalse(), "path %q should match %q", pat, path) + } + } + }) +}) + +func TestGlobSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Glob Suite") +} diff --git a/installers/deb/cf/DEBIAN/copyright b/installers/deb/cf/DEBIAN/copyright new file mode 100644 index 00000000000..efed891b294 --- /dev/null +++ b/installers/deb/cf/DEBIAN/copyright @@ -0,0 +1,208 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: CF +Source: https://github.com/cloudfoundry/cli + +Files: * +Copyright: Copyright 2014 Pivotal +License: Apache + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Pivotal + +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. diff --git a/installers/deb/cf/usr/share/doc/cf-cli/copyright b/installers/deb/cf/usr/share/doc/cf-cli/copyright new file mode 100644 index 00000000000..915b208920b --- /dev/null +++ b/installers/deb/cf/usr/share/doc/cf-cli/copyright @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Pivotal + +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. diff --git a/installers/deb/control.template b/installers/deb/control.template new file mode 100644 index 00000000000..67a2a3ef4e1 --- /dev/null +++ b/installers/deb/control.template @@ -0,0 +1,8 @@ +Package: cf-cli +Maintainer: Ted Young +Source: cf-cli +Homepage: https://github.com/cloudfoundry/cli +Section: misc +Priority: optional +Description: Cloud Foundry command line client +Installed-Size: 10480 diff --git a/installers/homebrew/cf-bin.rb b/installers/homebrew/cf-bin.rb new file mode 100644 index 00000000000..121321cf633 --- /dev/null +++ b/installers/homebrew/cf-bin.rb @@ -0,0 +1,17 @@ +require 'formula' + +class CfBin < Formula + homepage 'https://github.com/cloudfoundry/cli' + url 'https://github.com/cloudfoundry/cli/releases/download/v6.0.1/cf-darwin-amd64.tgz' + sha1 '548a83996ade1fb4c4334e4ebcfd558434c01daf' + + def install + system 'curl -O https://raw.github.com/cloudfoundry/cli/v6.0.1/LICENSE' + bin.install 'cf-darwin-amd64' => 'cf' + doc.install 'LICENSE' + end + + test do + system "#{bin}/cf" + end +end diff --git a/installers/homebrew/cf-src.rb b/installers/homebrew/cf-src.rb new file mode 100644 index 00000000000..76d7fe91e0f --- /dev/null +++ b/installers/homebrew/cf-src.rb @@ -0,0 +1,22 @@ +require 'formula' + +class CfSrc < Formula + homepage 'https://github.com/cloudfoundry/cli' + url 'https://github.com/cloudfoundry/cli.git', :tag => 'v6.0.1' + + head 'https://github.com/cloudfoundry/cli.git', :branch => 'master' + + depends_on 'go' => :build + + def install + inreplace 'cf/app_constants.go', 'SHA', 'homebrew' + inreplace 'cf/app_constants.go', 'BUILT_FROM_SOURCE', 'homebrew' + system 'bin/build' + bin.install 'out/cf' + doc.install 'LICENSE' + end + + test do + system "#{bin}/cf" + end +end diff --git a/installers/osx/COPYING b/installers/osx/COPYING new file mode 100644 index 00000000000..915b208920b --- /dev/null +++ b/installers/osx/COPYING @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Pivotal + +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. diff --git a/installers/osx/Distribution b/installers/osx/Distribution new file mode 100644 index 00000000000..19ab4579084 --- /dev/null +++ b/installers/osx/Distribution @@ -0,0 +1,26 @@ + + + + + + + + + Cloud Foundry CLI + + + + + + + + + + + + + + + + #com.cloudfoundry.cli.pkg + diff --git a/installers/osx/com.cloudfoundry.cli.pkg/PackageInfo b/installers/osx/com.cloudfoundry.cli.pkg/PackageInfo new file mode 100644 index 00000000000..22f49ecc566 --- /dev/null +++ b/installers/osx/com.cloudfoundry.cli.pkg/PackageInfo @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/installers/rpm/COPYING b/installers/rpm/COPYING new file mode 100644 index 00000000000..915b208920b --- /dev/null +++ b/installers/rpm/COPYING @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Pivotal + +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. diff --git a/installers/rpm/cf-cli.spec.template b/installers/rpm/cf-cli.spec.template new file mode 100644 index 00000000000..01bd0a3d0e0 --- /dev/null +++ b/installers/rpm/cf-cli.spec.template @@ -0,0 +1,26 @@ +Summary: Cloud Foundry command line client +Name: cf-cli +Release: 1 +Group: Development/Tools +License: Apache +Source: %{expand:%%(pwd)} + +%description +%{summary} + +%prep +rm -rf $RPM_BUILD_ROOT +mkdir -p $RPM_BUILD_ROOT/usr/bin +mkdir -p $RPM_BUILD_ROOT/usr/share/doc/cf-cli +cd $RPM_BUILD_ROOT +cp %{SOURCEURL0}/cf ./usr/bin/cf +cp %{SOURCEURL0}/COPYING ./usr/share/doc/cf-cli/COPYING + +%clean +rm -rf "$RPM_BUILD_ROOT" + +%files +%defattr(644,root,root) +/usr/share/doc/cf-cli/COPYING +%defattr(755,root,root) +/usr/bin/cf diff --git a/installers/windows/env_var_update.nsh b/installers/windows/env_var_update.nsh new file mode 100644 index 00000000000..80c12cd89dd --- /dev/null +++ b/installers/windows/env_var_update.nsh @@ -0,0 +1,327 @@ +/** + * EnvVarUpdate.nsh + * : Environmental Variables: append, prepend, and remove entries + * + * WARNING: If you use StrFunc.nsh header then include it before this file + * with all required definitions. This is to avoid conflicts + * + * Usage: + * ${EnvVarUpdate} "ResultVar" "EnvVarName" "Action" "RegLoc" "PathString" + * + * Credits: + * Version 1.0 + * * Cal Turney (turnec2) + * * Amir Szekely (KiCHiK) and e-circ for developing the forerunners of this + * function: AddToPath, un.RemoveFromPath, AddToEnvVar, un.RemoveFromEnvVar, + * WriteEnvStr, and un.DeleteEnvStr + * * Diego Pedroso (deguix) for StrTok + * * Kevin English (kenglish_hi) for StrContains + * * Hendri Adriaens (Smile2Me), Diego Pedroso (deguix), and Dan Fuhry + * (dandaman32) for StrReplace + * + * Version 1.1 (compatibility with StrFunc.nsh) + * * techtonik + * + * http://nsis.sourceforge.net/Environmental_Variables:_append%2C_prepend%2C_and_remove_entries + * + */ + + +!ifndef ENVVARUPDATE_FUNCTION +!define ENVVARUPDATE_FUNCTION +!verbose push +!verbose 3 +!include "LogicLib.nsh" +!include "WinMessages.NSH" +!include "StrFunc.nsh" + +; ---- Fix for conflict if StrFunc.nsh is already includes in main file ----------------------- +!macro _IncludeStrFunction StrFuncName + !ifndef ${StrFuncName}_INCLUDED + ${${StrFuncName}} + !endif + !ifndef Un${StrFuncName}_INCLUDED + ${Un${StrFuncName}} + !endif + !define un.${StrFuncName} "${Un${StrFuncName}}" +!macroend + +!insertmacro _IncludeStrFunction StrTok +!insertmacro _IncludeStrFunction StrStr +!insertmacro _IncludeStrFunction StrRep + +; ---------------------------------- Macro Definitions ---------------------------------------- +!macro _EnvVarUpdateConstructor ResultVar EnvVarName Action Regloc PathString + Push "${EnvVarName}" + Push "${Action}" + Push "${RegLoc}" + Push "${PathString}" + Call EnvVarUpdate + Pop "${ResultVar}" +!macroend +!define EnvVarUpdate '!insertmacro "_EnvVarUpdateConstructor"' + +!macro _unEnvVarUpdateConstructor ResultVar EnvVarName Action Regloc PathString + Push "${EnvVarName}" + Push "${Action}" + Push "${RegLoc}" + Push "${PathString}" + Call un.EnvVarUpdate + Pop "${ResultVar}" +!macroend +!define un.EnvVarUpdate '!insertmacro "_unEnvVarUpdateConstructor"' +; ---------------------------------- Macro Definitions end------------------------------------- + +;----------------------------------- EnvVarUpdate start---------------------------------------- +!define hklm_all_users 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' +!define hkcu_current_user 'HKCU "Environment"' + +!macro EnvVarUpdate UN + +Function ${UN}EnvVarUpdate + + Push $0 + Exch 4 + Exch $1 + Exch 3 + Exch $2 + Exch 2 + Exch $3 + Exch + Exch $4 + Push $5 + Push $6 + Push $7 + Push $8 + Push $9 + Push $R0 + + /* After this point: + ------------------------- + $0 = ResultVar (returned) + $1 = EnvVarName (input) + $2 = Action (input) + $3 = RegLoc (input) + $4 = PathString (input) + $5 = Orig EnvVar (read from registry) + $6 = Len of $0 (temp) + $7 = tempstr1 (temp) + $8 = Entry counter (temp) + $9 = tempstr2 (temp) + $R0 = tempChar (temp) */ + + ; Step 1: Read contents of EnvVarName from RegLoc + ; + ; Check for empty EnvVarName + ${If} $1 == "" + SetErrors + DetailPrint "ERROR: EnvVarName is blank" + Goto EnvVarUpdate_Restore_Vars + ${EndIf} + + ; Check for valid Action + ${If} $2 != "A" + ${AndIf} $2 != "P" + ${AndIf} $2 != "R" + SetErrors + DetailPrint "ERROR: Invalid Action - must be A, P, or R" + Goto EnvVarUpdate_Restore_Vars + ${EndIf} + + ${If} $3 == HKLM + ReadRegStr $5 ${hklm_all_users} $1 ; Get EnvVarName from all users into $5 + ${ElseIf} $3 == HKCU + ReadRegStr $5 ${hkcu_current_user} $1 ; Read EnvVarName from current user into $5 + ${Else} + SetErrors + DetailPrint 'ERROR: Action is [$3] but must be "HKLM" or HKCU"' + Goto EnvVarUpdate_Restore_Vars + ${EndIf} + + ; Check for empty PathString + ${If} $4 == "" + SetErrors + DetailPrint "ERROR: PathString is blank" + Goto EnvVarUpdate_Restore_Vars + ${EndIf} + + ; Make sure we've got some work to do + ${If} $5 == "" + ${AndIf} $2 == "R" + SetErrors + DetailPrint "$1 is empty - Nothing to remove" + Goto EnvVarUpdate_Restore_Vars + ${EndIf} + + ; Step 2: Scrub EnvVar + ; + StrCpy $0 $5 ; Copy the contents to $0 + ; Remove spaces around semicolons (NOTE: spaces before the 1st entry or + ; after the last one are not removed here but instead in Step 3) + ${If} $0 != "" ; If EnvVar is not empty ... + ${Do} + ${${UN}StrStr} $7 $0 " ;" + ${If} $7 == "" + ${ExitDo} + ${EndIf} + ${${UN}StrRep} $0 $0 " ;" ";" ; Remove ';' + ${Loop} + ${Do} + ${${UN}StrStr} $7 $0 "; " + ${If} $7 == "" + ${ExitDo} + ${EndIf} + ${${UN}StrRep} $0 $0 "; " ";" ; Remove ';' + ${Loop} + ${Do} + ${${UN}StrStr} $7 $0 ";;" + ${If} $7 == "" + ${ExitDo} + ${EndIf} + ${${UN}StrRep} $0 $0 ";;" ";" + ${Loop} + + ; Remove a leading or trailing semicolon from EnvVar + StrCpy $7 $0 1 0 + ${If} $7 == ";" + StrCpy $0 $0 "" 1 ; Change ';' to '' + ${EndIf} + StrLen $6 $0 + IntOp $6 $6 - 1 + StrCpy $7 $0 1 $6 + ${If} $7 == ";" + StrCpy $0 $0 $6 ; Change ';' to '' + ${EndIf} + ; DetailPrint "Scrubbed $1: [$0]" ; Uncomment to debug + ${EndIf} + + /* Step 3. Remove all instances of the target path/string (even if "A" or "P") + $6 = bool flag (1 = found and removed PathString) + $7 = a string (e.g. path) delimited by semicolon(s) + $8 = entry counter starting at 0 + $9 = copy of $0 + $R0 = tempChar */ + + ${If} $5 != "" ; If EnvVar is not empty ... + StrCpy $9 $0 + StrCpy $0 "" + StrCpy $8 0 + StrCpy $6 0 + + ${Do} + ${${UN}StrTok} $7 $9 ";" $8 "0" ; $7 = next entry, $8 = entry counter + + ${If} $7 == "" ; If we've run out of entries, + ${ExitDo} ; were done + ${EndIf} ; + + ; Remove leading and trailing spaces from this entry (critical step for Action=Remove) + ${Do} + StrCpy $R0 $7 1 + ${If} $R0 != " " + ${ExitDo} + ${EndIf} + StrCpy $7 $7 "" 1 ; Remove leading space + ${Loop} + ${Do} + StrCpy $R0 $7 1 -1 + ${If} $R0 != " " + ${ExitDo} + ${EndIf} + StrCpy $7 $7 -1 ; Remove trailing space + ${Loop} + ${If} $7 == $4 ; If string matches, remove it by not appending it + StrCpy $6 1 ; Set 'found' flag + ${ElseIf} $7 != $4 ; If string does NOT match + ${AndIf} $0 == "" ; and the 1st string being added to $0, + StrCpy $0 $7 ; copy it to $0 without a prepended semicolon + ${ElseIf} $7 != $4 ; If string does NOT match + ${AndIf} $0 != "" ; and this is NOT the 1st string to be added to $0, + StrCpy $0 $0;$7 ; append path to $0 with a prepended semicolon + ${EndIf} ; + + IntOp $8 $8 + 1 ; Bump counter + ${Loop} ; Check for duplicates until we run out of paths + ${EndIf} + + ; Step 4: Perform the requested Action + ; + ${If} $2 != "R" ; If Append or Prepend + ${If} $6 == 1 ; And if we found the target + DetailPrint "Target is already present in $1. It will be removed and" + ${EndIf} + ${If} $0 == "" ; If EnvVar is (now) empty + StrCpy $0 $4 ; just copy PathString to EnvVar + ${If} $6 == 0 ; If found flag is either 0 + ${OrIf} $6 == "" ; or blank (if EnvVarName is empty) + DetailPrint "$1 was empty and has been updated with the target" + ${EndIf} + ${ElseIf} $2 == "A" ; If Append (and EnvVar is not empty), + StrCpy $0 $0;$4 ; append PathString + ${If} $6 == 1 + DetailPrint "appended to $1" + ${Else} + DetailPrint "Target was appended to $1" + ${EndIf} + ${Else} ; If Prepend (and EnvVar is not empty), + StrCpy $0 $4;$0 ; prepend PathString + ${If} $6 == 1 + DetailPrint "prepended to $1" + ${Else} + DetailPrint "Target was prepended to $1" + ${EndIf} + ${EndIf} + ${Else} ; If Action = Remove + ${If} $6 == 1 ; and we found the target + DetailPrint "Target was found and removed from $1" + ${Else} + DetailPrint "Target was NOT found in $1 (nothing to remove)" + ${EndIf} + ${If} $0 == "" + DetailPrint "$1 is now empty" + ${EndIf} + ${EndIf} + + ; Step 5: Update the registry at RegLoc with the updated EnvVar and announce the change + ; + ClearErrors + ${If} $3 == HKLM + WriteRegExpandStr ${hklm_all_users} $1 $0 ; Write it in all users section + ${ElseIf} $3 == HKCU + WriteRegExpandStr ${hkcu_current_user} $1 $0 ; Write it to current user section + ${EndIf} + + IfErrors 0 +4 + MessageBox MB_OK|MB_ICONEXCLAMATION "Could not write updated $1 to $3" + DetailPrint "Could not write updated $1 to $3" + Goto EnvVarUpdate_Restore_Vars + + ; "Export" our change + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + EnvVarUpdate_Restore_Vars: + ; + ; Restore the user's variables and return ResultVar + Pop $R0 + Pop $9 + Pop $8 + Pop $7 + Pop $6 + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Push $0 ; Push my $0 (ResultVar) + Exch + Pop $0 ; Restore his $0 + +FunctionEnd + +!macroend ; EnvVarUpdate UN +!insertmacro EnvVarUpdate "" +!insertmacro EnvVarUpdate "un." +;----------------------------------- EnvVarUpdate end---------------------------------------- + +!verbose pop +!endif \ No newline at end of file diff --git a/installers/windows/install.nsi b/installers/windows/install.nsi new file mode 100644 index 00000000000..6f6d1a77a89 --- /dev/null +++ b/installers/windows/install.nsi @@ -0,0 +1,31 @@ +!include "env_var_update.nsh" + +Name "CloudFoundry CLI" +OutFile "cf_installer.exe" + +InstallDir $PROGRAMFILES\CloudFoundry +InstallDirRegKey HKLM "Software\CloudFoundryCLI" "Install_Dir" + +RequestExecutionLevel admin + +Page directory +Page instfiles + +; The stuff to install +Section "CloudFoundry CLI (required)" + + SectionIn RO + + ; Set output path to the installation directory. + SetOutPath $INSTDIR + + ; Put file there + File "cf.exe" + + ; Write the installation path into the registry + WriteRegStr HKLM Software\CloudFoundryCLI "Install_Dir" "$INSTDIR" + + ; Add output directory to system path + ${EnvVarUpdate} $0 "PATH" "A" "HKLM" "$INSTDIR" + +SectionEnd diff --git a/json/json_parser.go b/json/json_parser.go new file mode 100644 index 00000000000..42773aa3e9c --- /dev/null +++ b/json/json_parser.go @@ -0,0 +1,33 @@ +package json + +import ( + "encoding/json" + "io/ioutil" + "os" + + "github.com/cloudfoundry/cli/cf/errors" +) + +func ParseJSON(path string) ([]map[string]interface{}, error) { + if path == "" { + return nil, nil + } + + file, err := os.Open(path) + if err != nil { + return nil, err + } + + bytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + stringMaps := []map[string]interface{}{} + err = json.Unmarshal(bytes, &stringMaps) + if err != nil { + return nil, errors.NewWithFmt("Incorrect json format: %s", err.Error()) + } + + return stringMaps, nil +} diff --git a/json/json_parser_test.go b/json/json_parser_test.go new file mode 100644 index 00000000000..a3ca5dfefd3 --- /dev/null +++ b/json/json_parser_test.go @@ -0,0 +1,52 @@ +package json_test + +import ( + "io/ioutil" + "os" + + "github.com/cloudfoundry/cli/json" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ParseJson", func() { + var filename string + var tmpFile *os.File + + Context("when everything is proper", func() { + BeforeEach(func() { + tmpFile, _ = ioutil.TempFile("", "WONDERFULFILEWHOSENAMEISHARDTOREADBUTCONTAINSVALIDJSON") + filename = tmpFile.Name() + ioutil.WriteFile(filename, []byte("[{\"akey\": \"avalue\"}]"), 0644) + }) + + AfterEach(func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }) + + It("converts a json file into an unmarshalled slice of string->string map objects", func() { + stringMaps, err := json.ParseJSON(filename) + Expect(err).To(BeNil()) + Expect(stringMaps[0]["akey"]).To(Equal("avalue")) + }) + }) + + Context("when the JSON is invalid", func() { + BeforeEach(func() { + tmpFile, _ = ioutil.TempFile("", "TERRIBLEFILECONTAININGINVALIDJSONWHICHMAKESEVERYTHINGTERRIBLEANDSTILLHASANAMETHATSHARDTOREAD") + filename = tmpFile.Name() + ioutil.WriteFile(filename, []byte("SCARY NOISES}"), 0644) + }) + + AfterEach(func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }) + + It("tries to convert the json file but fails because it was given something it didn't like", func() { + _, err := json.ParseJSON(filename) + Expect(err).ToNot(BeNil()) + }) + }) +}) diff --git a/json/json_suite_test.go b/json/json_suite_test.go new file mode 100644 index 00000000000..fa471d35d31 --- /dev/null +++ b/json/json_suite_test.go @@ -0,0 +1,13 @@ +package json_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestJson(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Json Suite") +} diff --git a/main/locales_test.go b/main/locales_test.go new file mode 100644 index 00000000000..2613b82d4de --- /dev/null +++ b/main/locales_test.go @@ -0,0 +1,33 @@ +package main_test + +import ( + "os" + "time" + + "github.com/cloudfoundry/cli/cf/i18n" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("locales", func() { + var oldLocale string + + BeforeEach(func() { + oldLocale = os.Getenv("LANG") + }) + + AfterEach(func() { + os.Setenv("LANG", oldLocale) + }) + + It("exits 0 when help is run for each language", func() { + for _, locale := range i18n.SUPPORTED_LOCALES { + os.Setenv("LANG", locale) + result := Cf("help") + + Eventually(result, 5*time.Second).Should(Exit(0)) + } + }) +}) diff --git a/main/main.go b/main/main.go new file mode 100644 index 00000000000..0f0731392fa --- /dev/null +++ b/main/main.go @@ -0,0 +1,250 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "strings" + "time" + + "github.com/cloudfoundry/cli/cf/api" + "github.com/cloudfoundry/cli/cf/app" + "github.com/cloudfoundry/cli/cf/command_factory" + "github.com/cloudfoundry/cli/cf/command_metadata" + "github.com/cloudfoundry/cli/cf/command_runner" + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + "github.com/cloudfoundry/cli/cf/flag_helpers" + . "github.com/cloudfoundry/cli/cf/i18n" + "github.com/cloudfoundry/cli/cf/i18n/detection" + "github.com/cloudfoundry/cli/cf/manifest" + "github.com/cloudfoundry/cli/cf/net" + "github.com/cloudfoundry/cli/cf/panic_printer" + "github.com/cloudfoundry/cli/cf/requirements" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/cf/trace" + "github.com/cloudfoundry/cli/plugin/rpc" + "github.com/codegangsta/cli" +) + +var deps = setupDependencies() + +type cliDependencies struct { + termUI terminal.UI + configRepo core_config.Repository + pluginConfig plugin_config.PluginConfiguration + manifestRepo manifest.ManifestRepository + apiRepoLocator api.RepositoryLocator + gateways map[string]net.Gateway + teePrinter *terminal.TeePrinter + detector detection.Detector +} + +func setupDependencies() (deps *cliDependencies) { + deps = new(cliDependencies) + + deps.teePrinter = terminal.NewTeePrinter() + + deps.termUI = terminal.NewUI(os.Stdin, deps.teePrinter) + + deps.manifestRepo = manifest.NewManifestDiskRepository() + + errorHandler := func(err error) { + if err != nil { + deps.termUI.Failed(fmt.Sprintf("Config error: %s", err)) + } + } + deps.configRepo = core_config.NewRepositoryFromFilepath(config_helpers.DefaultFilePath(), errorHandler) + deps.pluginConfig = plugin_config.NewPluginConfig(errorHandler) + deps.detector = &detection.JibberJabberDetector{} + + T = Init(deps.configRepo, deps.detector) + + terminal.UserAskedForColors = deps.configRepo.ColorEnabled() + terminal.InitColorSupport() + + if os.Getenv("CF_TRACE") != "" { + trace.Logger = trace.NewLogger(os.Getenv("CF_TRACE")) + } else { + trace.Logger = trace.NewLogger(deps.configRepo.Trace()) + } + + deps.gateways = map[string]net.Gateway{ + "auth": net.NewUAAGateway(deps.configRepo, deps.termUI), + "cloud-controller": net.NewCloudControllerGateway(deps.configRepo, time.Now, deps.termUI), + "uaa": net.NewUAAGateway(deps.configRepo, deps.termUI), + } + deps.apiRepoLocator = api.NewRepositoryLocator(deps.configRepo, deps.gateways) + + return +} + +func main() { + defer handlePanics(deps.teePrinter) + defer deps.configRepo.Close() + + cmdFactory := command_factory.NewFactory(deps.termUI, deps.configRepo, deps.manifestRepo, deps.apiRepoLocator, deps.pluginConfig) + requirementsFactory := requirements.NewFactory(deps.termUI, deps.configRepo, deps.apiRepoLocator) + cmdRunner := command_runner.NewRunner(cmdFactory, requirementsFactory, deps.termUI) + + var badFlags string + metaDatas := cmdFactory.CommandMetadatas() + + if len(os.Args) > 1 { + flags := getCommandFlags(os.Args, metaDatas) + + badFlags = matchArgAndFlags(flags, os.Args[2:]) + + if badFlags != "" { + badFlags = badFlags + "\n\n" + } + } + + injectHelpTemplate(badFlags) + + theApp := app.NewApp(cmdRunner, metaDatas...) + //command `cf` without argument + if len(os.Args) == 1 || os.Args[1] == "help" { + theApp.Run(os.Args) + } else if cmdFactory.CheckIfCoreCmdExists(os.Args[1]) { + callCoreCommand(os.Args[0:], theApp) + } else { + // run each plugin and find the method/ + // run method if exist + ran := rpc.RunMethodIfExists(theApp, os.Args[1:], deps.teePrinter, deps.teePrinter) + if !ran { + theApp.Run(os.Args) + } + } +} + +func gatewaySliceFromMap(gateway_map map[string]net.Gateway) []net.WarningProducer { + gateways := []net.WarningProducer{} + for _, gateway := range gateway_map { + gateways = append(gateways, gateway) + } + return gateways +} + +func injectHelpTemplate(badFlags string) { + cli.CommandHelpTemplate = fmt.Sprintf(`%sNAME: + {{.Name}} - {{.Description}} +{{with .ShortName}} +ALIAS: + {{.}} +{{end}} +USAGE: + {{.Usage}}{{with .Flags}} + +OPTIONS: +{{range .}} {{.}} +{{end}}{{else}} +{{end}}`, badFlags) +} + +func handlePanics(printer terminal.Printer) { + panic_printer.UI = terminal.NewUI(os.Stdin, printer) + + commandArgs := strings.Join(os.Args, " ") + stackTrace := generateBacktrace() + + err := recover() + panic_printer.DisplayCrashDialog(err, commandArgs, stackTrace) + + if err != nil { + os.Exit(1) + } +} + +func generateBacktrace() string { + stackByteCount := 0 + STACK_SIZE_LIMIT := 1024 * 1024 + var bytes []byte + for stackSize := 1024; (stackByteCount == 0 || stackByteCount == stackSize) && stackSize < STACK_SIZE_LIMIT; stackSize = 2 * stackSize { + bytes = make([]byte, stackSize) + stackByteCount = runtime.Stack(bytes, true) + } + stackTrace := "\t" + strings.Replace(string(bytes), "\n", "\n\t", -1) + return stackTrace +} + +func callCoreCommand(args []string, theApp *cli.App) { + err := theApp.Run(args) + if err != nil { + os.Exit(1) + } + gateways := gatewaySliceFromMap(deps.gateways) + + warningsCollector := net.NewWarningsCollector(deps.termUI, gateways...) + warningsCollector.PrintWarnings() +} + +func getCommandFlags(args []string, metaDatas []command_metadata.CommandMetadata) []string { + var flags []string + for _, cmd := range metaDatas { + if args[1] == cmd.Name || args[1] == cmd.ShortName { + for _, flag := range cmd.Flags { + switch t := flag.(type) { + default: + case flag_helpers.StringSliceFlagWithNoDefault: + flags = append(flags, t.Name) + case flag_helpers.IntFlagWithNoDefault: + flags = append(flags, t.Name) + case flag_helpers.StringFlagWithNoDefault: + flags = append(flags, t.Name) + case cli.BoolFlag: + flags = append(flags, t.Name) + } + } + } + } + return flags +} + +func matchArgAndFlags(flags []string, args []string) string { + var badFlag, prefix string + multipleFlagErr := false + +Loop: + for _, arg := range args { + prefix = "" + + //only take flag name, ignore value after '=' + arg = strings.Split(arg, "=")[0] + + if arg == "--h" || arg == "-h" { + continue Loop + } + + if strings.HasPrefix(arg, "--") { + prefix = "--" + } else if strings.HasPrefix(arg, "-") { + prefix = "-" + } + arg = strings.TrimLeft(arg, prefix) + + if prefix != "" { + for _, flag := range flags { + if flag == arg { + continue Loop + } + } + + if badFlag == "" { + badFlag = fmt.Sprintf("\"%s%s\"", prefix, arg) + } else { + multipleFlagErr = true + badFlag = badFlag + fmt.Sprintf(", \"%s%s\"", prefix, arg) + } + } + } + + if multipleFlagErr && badFlag != "" { + badFlag = fmt.Sprintf("%s %s", T("Unknown flags:"), badFlag) + } else if badFlag != "" { + badFlag = fmt.Sprintf("%s %s", T("Unknown flag"), badFlag) + } + + return badFlag +} diff --git a/main/main_suite_test.go b/main/main_suite_test.go new file mode 100644 index 00000000000..735f80ba69a --- /dev/null +++ b/main/main_suite_test.go @@ -0,0 +1,31 @@ +package main_test + +import ( + "path/filepath" + + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_1") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_2") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_with_push") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_with_push_short_name") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_with_help") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "my_say") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "call_core_cmd") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "input") + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "panics") + + //compile plugin examples to ensure they're up to date + plugin_builder.BuildTestBinary(filepath.Join("..", "plugin_examples"), "basic_plugin") + plugin_builder.BuildTestBinary(filepath.Join("..", "plugin_examples"), "echo") + plugin_builder.BuildTestBinary(filepath.Join("..", "plugin_examples"), "interactive") + + RunSpecs(t, "Main Suite") +} diff --git a/main/main_test.go b/main/main_test.go new file mode 100644 index 00000000000..95e8a9debd5 --- /dev/null +++ b/main/main_test.go @@ -0,0 +1,204 @@ +package main_test + +import ( + "bufio" + "os" + "os/exec" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("main", func() { + var ( + old_PLUGINS_HOME string + ) + + BeforeEach(func() { + old_PLUGINS_HOME = os.Getenv("CF_PLUGIN_HOME") + + dir, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + fullDir := filepath.Join(dir, "..", "fixtures", "config", "main-plugin-test-config") + err = os.Setenv("CF_PLUGIN_HOME", fullDir) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + err := os.Setenv("CF_PLUGIN_HOME", old_PLUGINS_HOME) + Expect(err).NotTo(HaveOccurred()) + }) + + Describe("exit codes", func() { + It("exits non-zero when an unknown command is invoked", func() { + result := Cf("some-command-that-should-never-actually-be-a-real-thing-i-can-use") + + Eventually(result, 3*time.Second).Should(Say("not a registered command")) + Eventually(result).Should(Exit(1)) + }) + + It("exits non-zero when known command is invoked with invalid option", func() { + result := Cf("push", "--crazy") + Eventually(result).Should(Exit(1)) + }) + }) + + It("can print help for all core commands by executing only the command `cf`", func() { + output := Cf().Wait(3 * time.Second) + Eventually(output.Out.Contents).Should(ContainSubstring("A command line tool to interact with Cloud Foundry")) + }) + + Describe("Flag verification", func() { + It("informs user for any incorrect provided flags", func() { + result := Cf("push", "--no-hostname", "--bad-flag") + Eventually(result.Out).Should(Say("\"--bad-flag\"")) + Consistently(result.Out).ShouldNot(Say("\"--no-hostname\"")) + }) + + It("checks flags with prefix '--'", func() { + result := Cf("push", "not-a-flag", "--invalid-flag") + Eventually(result.Out).Should(Say("Unknown flag \"--invalid-flag\"")) + Consistently(result.Out).ShouldNot(Say("Unknown flag \"not-a-flag\"")) + }) + + It("checks flags with prefix '-'", func() { + result := Cf("push", "not-a-flag", "-invalid-flag") + Eventually(result.Out).Should(Say("\"-invalid-flag\"")) + Consistently(result.Out).ShouldNot(Say("\"not-a-flag\"")) + }) + + It("checks flags but ignores the value after '=' ", func() { + result := Cf("push", "-p=./", "-invalid-flag=blarg") + Eventually(result.Out).Should(Say("\"-invalid-flag\"")) + Consistently(result.Out).ShouldNot(Say("Unknown flag \"-p\"")) + }) + + It("outputs all unknown flags in single sentence", func() { + result := Cf("push", "--bad-flag1", "--bad-flag2", "--bad-flag3") + Eventually(result.Out).Should(Say("\"--bad-flag1\", \"--bad-flag2\", \"--bad-flag3\"")) + }) + + It("only checks input flags against flags from the provided command", func() { + result := Cf("push", "--no-hostname", "--skip-ssl-validation") + Eventually(result.Out).Should(Say("\"--skip-ssl-validation\"")) + }) + + It("accepts -h and --h flags for all commands", func() { + result := Cf("push", "-h") + Consistently(result.Out).ShouldNot(Say("Unknown flag \"-h\"")) + + result = Cf("target", "--h") + Consistently(result.Out).ShouldNot(Say("Unknown flag \"--h\"")) + }) + }) + + Describe("Plugins", func() { + It("Can call a plugin command from the Plugins configuration if it does not exist as a cf command", func() { + output := Cf("test_1_cmd1").Wait(3 * time.Second) + Eventually(output.Out).Should(Say("You called cmd1 in test_1")) + }) + + It("Can call another plugin command when more than one plugin is installed", func() { + output := Cf("test_2_cmd1").Wait(3 * time.Second) + Eventually(output.Out).Should(Say("You called cmd1 in test_2")) + }) + + It("informs user for any invalid commands", func() { + output := Cf("foo-bar") + Eventually(output.Out, 3*time.Second).Should(Say("'foo-bar' is not a registered command")) + }) + + It("Calls help if the plugin shares the same name", func() { + output := Cf("help") + Consistently(output.Out, 1).ShouldNot(Say("You called help in test_with_help")) + }) + + It("Calls the core push command if the plugin shares the same name", func() { + output := Cf("push") + Consistently(output.Out, 1).ShouldNot(Say("You called push in test_with_push")) + }) + + It("Calls the core short name if a plugin shares the same name", func() { + output := Cf("p") + Consistently(output.Out, 1).ShouldNot(Say("You called p within the plugin")) + }) + + It("Passes all arguments to a plugin", func() { + output := Cf("my-say", "foo").Wait(3 * time.Second) + Eventually(output.Out).Should(Say("foo")) + }) + + It("Passes all arguments and flags to a plugin", func() { + output := Cf("my-say", "foo", "--loud").Wait(3 * time.Second) + Eventually(output.Out).Should(Say("FOO")) + }) + + It("Calls a plugin that calls core commands", func() { + output := Cf("awesomeness").Wait(3 * time.Second) + Eventually(output.Out).Should(Say("my-say")) //look for another plugin + }) + + It("Sends stdoutput to the plugin to echo", func() { + output := Cf("core-command", "plugins").Wait(3 * time.Second) + Eventually(output.Out.Contents).Should(MatchRegexp("Command output from the plugin(.*\\W)*awesomeness(.*\\W)*FIN")) + }) + + It("Can call a core commmand from a plugin without terminal output", func() { + output := Cf("core-command-quiet", "plugins").Wait(3 * time.Second) + Eventually(output.Out.Contents).Should(MatchRegexp("^\n---------- Command output from the plugin")) + }) + + It("Can call a plugin that requires stdin (interactive)", func() { + session := CfWithIo("input", "silly\n").Wait(5 * time.Second) + Eventually(session.Out).Should(Say("silly")) + }) + + It("exits 1 when a plugin panics", func() { + session := Cf("panic").Wait(5 * time.Second) + Eventually(session).Should(Exit(1)) + }) + + It("exits 1 when a plugin exits 1", func() { + session := Cf("exit1").Wait(5 * time.Second) + Eventually(session).Should(Exit(1)) + }) + }) +}) + +func Cf(args ...string) *Session { + path, err := Build("github.com/cloudfoundry/cli/main") + Expect(err).NotTo(HaveOccurred()) + + session, err := Start(exec.Command(path, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + return session +} +func CfWithIo(command string, args string) *Session { + path, err := Build("github.com/cloudfoundry/cli/main") + Expect(err).NotTo(HaveOccurred()) + + cmd := exec.Command(path, command) + + stdin, err := cmd.StdinPipe() + Expect(err).ToNot(HaveOccurred()) + + buffer := bufio.NewWriter(stdin) + buffer.WriteString(args) + buffer.Flush() + + session, err := Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + return session +} + +// gexec.Build leaves a compiled binary behind in /tmp. +var _ = AfterSuite(func() { + CleanupBuildArtifacts() +}) diff --git a/makefile b/makefile new file mode 100644 index 00000000000..a54ca2c22d0 --- /dev/null +++ b/makefile @@ -0,0 +1,72 @@ +GODEPS = $(realpath ./Godeps/_workspace) +GOPATH := $(GODEPS):$(GOPATH) +PATH := $(GODEPS)/bin:$(PATH) + +# target: all - Run tests and generate binary +all: test build + +# target: help - Display targets +help: + @egrep "^# target:" [Mm]akefile | sort - + +# target: clean - Cleans build artifacts +clean: + echo Cleaning build artifacts... + go clean + echo + +# target: generate-language-resource - Creates language resource file +generate-language-resource: + echo Generating i18n resource file + go-bindata \ + -pkg resources \ + -ignore ".go" \ + -o cf/resources/i18n_resources.go \ + cf/i18n/resources/... cf/i18n/test_fixtures/... + echo + +# target: fmt - Formats go code +fmt format: + echo Formatting Packages... + go fmt ./cf/... ./testhelpers/... ./generic/... ./main/... ./glob/... ./words/... + echo + +# target: test - Runs CLI tests +test: clean generate-language-resource format + echo Testing packages: + LC_ALL="en_US.UTF-8" \ + go test ./cf/... ./generic/... -parallel 4 $(TEST_ARG) + echo + $(MAKE) vet + +# target: ginkgo - Runs CLI tests with ginkgo command +ginkgo: clean generate-language-resource format + echo Testing packages: + LC_ALL="en_US.UTF-8" \ + ginkgo -p cf/* generic + echo + $(MAKE) vet + +# target: vet - Vets CLI for issues +vet: + echo Vetting packages for potential issues... + go tool vet cf/. + echo + +# target: build - Build CLI binary +build: format generate-language-resource + echo Generating Binary... + mkdir -p out + go build -o out/cf ./main + echo + +# target: install-dev-tools - Installs dev tools needed to work on the CLI +install-dev-tools: + @echo Installing development tools into $(GODEPS) + go get github.com/onsi/ginkgo/ginkgo + go get github.com/onsi/gomega + go get code.google.com/p/go.tools/cmd/vet + go get github.com/jteeuwen/go-bindata/... + +.PHONY: all help clean generate-language-resource fmt format test ginkgo vet build install-dev-tools +.SILENT: all help clean generate-language-resource fmt format test ginkgo vet build diff --git a/plugin/cli_connection.go b/plugin/cli_connection.go new file mode 100644 index 00000000000..ac3d53b9dfe --- /dev/null +++ b/plugin/cli_connection.go @@ -0,0 +1,95 @@ +package plugin + +import ( + "errors" + "fmt" + "net" + "net/rpc" + "os" + "time" +) + +type cliConnection struct { + cliServerPort string +} + +func NewCliConnection(cliServerPort string) *cliConnection { + return &cliConnection{ + cliServerPort: cliServerPort, + } +} + +func (cliConnection *cliConnection) sendPluginMetadataToCliServer(metadata PluginMetadata) { + cliServerConn, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + var success bool + + err = cliServerConn.Call("CliRpcCmd.SetPluginMetadata", metadata, &success) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if !success { + os.Exit(1) + } + + os.Exit(0) +} + +func (cliConnection *cliConnection) CliCommandWithoutTerminalOutput(args ...string) ([]string, error) { + return cliConnection.callCliCommand(true, args...) +} + +func (cliConnection *cliConnection) CliCommand(args ...string) ([]string, error) { + return cliConnection.callCliCommand(false, args...) +} + +func (cliConnection *cliConnection) callCliCommand(silently bool, args ...string) ([]string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return []string{}, err + } + + var success bool + + client.Call("CliRpcCmd.DisableTerminalOutput", silently, &success) + err = client.Call("CliRpcCmd.CallCoreCommand", args, &success) + + var cmdOutput []string + outputErr := client.Call("CliRpcCmd.GetOutputAndReset", success, &cmdOutput) + + if err != nil { + return cmdOutput, err + } else if !success { + return cmdOutput, errors.New("Error executing cli core command") + } + + if outputErr != nil { + return cmdOutput, errors.New("something completely unexpected happened") + } + return cmdOutput, nil +} + +func (cliConnection *cliConnection) pingCLI() { + //call back to cf saying we have been setup + var connErr error + var conn net.Conn + for i := 0; i < 5; i++ { + conn, connErr = net.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if connErr != nil { + time.Sleep(200 * time.Millisecond) + } else { + conn.Close() + break + } + } + if connErr != nil { + fmt.Println(connErr) + os.Exit(1) + } +} diff --git a/plugin/fakes/fake_cli_connection.go b/plugin/fakes/fake_cli_connection.go new file mode 100644 index 00000000000..b3dbab723d1 --- /dev/null +++ b/plugin/fakes/fake_cli_connection.go @@ -0,0 +1,95 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + . "github.com/cloudfoundry/cli/plugin" +) + +type FakeCliConnection struct { + CliCommandWithoutTerminalOutputStub func(args ...string) ([]string, error) + cliCommandWithoutTerminalOutputMutex sync.RWMutex + cliCommandWithoutTerminalOutputArgsForCall []struct { + args []string + } + cliCommandWithoutTerminalOutputReturns struct { + result1 []string + result2 error + } + CliCommandStub func(args ...string) ([]string, error) + cliCommandMutex sync.RWMutex + cliCommandArgsForCall []struct { + args []string + } + cliCommandReturns struct { + result1 []string + result2 error + } +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutput(args ...string) ([]string, error) { + fake.cliCommandWithoutTerminalOutputMutex.Lock() + defer fake.cliCommandWithoutTerminalOutputMutex.Unlock() + fake.cliCommandWithoutTerminalOutputArgsForCall = append(fake.cliCommandWithoutTerminalOutputArgsForCall, struct { + args []string + }{args}) + if fake.CliCommandWithoutTerminalOutputStub != nil { + return fake.CliCommandWithoutTerminalOutputStub(args...) + } else { + return fake.cliCommandWithoutTerminalOutputReturns.result1, fake.cliCommandWithoutTerminalOutputReturns.result2 + } +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputCallCount() int { + fake.cliCommandWithoutTerminalOutputMutex.RLock() + defer fake.cliCommandWithoutTerminalOutputMutex.RUnlock() + return len(fake.cliCommandWithoutTerminalOutputArgsForCall) +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputArgsForCall(i int) []string { + fake.cliCommandWithoutTerminalOutputMutex.RLock() + defer fake.cliCommandWithoutTerminalOutputMutex.RUnlock() + return fake.cliCommandWithoutTerminalOutputArgsForCall[i].args +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputReturns(result1 []string, result2 error) { + fake.cliCommandWithoutTerminalOutputReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) CliCommand(args ...string) ([]string, error) { + fake.cliCommandMutex.Lock() + defer fake.cliCommandMutex.Unlock() + fake.cliCommandArgsForCall = append(fake.cliCommandArgsForCall, struct { + args []string + }{args}) + if fake.CliCommandStub != nil { + return fake.CliCommandStub(args...) + } else { + return fake.cliCommandReturns.result1, fake.cliCommandReturns.result2 + } +} + +func (fake *FakeCliConnection) CliCommandCallCount() int { + fake.cliCommandMutex.RLock() + defer fake.cliCommandMutex.RUnlock() + return len(fake.cliCommandArgsForCall) +} + +func (fake *FakeCliConnection) CliCommandArgsForCall(i int) []string { + fake.cliCommandMutex.RLock() + defer fake.cliCommandMutex.RUnlock() + return fake.cliCommandArgsForCall[i].args +} + +func (fake *FakeCliConnection) CliCommandReturns(result1 []string, result2 error) { + fake.cliCommandReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +var _ CliConnection = new(FakeCliConnection) diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 00000000000..2d0f7109a0a --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,27 @@ +package plugin + +/** + Command interface needs to be implemented for a runnable plugin of `cf` +**/ +type Plugin interface { + Run(cliConnection CliConnection, args []string) + GetMetadata() PluginMetadata +} + +/** + List of commands avaiable to CliConnection variable passed into run +**/ +type CliConnection interface { + CliCommandWithoutTerminalOutput(args ...string) ([]string, error) + CliCommand(args ...string) ([]string, error) +} + +type PluginMetadata struct { + Name string + Commands []Command +} + +type Command struct { + Name string + HelpText string +} diff --git a/plugin/plugin_shim.go b/plugin/plugin_shim.go new file mode 100644 index 00000000000..87be99cc33e --- /dev/null +++ b/plugin/plugin_shim.go @@ -0,0 +1,24 @@ +package plugin + +import "os" + +/** + * This function is called by the plugin to setup their server. This allows us to call Run on the plugin + * os.Args[1] port CF_CLI rpc server is running on + * os.Args[2] **OPTIONAL** + * SendMetadata - used to fetch the plugin metadata +**/ +func Start(cmd Plugin) { + cliConnection := NewCliConnection(os.Args[1]) + + cliConnection.pingCLI() + if isMetadataRequest(os.Args) { + cliConnection.sendPluginMetadataToCliServer(cmd.GetMetadata()) + } else { + cmd.Run(cliConnection, os.Args[2:]) + } +} + +func isMetadataRequest(args []string) bool { + return len(args) == 3 && args[2] == "SendMetadata" +} diff --git a/plugin/plugin_shim_test.go b/plugin/plugin_shim_test.go new file mode 100644 index 00000000000..910af90edcb --- /dev/null +++ b/plugin/plugin_shim_test.go @@ -0,0 +1,25 @@ +package plugin_test + +import ( + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Command", func() { + var ( + validPluginPath = filepath.Join("..", "fixtures", "plugins", "test_1.exe") + ) + + Describe(".Start", func() { + It("Exits with status 1 if it cannot ping the host port passed as an argument", func() { + args := []string{"0", "0"} + session, err := Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(session, 2).Should(Exit(1)) + }) + }) +}) diff --git a/plugin/plugin_suite_test.go b/plugin/plugin_suite_test.go new file mode 100644 index 00000000000..3a799a4b742 --- /dev/null +++ b/plugin/plugin_suite_test.go @@ -0,0 +1,17 @@ +package plugin_test + +import ( + "path/filepath" + + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_1") + RunSpecs(t, "Plugin Suite") +} diff --git a/plugin/rpc/cli_rpc_server.go b/plugin/rpc/cli_rpc_server.go new file mode 100644 index 00000000000..74452f922bc --- /dev/null +++ b/plugin/rpc/cli_rpc_server.go @@ -0,0 +1,115 @@ +package rpc + +import ( + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/plugin" + "github.com/codegangsta/cli" + + "fmt" + "net" + "net/rpc" + "strconv" +) + +type CliRpcService struct { + listener net.Listener + stopCh chan struct{} + Pinged bool + RpcCmd *CliRpcCmd +} + +type CliRpcCmd struct { + PluginMetadata *plugin.PluginMetadata + coreCommandRunner *cli.App + outputCapture terminal.OutputCapture + terminalOutputSwitch terminal.TerminalOutputSwitch +} + +func NewRpcService(commandRunner *cli.App, outputCapture terminal.OutputCapture, terminalOutputSwitch terminal.TerminalOutputSwitch) (*CliRpcService, error) { + rpcService := &CliRpcService{ + RpcCmd: &CliRpcCmd{ + PluginMetadata: &plugin.PluginMetadata{}, + coreCommandRunner: commandRunner, + outputCapture: outputCapture, + terminalOutputSwitch: terminalOutputSwitch, + }, + } + + err := rpc.Register(rpcService.RpcCmd) + if err != nil { + return nil, err + } + + return rpcService, nil +} + +func (cli *CliRpcService) Stop() { + close(cli.stopCh) + cli.listener.Close() +} + +func (cli *CliRpcService) Port() string { + return strconv.Itoa(cli.listener.Addr().(*net.TCPAddr).Port) +} + +func (cli *CliRpcService) Start() error { + var err error + + cli.stopCh = make(chan struct{}) + + cli.listener, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return err + } + + go func() { + for { + conn, err := cli.listener.Accept() + if err != nil { + select { + case <-cli.stopCh: + return + default: + fmt.Println(err) + } + } else { + go rpc.ServeConn(conn) + } + } + }() + + return nil +} + +func (cmd *CliRpcCmd) SetPluginMetadata(pluginMetadata plugin.PluginMetadata, retVal *bool) error { + cmd.PluginMetadata = &pluginMetadata + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) DisableTerminalOutput(disable bool, retVal *bool) error { + cmd.terminalOutputSwitch.DisableTerminalOutput(disable) + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) CallCoreCommand(args []string, retVal *bool) error { + defer func() { + recover() + }() + + err := cmd.coreCommandRunner.Run(append([]string{"CF_NAME"}, args...)) + + if err != nil { + *retVal = false + return err + } + + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) GetOutputAndReset(args bool, retVal *[]string) error { + *retVal = cmd.outputCapture.GetOutputAndReset() + return nil +} diff --git a/plugin/rpc/cli_rpc_server_test.go b/plugin/rpc/cli_rpc_server_test.go new file mode 100644 index 00000000000..50fc2cdf097 --- /dev/null +++ b/plugin/rpc/cli_rpc_server_test.go @@ -0,0 +1,316 @@ +package rpc_test + +import ( + "net" + "net/rpc" + "time" + + "github.com/cloudfoundry/cli/cf/terminal/fakes" + "github.com/cloudfoundry/cli/plugin" + . "github.com/cloudfoundry/cli/plugin/rpc" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + "github.com/codegangsta/cli" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Server", func() { + var ( + err error + client *rpc.Client + rpcService *CliRpcService + ) + + AfterEach(func() { + if client != nil { + client.Close() + } + }) + + BeforeEach(func() { + rpc.DefaultServer = rpc.NewServer() + }) + + Describe(".NewRpcService", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an err of another Rpc process is already registered", func() { + _, err := NewRpcService(nil, nil, nil) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Stop", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("shuts down the rpc server", func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Start", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("Start an Rpc server for communication", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe(".SetPluginMetadata", func() { + var ( + metadata *plugin.PluginMetadata + ) + + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + metadata = &plugin.PluginMetadata{ + Name: "foo", + Commands: []plugin.Command{ + {Name: "cmd_1", HelpText: "cm 1 help text"}, + {Name: "cmd_2", HelpText: "cmd 2 help text"}, + }, + } + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("set the rpc command's Return Data", func() { + var success bool + err = client.Call("CliRpcCmd.SetPluginMetadata", metadata, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + Expect(rpcService.RpcCmd.PluginMetadata).To(Equal(metadata)) + }) + }) + + Describe(".GetOutputAndReset", func() { + Context("success", func() { + BeforeEach(func() { + outputCapture := &fakes.FakeOutputCapture{} + outputCapture.GetOutputAndResetReturns([]string{"hi from command"}) + rpcService, err = NewRpcService(nil, outputCapture, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("should return the logs from the output capture", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + var output []string + client.Call("CliRpcCmd.GetOutputAndReset", false, &output) + + Expect(output).To(Equal([]string{"hi from command"})) + }) + }) + }) + + Describe("disabling terminal output", func() { + var terminalOutputSwitch *fakes.FakeTerminalOutputSwitch + + BeforeEach(func() { + terminalOutputSwitch = &fakes.FakeTerminalOutputSwitch{} + rpcService, err = NewRpcService(nil, nil, terminalOutputSwitch) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("should disable the terminal output switch", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.DisableTerminalOutput", true, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + Expect(terminalOutputSwitch.DisableTerminalOutputCallCount()).To(Equal(1)) + Expect(terminalOutputSwitch.DisableTerminalOutputArgsForCall(0)).To(Equal(true)) + }) + }) + + Describe(".CallCoreCommand", func() { + Context("success", func() { + BeforeEach(func() { + app := &cli.App{ + Commands: []cli.Command{ + { + Name: "test_cmd", + Description: "test_cmd description", + Usage: "test_cmd usage", + Action: func(context *cli.Context) { + return + }, + }, + }, + } + + outputCapture := &fakes.FakeOutputCapture{} + + rpcService, err = NewRpcService(app, outputCapture, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("calls the code gangsta cli App command", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd"}, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + }) + }) + + Context("fail", func() { + BeforeEach(func() { + app := &cli.App{ + Commands: []cli.Command{ + { + Name: "test_cmd", + Description: "test_cmd description", + Usage: "test_cmd usage", + Action: func(context *cli.Context) { + panic("ERROR") + }, + }, + }, + } + outputCapture := &fakes.FakeOutputCapture{} + rpcService, err = NewRpcService(app, outputCapture, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns false in success if the command cannot be found", func() { + io_helpers.CaptureOutput(func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"not_a_cmd"}, &success) + Expect(success).To(BeFalse()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("returns an error if a command cannot parse provided flags", func() { + io_helpers.CaptureOutput(func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd", "-invalid_flag"}, &success) + + Expect(err).To(HaveOccurred()) + Expect(success).To(BeFalse()) + }) + }) + + It("recovers from a panic from any core command", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd"}, &success) + + Expect(success).To(BeFalse()) + }) + }) + }) +}) + +func pingCli(port string) { + var connErr error + var conn net.Conn + for i := 0; i < 5; i++ { + conn, connErr = net.Dial("tcp", "127.0.0.1:"+port) + if connErr != nil { + time.Sleep(200 * time.Millisecond) + } else { + conn.Close() + break + } + } + Expect(connErr).ToNot(HaveOccurred()) +} diff --git a/plugin/rpc/rpc_suite_test.go b/plugin/rpc/rpc_suite_test.go new file mode 100644 index 00000000000..094a948a1f2 --- /dev/null +++ b/plugin/rpc/rpc_suite_test.go @@ -0,0 +1,16 @@ +package rpc_test + +import ( + "github.com/cloudfoundry/cli/plugin/rpc" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +var rpcService *rpc.CliRpcService + +func TestRpc(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Rpc Suite") +} diff --git a/plugin/rpc/run_plugin.go b/plugin/rpc/run_plugin.go new file mode 100644 index 00000000000..67984ac416e --- /dev/null +++ b/plugin/rpc/run_plugin.go @@ -0,0 +1,58 @@ +package rpc + +import ( + "os" + "os/exec" + + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/codegangsta/cli" +) + +func RunMethodIfExists(coreCommandRunner *cli.App, args []string, outputCapture terminal.OutputCapture, terminalOutputSwitch terminal.TerminalOutputSwitch) bool { + pluginsConfig := plugin_config.NewPluginConfig(func(err error) { panic(err) }) + pluginList := pluginsConfig.Plugins() + for _, metadata := range pluginList { + for _, command := range metadata.Commands { + if command.Name == args[0] { + cliServer, err := startCliServer(coreCommandRunner, outputCapture, terminalOutputSwitch) + if err != nil { + os.Exit(1) + } + + defer cliServer.Stop() + pluginArgs := append([]string{cliServer.Port()}, args...) + cmd := exec.Command(metadata.Location, pluginArgs...) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + + defer stopPlugin(cmd) + err = cmd.Run() + if err != nil { + os.Exit(1) + } + return true + } + } + } + return false +} + +func startCliServer(coreCommandRunner *cli.App, outputCapture terminal.OutputCapture, terminalOutputSwitch terminal.TerminalOutputSwitch) (*CliRpcService, error) { + cliServer, err := NewRpcService(coreCommandRunner, outputCapture, terminalOutputSwitch) + if err != nil { + return nil, err + } + + err = cliServer.Start() + if err != nil { + return nil, err + } + + return cliServer, nil +} + +func stopPlugin(plugin *exec.Cmd) { + plugin.Process.Kill() + plugin.Wait() +} diff --git a/plugin_examples/README.md b/plugin_examples/README.md new file mode 100644 index 00000000000..3f8ae551376 --- /dev/null +++ b/plugin_examples/README.md @@ -0,0 +1,92 @@ +# Developing a Plugin + +This README discusses how to develop a cf CLI plugin. +For user-focused documentation, see [Using the cf CLI](http://docs.cloudfoundry.org/devguide/installcf/use-cli-plugins.html). + + +## Development Requirements + +- Golang installed +- Tagged version of CLI release source code that supports plugins; cf CLI v.6.7.0 and above + +## Architecture Overview + +The cf CLI plugin architecture model follows the remote procedure call (RPC) model. +The cf CLI invokes each plugin, runs it as an independent executable, and handles all start, stop, and clean up tasks for plugin executable resources. + +Plugins that you develop for the cf CLI must conform to a predefined plugin interface that we discuss below. + +## Writing a Plugin + +To write a plugin for the cf CLI, implement the +[predefined plugin interface](https://github.com/cloudfoundry/cli/blob/master/plugin/plugin.go). + +The interface uses a `Run(...)` method as the main entry point between the CLI +and a plugin. This method receives the following arguments: + + - A struct `plugin.CliConnection` that contains methods for invoking cf CLI commands + - A string array that contains the arguments passed from the `cf` process + +The `GetMetadata()` function informs the CLI of the name of a plugin, the +commands it implements, and help text for each command that users can display +with `cf help`. + + To initialize a plugin, call `plugin.Start(new(MyPluginStruct))` from within the `main()` method of your plugin. The `plugin.Start(...)` function requires a new reference to the struct that implements the defined interface. + +This repo contains a basic plugin example [here](https://github.com/cloudfoundry/cli/blob/master/plugin_examples/basic_plugin.go). + +### Using Command Line Arguments + +The `Run(...)` method accepts the command line arguments and flags that you +define for a plugin. + + See the [command line arguments example] (https://github.com/cloudfoundry/cli/blob/master/plugin_examples/echo.go) included in this repo. + +### Calling CLI Commands + +You can invoke CLI commands with `cliConnection.CliCommand([]args)` from + within a plugin's `Run(...)` method. The `Run(...)` method receives the +`cliConnection` as its first argument. + +The `cliConnection.CliCommand([]args)` returns the output printed by the command and an error. The output is returned as a slice of strings. The error +will be present if the call to the CLI command fails. + +See the [calling CLI commands example](https://github.com/cloudfoundry/cli/blob/master/plugin_examples/call_cli_cmd/main/call_cli_cmd.go) included in this repo. + +### Creating Interactive Plugins + +Because a plugin has access to stdin during a call to the `Run(...)` method, you can create interactive plugins. See the [interactive plugin example](https://github.com/cloudfoundry/cli/blob/master/plugin_examples/interactive.go) + included in this repo. + +## Compiling Plugin Source Code + +The cf CLI requires an executable file to install the plugin. You must compile the source code with the `go build` command before distributing the plugin, or instruct your users to compile the plugin source code before +installing the plugin. For information about compiling Go source code, see [Compile packages and dependencies](https://golang.org/cmd/go/). + +## Using Plugins + +After you compile a plugin, use the following commands to install and manage the plugin. + +### Installing Plugins + +To install a plugin, run: + +`cf install-plugin PATH_TO_PLUGIN_BINARY` + +### Listing Plugins + +To display a list of installed plugins and the commands available from each plugin, run: + +`cf plugins` + +### Uninstalling Plugins + +To remove a plugin, run: + +`cf uninstall-plugin PLUGIN_NAME` + +## Known Issues + +- When invoking a CLI command using `cliConnection.CliCommand([]args)` a plugin developer will not receive output generated by the codegangsta/cli package. This includes usage failures when executing a cli command, `cf help`, or `cli SOME-COMMAND -h`. + + diff --git a/plugin_examples/basic_plugin.go b/plugin_examples/basic_plugin.go new file mode 100644 index 00000000000..d95bb3d9cb7 --- /dev/null +++ b/plugin_examples/basic_plugin.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +/* +* This is the struct implementing the interface defined by the core CLI. It can +* be found at "github.com/cloudfoundry/cli/plugin/plugin.go" +* + */ +type BasicPlugin struct{} + +/* +* This function must be implemented by any plugin because it is part of the +* plugin interface defined by the core CLI. +* +* Run(....) is the entry point when the core CLI is invoking a command defined +* by a plugin. The first parameter, plugin.CliConnection, is a struct that can +* be used to invoke cli commands. The second paramter, args, is a slice of +* strings. args[0] will be the name of the command, and will be followed by +* any additional arguments a cli user typed in. +* +* Any error handling should be handled with the plugin itself (this means printing +* user facing errors). The CLI will exit 0 if the plugin exits 0 and will exit +* 1 should the plugin exits nonzero. + */ +func (c *BasicPlugin) Run(cliConnection plugin.CliConnection, args []string) { + // Ensure that we called the command basic-plugin-command + if args[0] == "basic-plugin-command" { + fmt.Println("Running the basic-plugin-command") + } +} + +/* +* This function must be implemented as part of the plugin interface +* defined by the core CLI. +* +* GetMetadata() returns a PluginMetadata struct. The first field, Name, +* determines the name of the plugin which should generally be without spaces. +* If there are spaces in the name a user will need to properly quote the name +* during uninstall otherwise the name will be treated as seperate arguments. +* The second value is a slice of Command structs. Our slice only contains one +* Command Struct, but could contain any number of them. The first field Name +* defines the command `cf basic-plugin-command` once installed into the CLI. The +* second field, HelpText, is used by the core CLI to display help information +* to the user in the core commands `cf help`, `cf`, or `cf -h`. + */ +func (c *BasicPlugin) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "MyBasicPlugin", + Commands: []plugin.Command{ + plugin.Command{ + Name: "basic-plugin-command", + HelpText: "Basic plugin command's help text", + }, + }, + } +} + +/* +* Unlike most Go programs, the `Main()` function will not be used to run all of the +* commands provided in your plugin. Main will be used to initialize the plugin +* process, as well as any dependencies you might require for your +* plugin. + */ +func main() { + // Any initialization for your plugin can be handled here + // + // Note: to run the plugin.Start method, we pass in a pointer to the struct + // implementing the interface defined at "github.com/cloudfoundry/cli/plugin/plugin.go" + // + // Note: The plugin's main() method is invoked at install time to collect + // metadata. The plugin will exit 0 and the Run([]string) method will not be + // invoked. + plugin.Start(new(BasicPlugin)) + // Plugin code should be written in the Run([]string) method, + // ensuring the plugin environment is bootstrapped. +} diff --git a/plugin_examples/call_cli_cmd/main/call_cli_cmd.go b/plugin_examples/call_cli_cmd/main/call_cli_cmd.go new file mode 100644 index 00000000000..d248aafd502 --- /dev/null +++ b/plugin_examples/call_cli_cmd/main/call_cli_cmd.go @@ -0,0 +1,54 @@ +/** +* This plugin is an example plugin that allows a user to call a cli-command +* by typing `cf cli-command name-of-command args.....`. This plugin also prints +* the output returned by the CLI when a cli-command is invoked. + */ +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type CliCmd struct{} + +func (c *CliCmd) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "CliCmd", + Commands: []plugin.Command{ + { + Name: "cli-command", + HelpText: "Command to call cli command. It passes all arguments through to the command", + }, + }, + } +} + +func main() { + plugin.Start(new(CliCmd)) +} + +func (c *CliCmd) Run(cliConnection plugin.CliConnection, args []string) { + // Invoke the cf command passed as the set of arguments + // after the first argument. + // + // Calls to plugin.CliCommand([]string) must be done after the invocation + // of plugin.Start() to ensure the environment is bootstrapped. + output, err := cliConnection.CliCommand(args[1:]...) + + // The call to plugin.CliCommand() returns an error if the cli command + // returns a non-zero return code or panics. The output written by the CLI + // is returned in any case. + if err != nil { + fmt.Println("PLUGIN ERROR: Error from CliCommand: ", err) + } + + // Print the output returned from the CLI command. + fmt.Println("") + fmt.Println("---------- Command output from the plugin ----------") + for index, val := range output { + fmt.Println("#", index, " value: ", val) + } + fmt.Println("---------- FIN -----------") +} diff --git a/plugin_examples/call_cli_cmd/main/call_cli_cmd_test.go b/plugin_examples/call_cli_cmd/main/call_cli_cmd_test.go new file mode 100644 index 00000000000..398ce85d699 --- /dev/null +++ b/plugin_examples/call_cli_cmd/main/call_cli_cmd_test.go @@ -0,0 +1,42 @@ +package main_test + +import ( + "github.com/cloudfoundry/cli/plugin/fakes" + . "github.com/cloudfoundry/cli/plugin_examples/call_cli_cmd/main" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("CallCliCmd", func() { + Describe(".Run", func() { + var fakeCliConnection *fakes.FakeCliConnection + var callCliCommandPlugin *CliCmd + + BeforeEach(func() { + fakeCliConnection = &fakes.FakeCliConnection{} + callCliCommandPlugin = &CliCmd{} + }) + + It("calls the cli command that is passed as an argument", func() { + io_helpers.CaptureOutput(func() { + callCliCommandPlugin.Run(fakeCliConnection, []string{"cli-command", "plugins", "arg1"}) + }) + + Expect(fakeCliConnection.CliCommandArgsForCall(0)[0]).To(Equal("plugins")) + Expect(fakeCliConnection.CliCommandArgsForCall(0)[1]).To(Equal("arg1")) + }) + + It("ouputs the text returned by the cli command", func() { + fakeCliConnection.CliCommandReturns([]string{"Hi", "Mom"}, nil) + output := io_helpers.CaptureOutput(func() { + callCliCommandPlugin.Run(fakeCliConnection, []string{"cli-command", "plugins", "arg1"}) + }) + + Expect(output[1]).To(Equal("---------- Command output from the plugin ----------")) + Expect(output[2]).To(Equal("# 0 value: Hi")) + Expect(output[3]).To(Equal("# 1 value: Mom")) + Expect(output[4]).To(Equal("---------- FIN -----------")) + }) + }) +}) diff --git a/plugin_examples/call_cli_cmd/main/main_suite_test.go b/plugin_examples/call_cli_cmd/main/main_suite_test.go new file mode 100644 index 00000000000..c90d8ae5d6d --- /dev/null +++ b/plugin_examples/call_cli_cmd/main/main_suite_test.go @@ -0,0 +1,17 @@ +package main_test + +import ( + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + + plugin_builder.BuildTestBinary(".", "call_cli_cmd") + + RunSpecs(t, "Main Suite") +} diff --git a/plugin_examples/echo.go b/plugin_examples/echo.go new file mode 100644 index 00000000000..40c9956a035 --- /dev/null +++ b/plugin_examples/echo.go @@ -0,0 +1,81 @@ +/** +* This is an example plugin where we use both arguments and flags. The plugin +* will echo all arguments passed to it. The flag -uppercase will upcase the +* arguments passed to the command. The help flag will print the usage text for +* this command and exit, ignoring any other arguments passed. + */ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/cloudfoundry/cli/plugin" +) + +type PluginDemonstratingParams struct { + help *bool + uppercase *bool +} + +func main() { + plugin.Start(new(PluginDemonstratingParams)) +} + +func (pluginDemo *PluginDemonstratingParams) Run(cliConnection plugin.CliConnection, args []string) { + // Initialize flags + echoFlagSet := flag.NewFlagSet("echo", flag.ExitOnError) + help := echoFlagSet.Bool("help", false, "passed to display help text") + uppercase := echoFlagSet.Bool("uppercase", false, "displayes all provided text in uppercase") + + // Parse starting from [1] because the [0]th element is the + // name of the command + err := echoFlagSet.Parse(args[1:]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if *help { + printHelp() + os.Exit(0) + } + + var itemToEcho string + for _, value := range echoFlagSet.Args() { + if *uppercase { + itemToEcho += strings.ToUpper(value) + " " + } else { + itemToEcho += value + " " + } + } + + fmt.Println(itemToEcho) +} + +func (pluginDemo *PluginDemonstratingParams) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "EchoDemo", + Commands: []plugin.Command{ + { + Name: "echo", + HelpText: "Echo text passed into the command. To obtain more information use --help", + }, + }, + } +} + +func printHelp() { + fmt.Println(` +cf echo [-uppercase] text + +OPTIONAL PARAMS: +-help: used to display this additional output. +-uppercase: If this param is passed, which ever word is passed to echo will be all capitals. + +REQUIRED PARAMS: +text: text to echo + `) +} diff --git a/plugin_examples/interactive.go b/plugin_examples/interactive.go new file mode 100644 index 00000000000..ef2a394a23a --- /dev/null +++ b/plugin_examples/interactive.go @@ -0,0 +1,43 @@ +/** +* This is an example of an interactive plugin. The plugin is invoked with +* `cf interactive` after which the user is prompted to enter a word. This word is +* then echoed back to the user. + */ + +package main + +import ( + "fmt" + + "github.com/cloudfoundry/cli/plugin" +) + +type Interactive struct{} + +func (c *Interactive) Run(cliConnection plugin.CliConnection, args []string) { + if args[0] == "interactive" { + var Echo string + fmt.Printf("Enter word: ") + + // Simple scan to wait for interactive from stdin + fmt.Scanf("%s", &Echo) + + fmt.Println("Your word was:", Echo) + } +} + +func (c *Interactive) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Interactive", + Commands: []plugin.Command{ + { + Name: "interactive", + HelpText: "help text for interactive", + }, + }, + } +} + +func main() { + plugin.Start(new(Interactive)) +} diff --git a/release/index.html b/release/index.html new file mode 100644 index 00000000000..d34630caceb --- /dev/null +++ b/release/index.html @@ -0,0 +1,29 @@ + + + + Cloud Foundry CLI Stable Releases + + + +

Cloud Foundry CLI Stable Releases

+ +

Binaries

+ + +

Installers

+ + diff --git a/src/cf/api/app_events.go b/src/cf/api/app_events.go deleted file mode 100644 index c5bb2af8d33..00000000000 --- a/src/cf/api/app_events.go +++ /dev/null @@ -1,83 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "time" -) - -const APP_EVENT_TIMESTAMP_FORMAT = "2006-01-02T15:04:05-07:00" - -type PaginatedEventResources struct { - Resources []EventResource - NextURL string `json:"next_url"` -} - -type EventResource struct { - Resource - Entity EventEntity -} - -type EventEntity struct { - Timestamp time.Time - ExitDescription string `json:"exit_description"` - ExitStatus int `json:"exit_status"` - InstanceIndex int `json:"instance_index"` -} - -type AppEventsRepository interface { - ListEvents(appGuid string) (events chan []cf.EventFields, statusChan chan net.ApiResponse) -} - -type CloudControllerAppEventsRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerAppEventsRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerAppEventsRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerAppEventsRepository) ListEvents(appGuid string) (eventChan chan []cf.EventFields, statusChan chan net.ApiResponse) { - - eventChan = make(chan []cf.EventFields, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := fmt.Sprintf("/v2/apps/%s/events", appGuid) - for path != "" { - url := fmt.Sprintf("%s%s", repo.config.Target, path) - eventResources := &PaginatedEventResources{} - apiResponse := repo.gateway.GetResource(url, repo.config.AccessToken, eventResources) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(eventChan) - close(statusChan) - return - } - - events := []cf.EventFields{} - for _, resource := range eventResources.Resources { - events = append(events, cf.EventFields{ - Timestamp: resource.Entity.Timestamp, - ExitDescription: resource.Entity.ExitDescription, - ExitStatus: resource.Entity.ExitStatus, - InstanceIndex: resource.Entity.InstanceIndex, - }) - } - if len(events) > 0 { - eventChan <- events - } - - path = eventResources.NextURL - } - close(eventChan) - close(statusChan) - }() - - return -} diff --git a/src/cf/api/app_events_test.go b/src/cf/api/app_events_test.go deleted file mode 100644 index 46202d3472a..00000000000 --- a/src/cf/api/app_events_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - testnet "testhelpers/net" - "testing" - "time" -) - -var firstPageEventsRequest = testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-app-guid/events", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{ - "total_results": 58, - "total_pages": 2, - "prev_url": null, - "next_url": "/v2/apps/my-app-guid/events?inline-relations-depth=1&page=2&results-per-page=50", - "resources": [ - { - "entity": { - "instance_index": 1, - "exit_status": 1, - "exit_description": "app instance exited", - "timestamp": "2013-10-07T16:51:07+00:00" - } - } - ] -} -`}, -} -var secondPageEventsRequest = testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-app-guid/events", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{ - "total_results": 58, - "total_pages": 2, - "prev_url": null, - "next_url": "", - "resources": [ - { - "entity": { - "instance_index": 2, - "exit_status": 2, - "exit_description": "app instance was stopped", - "timestamp": "2013-10-07T17:51:07+00:00" - } - } - ] -} -`}, -} - -var notFoundRequest = testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-app-guid/events", - Response: testnet.TestResponse{ - Status: http.StatusNotFound, - }, -} - -func TestListEvents(t *testing.T) { - listEventsServer, handler := testnet.NewTLSServer(t, []testnet.TestRequest{ - firstPageEventsRequest, - secondPageEventsRequest, - }) - defer listEventsServer.Close() - - config := &configuration.Configuration{ - Target: listEventsServer.URL, - AccessToken: "BEARER my_access_token", - } - repo := NewCloudControllerAppEventsRepository(config, net.NewCloudControllerGateway()) - - eventChan, apiErr := repo.ListEvents("my-app-guid") - - firstExpectedTime, err := time.Parse(APP_EVENT_TIMESTAMP_FORMAT, "2013-10-07T16:51:07+00:00") - secondExpectedTime, err := time.Parse(APP_EVENT_TIMESTAMP_FORMAT, "2013-10-07T17:51:07+00:00") - expectedEvents := []cf.EventFields{ - { - InstanceIndex: 1, - ExitStatus: 1, - ExitDescription: "app instance exited", - Timestamp: firstExpectedTime, - }, - { - InstanceIndex: 2, - ExitStatus: 2, - ExitDescription: "app instance was stopped", - Timestamp: secondExpectedTime, - }, - } - - list := []cf.EventFields{} - for events := range eventChan { - list = append(list, events...) - } - - _, open := <-apiErr - - assert.NoError(t, err) - assert.False(t, open) - assert.Equal(t, list, expectedEvents) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestListEventsWithNoEvents(t *testing.T) { - emptyEventsRequest := testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-app-guid/events", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": []}`}, - } - - listEventsServer, handler := testnet.NewTLSServer(t, []testnet.TestRequest{emptyEventsRequest}) - defer listEventsServer.Close() - - config := &configuration.Configuration{ - Target: listEventsServer.URL, - AccessToken: "BEARER my_access_token", - } - repo := NewCloudControllerAppEventsRepository(config, net.NewCloudControllerGateway()) - eventChan, apiErr := repo.ListEvents("my-app-guid") - - _, ok := <-eventChan - _, open := <-apiErr - - assert.False(t, ok) - assert.False(t, open) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestListEventsNotFound(t *testing.T) { - - listEventsServer, handler := testnet.NewTLSServer(t, []testnet.TestRequest{ - firstPageEventsRequest, - notFoundRequest, - }) - defer listEventsServer.Close() - - config := &configuration.Configuration{ - Target: listEventsServer.URL, - AccessToken: "BEARER my_access_token", - } - repo := NewCloudControllerAppEventsRepository(config, net.NewCloudControllerGateway()) - eventChan, apiErr := repo.ListEvents("my-app-guid") - - firstExpectedTime, err := time.Parse(APP_EVENT_TIMESTAMP_FORMAT, "2013-10-07T16:51:07+00:00") - expectedEvents := []cf.EventFields{ - { - InstanceIndex: 1, - ExitStatus: 1, - ExitDescription: "app instance exited", - Timestamp: firstExpectedTime, - }, - } - - list := []cf.EventFields{} - for events := range eventChan { - list = append(list, events...) - } - - apiResponse := <-apiErr - - assert.NoError(t, err) - assert.Equal(t, list, expectedEvents) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.True(t, handler.AllRequestsCalled()) -} diff --git a/src/cf/api/app_files.go b/src/cf/api/app_files.go deleted file mode 100644 index ff224329a83..00000000000 --- a/src/cf/api/app_files.go +++ /dev/null @@ -1,33 +0,0 @@ -package api - -import ( - "cf/configuration" - "cf/net" - "fmt" -) - -type AppFilesRepository interface { - ListFiles(appGuid, path string) (files string, apiResponse net.ApiResponse) -} - -type CloudControllerAppFilesRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerAppFilesRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerAppFilesRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerAppFilesRepository) ListFiles(appGuid, path string) (files string, apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/apps/%s/instances/0/files/%s", repo.config.Target, appGuid, path) - request, apiResponse := repo.gateway.NewRequest("GET", url, repo.config.AccessToken, nil) - if apiResponse.IsNotSuccessful() { - return - } - - files, _, apiResponse = repo.gateway.PerformRequestForTextResponse(request) - return -} diff --git a/src/cf/api/app_files_test.go b/src/cf/api/app_files_test.go deleted file mode 100644 index 69a497103ac..00000000000 --- a/src/cf/api/app_files_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package api - -import ( - "cf/configuration" - "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestListFiles(t *testing.T) { - expectedResponse := "file 1\n file 2\n file 3" - - listFilesEndpoint := func(writer http.ResponseWriter, request *http.Request) { - methodMatches := request.Method == "GET" - pathMatches := request.URL.Path == "/some/path" - - if !methodMatches || !pathMatches { - fmt.Printf("One of the matchers did not match. Method [%t] Path [%t]", - methodMatches, pathMatches) - - writer.WriteHeader(http.StatusInternalServerError) - return - } - - writer.WriteHeader(http.StatusOK) - fmt.Fprint(writer, expectedResponse) - } - - listFilesServer := httptest.NewTLSServer(http.HandlerFunc(listFilesEndpoint)) - defer listFilesServer.Close() - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-app-guid/instances/0/files/some/path", - Response: testnet.TestResponse{ - Status: http.StatusTemporaryRedirect, - Header: http.Header{ - "Location": {fmt.Sprintf("%s/some/path", listFilesServer.URL)}, - }, - }, - }) - - listFilesRedirectServer, handler := testnet.NewTLSServer(t, []testnet.TestRequest{req}) - defer listFilesRedirectServer.Close() - - config := &configuration.Configuration{ - Target: listFilesRedirectServer.URL, - AccessToken: "BEARER my_access_token", - } - - gateway := net.NewCloudControllerGateway() - repo := NewCloudControllerAppFilesRepository(config, gateway) - list, err := repo.ListFiles("my-app-guid", "some/path") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, err.IsNotSuccessful()) - assert.Equal(t, list, expectedResponse) -} diff --git a/src/cf/api/app_instances.go b/src/cf/api/app_instances.go deleted file mode 100644 index 69524b8b65f..00000000000 --- a/src/cf/api/app_instances.go +++ /dev/null @@ -1,104 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strconv" - "strings" - "time" -) - -type InstancesApiResponse map[string]InstanceApiResponse - -type InstanceApiResponse struct { - State string - Since float64 -} - -type StatsApiResponse map[string]InstanceStatsApiResponse - -type InstanceStatsApiResponse struct { - Stats struct { - DiskQuota uint64 `json:"disk_quota"` - MemQuota uint64 `json:"mem_quota"` - Usage struct { - Cpu float64 - Disk uint64 - Mem uint64 - } - } -} - -type AppInstancesRepository interface { - GetInstances(appGuid string) (instances []cf.AppInstanceFields, apiResponse net.ApiResponse) -} - -type CloudControllerAppInstancesRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerAppInstancesRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerAppInstancesRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerAppInstancesRepository) GetInstances(appGuid string) (instances []cf.AppInstanceFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s/instances", repo.config.Target, appGuid) - request, apiResponse := repo.gateway.NewRequest("GET", path, repo.config.AccessToken, nil) - if apiResponse.IsNotSuccessful() { - return - } - - instancesResponse := InstancesApiResponse{} - - _, apiResponse = repo.gateway.PerformRequestForJSONResponse(request, &instancesResponse) - if apiResponse.IsNotSuccessful() { - return - } - - instances = make([]cf.AppInstanceFields, len(instancesResponse), len(instancesResponse)) - for k, v := range instancesResponse { - index, err := strconv.Atoi(k) - if err != nil { - continue - } - - instances[index] = cf.AppInstanceFields{ - State: cf.InstanceState(strings.ToLower(v.State)), - Since: time.Unix(int64(v.Since), 0), - } - } - - return repo.updateInstancesWithStats(appGuid, instances) -} - -func (repo CloudControllerAppInstancesRepository) updateInstancesWithStats(guid string, instances []cf.AppInstanceFields) (updatedInst []cf.AppInstanceFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s/stats", repo.config.Target, guid) - statsResponse := StatsApiResponse{} - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, &statsResponse) - if apiResponse.IsNotSuccessful() { - return - } - - updatedInst = make([]cf.AppInstanceFields, len(statsResponse), len(statsResponse)) - for k, v := range statsResponse { - index, err := strconv.Atoi(k) - if err != nil { - continue - } - - instance := instances[index] - instance.CpuUsage = v.Stats.Usage.Cpu - instance.DiskQuota = v.Stats.DiskQuota - instance.DiskUsage = v.Stats.Usage.Disk - instance.MemQuota = v.Stats.MemQuota - instance.MemUsage = v.Stats.Usage.Mem - - updatedInst[index] = instance - } - return -} diff --git a/src/cf/api/app_instances_test.go b/src/cf/api/app_instances_test.go deleted file mode 100644 index 71e404a71d1..00000000000 --- a/src/cf/api/app_instances_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" - "time" -) - -var appStatsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-cool-app-guid/stats", - Response: testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "1":{ - "stats": { - "disk_quota": 10000, - "mem_quota": 1024, - "usage": { - "cpu": 0.3, - "disk": 10000, - "mem": 1024 - } - } - }, - "0":{ - "stats": { - "disk_quota": 1073741824, - "mem_quota": 67108864, - "usage": { - "cpu": 3.659571249238058e-05, - "disk": 56037376, - "mem": 19218432 - } - } - } -}`}}) - -var appInstancesRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/apps/my-cool-app-guid/instances", - Response: testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "1": { - "state": "STARTING", - "since": 1379522342.6783738 - }, - "0": { - "state": "RUNNING", - "since": 1379522342.6783738 - } -}`}}) - -func TestAppInstancesGetInstances(t *testing.T) { - ts, handler, repo := createAppInstancesRepo(t, []testnet.TestRequest{ - appInstancesRequest, - appStatsRequest, - }) - defer ts.Close() - appGuid := "my-cool-app-guid" - - instances, err := repo.GetInstances(appGuid) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, err.IsNotSuccessful()) - - assert.Equal(t, len(instances), 2) - - assert.Equal(t, instances[0].State, cf.InstanceRunning) - assert.Equal(t, instances[1].State, cf.InstanceStarting) - - instance0 := instances[0] - assert.Equal(t, instance0.Since, time.Unix(1379522342, 0)) - assert.Exactly(t, instance0.DiskQuota, uint64(1073741824)) - assert.Exactly(t, instance0.DiskUsage, uint64(56037376)) - assert.Exactly(t, instance0.MemQuota, uint64(67108864)) - assert.Exactly(t, instance0.MemUsage, uint64(19218432)) - assert.Equal(t, instance0.CpuUsage, 3.659571249238058e-05) -} - -func createAppInstancesRepo(t *testing.T, requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo AppInstancesRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - SpaceFields: space, - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerAppInstancesRepository(config, gateway) - return -} diff --git a/src/cf/api/app_summary.go b/src/cf/api/app_summary.go deleted file mode 100644 index 4d1058fd019..00000000000 --- a/src/cf/api/app_summary.go +++ /dev/null @@ -1,132 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type ApplicationSummaries struct { - Apps []ApplicationFromSummary -} - -func (resource ApplicationSummaries) ToModels() (apps []cf.ApplicationFields) { - for _, appSummary := range resource.Apps { - apps = append(apps, appSummary.ToFields()) - } - return -} - -type ApplicationFromSummary struct { - Guid string - Name string - Routes []RouteSummary - RunningInstances int `json:"running_instances"` - Memory uint64 - Instances int - DiskQuota uint64 `json:"disk_quota"` - Urls []string - State string -} - -func (resource ApplicationFromSummary) ToFields() (app cf.ApplicationFields) { - app = cf.ApplicationFields{} - app.Guid = resource.Guid - app.Name = resource.Name - app.State = strings.ToLower(resource.State) - app.InstanceCount = resource.Instances - app.DiskQuota = resource.DiskQuota - app.RunningInstances = resource.RunningInstances - app.Memory = resource.Memory - - return -} - -func (resource ApplicationFromSummary) ToModel() (app cf.AppSummary) { - app.ApplicationFields = resource.ToFields() - routes := []cf.RouteSummary{} - for _, route := range resource.Routes { - routes = append(routes, route.ToModel()) - } - app.RouteSummaries = routes - - return -} - -type RouteSummary struct { - Guid string - Host string - Domain DomainSummary -} - -func (resource RouteSummary) ToModel() (route cf.RouteSummary) { - domain := cf.DomainFields{} - domain.Guid = resource.Domain.Guid - domain.Name = resource.Domain.Name - domain.Shared = resource.Domain.OwningOrganizationGuid != "" - - route.Guid = resource.Guid - route.Host = resource.Host - route.Domain = domain - return -} - -type DomainSummary struct { - Guid string - Name string - OwningOrganizationGuid string -} - -type AppSummaryRepository interface { - GetSummariesInCurrentSpace() (apps []cf.AppSummary, apiResponse net.ApiResponse) - GetSummary(appGuid string) (summary cf.AppSummary, apiResponse net.ApiResponse) -} - -type CloudControllerAppSummaryRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerAppSummaryRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerAppSummaryRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerAppSummaryRepository) GetSummariesInCurrentSpace() (apps []cf.AppSummary, apiResponse net.ApiResponse) { - resources := new(ApplicationSummaries) - - path := fmt.Sprintf("%s/v2/spaces/%s/summary", repo.config.Target, repo.config.SpaceFields.Guid) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - for _, resource := range resources.Apps { - var app cf.AppSummary - app, apiResponse = repo.createSummary(&resource) - if apiResponse.IsNotSuccessful() { - return - } - apps = append(apps, app) - } - return -} - -func (repo CloudControllerAppSummaryRepository) GetSummary(appGuid string) (summary cf.AppSummary, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s/summary", repo.config.Target, appGuid) - summaryResponse := new(ApplicationFromSummary) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, summaryResponse) - if apiResponse.IsNotSuccessful() { - return - } - - return repo.createSummary(summaryResponse) -} - -func (repo CloudControllerAppSummaryRepository) createSummary(resource *ApplicationFromSummary) (summary cf.AppSummary, apiResponse net.ApiResponse) { - summary = resource.ToModel() - return -} diff --git a/src/cf/api/app_summary_test.go b/src/cf/api/app_summary_test.go deleted file mode 100644 index 529e3693a5a..00000000000 --- a/src/cf/api/app_summary_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var getAppSummariesResponseBody = ` -{ - "apps":[ - { - "guid":"app-1-guid", - "routes":[ - { - "guid":"route-1-guid", - "host":"app1", - "domain":{ - "guid":"domain-1-guid", - "name":"cfapps.io" - } - } - ], - "running_instances":1, - "name":"app1", - "memory":128, - "instances":1, - "state":"STARTED", - "service_names":[ - "my-service-instance" - ] - },{ - "guid":"app-2-guid", - "routes":[ - { - "guid":"route-2-guid", - "host":"app2", - "domain":{ - "guid":"domain-1-guid", - "name":"cfapps.io" - } - }, - { - "guid":"route-2-guid", - "host":"foo", - "domain":{ - "guid":"domain-1-guid", - "name":"cfapps.io" - } - } - ], - "running_instances":1, - "name":"app2", - "memory":512, - "instances":3, - "state":"STARTED", - "service_names":[ - "my-service-instance" - ] - } - ] -}` - -func TestGetAppSummariesInCurrentSpace(t *testing.T) { - getAppSummariesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/summary", - Response: testnet.TestResponse{Status: http.StatusOK, Body: getAppSummariesResponseBody}, - }) - - ts, handler, repo := createAppSummaryRepo(t, []testnet.TestRequest{getAppSummariesRequest}) - defer ts.Close() - - apps, apiResponse := repo.GetSummariesInCurrentSpace() - assert.True(t, handler.AllRequestsCalled()) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, 2, len(apps)) - - app1 := apps[0] - assert.Equal(t, app1.Name, "app1") - assert.Equal(t, app1.Guid, "app-1-guid") - assert.Equal(t, len(app1.RouteSummaries), 1) - assert.Equal(t, app1.RouteSummaries[0].URL(), "app1.cfapps.io") - - assert.Equal(t, app1.State, "started") - assert.Equal(t, app1.InstanceCount, 1) - assert.Equal(t, app1.RunningInstances, 1) - assert.Equal(t, app1.Memory, uint64(128)) - - app2 := apps[1] - assert.Equal(t, app2.Name, "app2") - assert.Equal(t, app2.Guid, "app-2-guid") - assert.Equal(t, len(app2.RouteSummaries), 2) - assert.Equal(t, app2.RouteSummaries[0].URL(), "app2.cfapps.io") - assert.Equal(t, app2.RouteSummaries[1].URL(), "foo.cfapps.io") - - assert.Equal(t, app2.State, "started") - assert.Equal(t, app2.InstanceCount, 3) - assert.Equal(t, app2.RunningInstances, 1) - assert.Equal(t, app2.Memory, uint64(512)) -} - -func createAppSummaryRepo(t *testing.T, requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo AppSummaryRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - SpaceFields: space, - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerAppSummaryRepository(config, gateway) - return -} diff --git a/src/cf/api/application_bits.go b/src/cf/api/application_bits.go deleted file mode 100644 index 2884b236b56..00000000000 --- a/src/cf/api/application_bits.go +++ /dev/null @@ -1,356 +0,0 @@ -package api - -import ( - "archive/zip" - "bytes" - "cf" - "cf/configuration" - "cf/net" - "encoding/json" - "errors" - "fileutils" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "os" - "path/filepath" - "strings" - "time" -) - -type AppFileResource struct { - Path string `json:"fn"` - Sha1 string `json:"sha1"` - Size int64 `json:"size"` -} - -type ApplicationBitsRepository interface { - UploadApp(appGuid, dir string) (apiResponse net.ApiResponse) -} - -type CloudControllerApplicationBitsRepository struct { - config *configuration.Configuration - gateway net.Gateway - zipper cf.Zipper -} - -func NewCloudControllerApplicationBitsRepository(config *configuration.Configuration, gateway net.Gateway, zipper cf.Zipper) (repo CloudControllerApplicationBitsRepository) { - repo.config = config - repo.gateway = gateway - repo.zipper = zipper - return -} - -func (repo CloudControllerApplicationBitsRepository) UploadApp(appGuid string, appDir string) (apiResponse net.ApiResponse) { - fileutils.TempDir("apps", func(uploadDir string, err error) { - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - return - } - - var presentResourcesJson []byte - repo.sourceDir(appDir, func(sourceDir string, sourceErr error) { - if sourceErr != nil { - err = sourceErr - return - } - presentResourcesJson, err = repo.copyUploadableFiles(sourceDir, uploadDir) - }) - - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - return - } - - fileutils.TempFile("uploads", func(zipFile *os.File, err error) { - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - return - } - - err = repo.zipper.Zip(uploadDir, zipFile) - if err != nil { - apiResponse = net.NewApiResponseWithError("Error zipping application", err) - return - } - - apiResponse = repo.uploadBits(appGuid, zipFile, presentResourcesJson) - if apiResponse.IsNotSuccessful() { - return - } - }) - }) - return -} - -func (repo CloudControllerApplicationBitsRepository) uploadBits(appGuid string, zipFile *os.File, presentResourcesJson []byte) (apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/apps/%s/bits?async=true", repo.config.Target, appGuid) - - fileutils.TempFile("requests", func(requestFile *os.File, err error) { - if err != nil { - apiResponse = net.NewApiResponseWithError("Error creating tmp file: %s", err) - return - } - - boundary, err := repo.writeUploadBody(zipFile, requestFile, presentResourcesJson) - if err != nil { - apiResponse = net.NewApiResponseWithError("Error writing to tmp file: %s", err) - return - } - - var request *net.Request - request, apiResponse = repo.gateway.NewRequest("PUT", url, repo.config.AccessToken, requestFile) - if apiResponse.IsNotSuccessful() { - return - } - - contentType := fmt.Sprintf("multipart/form-data; boundary=%s", boundary) - request.HttpReq.Header.Set("Content-Type", contentType) - - response := &Resource{} - _, apiResponse = repo.gateway.PerformRequestForJSONResponse(request, response) - if apiResponse.IsNotSuccessful() { - return - } - - jobGuid := response.Metadata.Guid - apiResponse = repo.pollUploadProgress(jobGuid) - }) - - return -} - -const ( - uploadStatusFinished = "finished" - uploadStatusFailed = "failed" -) - -type UploadProgressEntity struct { - Status string -} - -type UploadProgressResponse struct { - Metadata Metadata - Entity UploadProgressEntity -} - -func (repo CloudControllerApplicationBitsRepository) pollUploadProgress(jobGuid string) (apiResponse net.ApiResponse) { - finished := false - for !finished { - finished, apiResponse = repo.uploadProgress(jobGuid) - if apiResponse.IsNotSuccessful() { - return - } - time.Sleep(time.Second) - } - return -} - -func (repo CloudControllerApplicationBitsRepository) uploadProgress(jobGuid string) (finished bool, apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/jobs/%s", repo.config.Target, jobGuid) - request, apiResponse := repo.gateway.NewRequest("GET", url, repo.config.AccessToken, nil) - response := &UploadProgressResponse{} - _, apiResponse = repo.gateway.PerformRequestForJSONResponse(request, response) - - switch response.Entity.Status { - case uploadStatusFinished: - finished = true - case uploadStatusFailed: - apiResponse = net.NewApiResponseWithMessage("Failed to complete upload.") - } - - return -} - -func (repo CloudControllerApplicationBitsRepository) sourceDir(appDir string, cb func(sourceDir string, err error)) { - // If appDir is a zip, first extract it to a temporary directory - if !repo.fileIsZip(appDir) { - cb(appDir, nil) - return - } - - fileutils.TempDir("unzipped-app", func(tmpDir string, err error) { - if err != nil { - cb("", err) - return - } - - err = repo.extractZip(appDir, tmpDir) - cb(tmpDir, err) - }) -} - -func (repo CloudControllerApplicationBitsRepository) copyUploadableFiles(appDir string, uploadDir string) (presentResourcesJson []byte, err error) { - // Find which files need to be uploaded - allAppFiles, err := cf.AppFilesInDir(appDir) - if err != nil { - return - } - - appFilesToUpload, presentResourcesJson, apiResponse := repo.getFilesToUpload(allAppFiles) - if apiResponse.IsNotSuccessful() { - err = errors.New(apiResponse.Message) - return - } - - // Copy files into a temporary directory and return it - err = cf.CopyFiles(appFilesToUpload, appDir, uploadDir) - if err != nil { - return - } - - return -} - -func (repo CloudControllerApplicationBitsRepository) fileIsZip(file string) bool { - isZip := strings.HasSuffix(file, ".zip") - isWar := strings.HasSuffix(file, ".war") - isJar := strings.HasSuffix(file, ".jar") - - return isZip || isWar || isJar -} - -func (repo CloudControllerApplicationBitsRepository) extractZip(zipFile string, destDir string) (err error) { - r, err := zip.OpenReader(zipFile) - if err != nil { - return - } - defer r.Close() - - for _, f := range r.File { - func() { - // Don't try to extract directories - if f.FileInfo().IsDir() { - return - } - - if err != nil { - return - } - - var rc io.ReadCloser - rc, err = f.Open() - if err != nil { - return - } - - defer rc.Close() - - destFilePath := filepath.Join(destDir, f.Name) - - err = fileutils.CopyReaderToPath(rc, destFilePath) - if err != nil { - return - } - - err = os.Chmod(destFilePath, f.FileInfo().Mode()) - if err != nil { - return - } - }() - } - - return -} -func (repo CloudControllerApplicationBitsRepository) getFilesToUpload(allAppFiles []cf.AppFileFields) (appFilesToUpload []cf.AppFileFields, presentResourcesJson []byte, apiResponse net.ApiResponse) { - appFilesRequest := []AppFileResource{} - for _, file := range allAppFiles { - appFilesRequest = append(appFilesRequest, AppFileResource{ - Path: file.Path, - Sha1: file.Sha1, - Size: file.Size, - }) - } - - allAppFilesJson, err := json.Marshal(appFilesRequest) - if err != nil { - apiResponse = net.NewApiResponseWithError("Failed to create json for resource_match request", err) - return - } - - path := fmt.Sprintf("%s/v2/resource_match", repo.config.Target) - req, apiResponse := repo.gateway.NewRequest("PUT", path, repo.config.AccessToken, bytes.NewReader(allAppFilesJson)) - if apiResponse.IsNotSuccessful() { - return - } - - presentResourcesJson, _, apiResponse = repo.gateway.PerformRequestForResponseBytes(req) - - fileResource := []AppFileResource{} - err = json.Unmarshal(presentResourcesJson, &fileResource) - - if err != nil { - apiResponse = net.NewApiResponseWithError("Failed to unmarshal json response from resource_match request", err) - return - } - - appFilesToUpload = make([]cf.AppFileFields, len(allAppFiles)) - copy(appFilesToUpload, allAppFiles) - for _, file := range fileResource { - appFile := cf.AppFileFields{ - Path: file.Path, - Sha1: file.Sha1, - Size: file.Size, - } - appFilesToUpload = repo.deleteAppFile(appFilesToUpload, appFile) - } - - return -} - -func (repo CloudControllerApplicationBitsRepository) deleteAppFile(appFiles []cf.AppFileFields, targetFile cf.AppFileFields) []cf.AppFileFields { - for i, file := range appFiles { - if file.Path == targetFile.Path { - appFiles[i] = appFiles[len(appFiles)-1] - return appFiles[:len(appFiles)-1] - } - } - return appFiles -} - -func (repo CloudControllerApplicationBitsRepository) writeUploadBody(zipFile *os.File, body *os.File, presentResourcesJson []byte) (boundary string, err error) { - writer := multipart.NewWriter(body) - defer writer.Close() - - boundary = writer.Boundary() - - part, err := writer.CreateFormField("resources") - if err != nil { - return - } - - _, err = io.Copy(part, bytes.NewBuffer(presentResourcesJson)) - if err != nil { - return - } - - zipStats, err := zipFile.Stat() - if err != nil { - return - } - - if zipStats.Size() == 0 { - return - } - - part, err = createZipPartWriter(zipStats, writer) - if err != nil { - return - } - - _, err = io.Copy(part, zipFile) - if err != nil { - return - } - return -} - -func createZipPartWriter(zipStats os.FileInfo, writer *multipart.Writer) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="application"; filename="application.zip"`) - h.Set("Content-Type", "application/zip") - h.Set("Content-Length", fmt.Sprintf("%d", zipStats.Size())) - h.Set("Content-Transfer-Encoding", "binary") - return writer.CreatePart(h) -} diff --git a/src/cf/api/application_bits_test.go b/src/cf/api/application_bits_test.go deleted file mode 100644 index 4c70e8cc139..00000000000 --- a/src/cf/api/application_bits_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package api - -import ( - "archive/zip" - "cf" - "cf/configuration" - "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var expectedResources = testnet.RemoveWhiteSpaceFromBody(`[ - { - "fn": "Gemfile", - "sha1": "d9c3a51de5c89c11331d3b90b972789f1a14699a", - "size": 59 - }, - { - "fn": "Gemfile.lock", - "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d", - "size": 229 - }, - { - "fn": "app.rb", - "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b", - "size": 51 - }, - { - "fn": "config.ru", - "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", - "size": 70 - }, - { - "fn": "manifest.yml", - "sha1": "19b5b4225dc64da3213b1ffaa1e1920ee5faf36c", - "size": 111 - } -]`) - -var matchedResources = testnet.RemoveWhiteSpaceFromBody(`[ - { - "fn": "app.rb", - "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b", - "size": 51 - }, - { - "fn": "config.ru", - "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", - "size": 70 - } -]`) - -var uploadApplicationRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-cool-app-guid/bits", - Matcher: uploadBodyMatcher, - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: ` -{ - "metadata":{ - "guid": "my-job-guid" - } -} - `}, -}) - -var matchResourceRequest = testnet.TestRequest{ - Method: "PUT", - Path: "/v2/resource_match", - Matcher: testnet.RequestBodyMatcher(expectedResources), - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: matchedResources, - }, -} - -var defaultRequests = []testnet.TestRequest{ - matchResourceRequest, - uploadApplicationRequest, - createProgressEndpoint("running"), - createProgressEndpoint("finished"), -} - -var expectedApplicationContent = []string{"Gemfile", "Gemfile.lock", "manifest.yml"} - -var uploadBodyMatcher = func(t *testing.T, request *http.Request) { - err := request.ParseMultipartForm(4096) - if err != nil { - assert.Fail(t, "Failed parsing multipart form", err) - return - } - defer request.MultipartForm.RemoveAll() - - assert.Equal(t, len(request.MultipartForm.Value), 1, "Should have 1 value") - valuePart, ok := request.MultipartForm.Value["resources"] - assert.True(t, ok, "Resource manifest not present") - assert.Equal(t, len(valuePart), 1, "Wrong number of values") - - resourceManifest := valuePart[0] - chompedResourceManifest := strings.Replace(resourceManifest, "\n", "", -1) - assert.Equal(t, chompedResourceManifest, matchedResources, "Resources do not match") - - assert.Equal(t, len(request.MultipartForm.File), 1, "Wrong number of files") - - fileHeaders, ok := request.MultipartForm.File["application"] - assert.True(t, ok, "Application file part not present") - assert.Equal(t, len(fileHeaders), 1, "Wrong number of files") - - applicationFile := fileHeaders[0] - assert.Equal(t, applicationFile.Filename, "application.zip", "Wrong file name") - - file, err := applicationFile.Open() - if err != nil { - assert.Fail(t, "Cannot get multipart file", err.Error()) - return - } - - length, err := strconv.ParseInt(applicationFile.Header.Get("content-length"), 10, 64) - if err != nil { - assert.Fail(t, "Cannot convert content-length to int", err.Error()) - return - } - - zipReader, err := zip.NewReader(file, length) - if err != nil { - assert.Fail(t, "Error reading zip content", err.Error()) - return - } - - assert.Equal(t, len(zipReader.File), 3, "Wrong number of files in zip") - assert.Equal(t, zipReader.File[0].Mode(), uint32(os.ModePerm)) - -nextFile: - for _, f := range zipReader.File { - for _, expected := range expectedApplicationContent { - if f.Name == expected { - continue nextFile - } - } - assert.Fail(t, "Missing file: "+f.Name) - } -} - -func createProgressEndpoint(status string) (req testnet.TestRequest) { - body := fmt.Sprintf(` - { - "entity":{ - "status":"%s" - } - }`, status) - - req.Method = "GET" - req.Path = "/v2/jobs/my-job-guid" - req.Response = testnet.TestResponse{ - Status: http.StatusCreated, - Body: body, - } - - return -} - -func TestUploadWithInvalidDirectory(t *testing.T) { - config := &configuration.Configuration{} - gateway := net.NewCloudControllerGateway() - zipper := &cf.ApplicationZipper{} - - repo := NewCloudControllerApplicationBitsRepository(config, gateway, zipper) - - apiResponse := repo.UploadApp("app-guid", "/foo/bar") - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Contains(t, apiResponse.Message, "/foo/bar") -} - -func TestUploadApp(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - dir = filepath.Join(dir, "../../fixtures/example-app") - err = os.Chmod(filepath.Join(dir, "Gemfile"), os.ModePerm) - - assert.NoError(t, err) - - _, apiResponse := testUploadApp(t, dir, defaultRequests) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestCreateUploadDirWithAZipFile(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - dir = filepath.Join(dir, "../../fixtures/example-app.zip") - - _, apiResponse := testUploadApp(t, dir, defaultRequests) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestUploadAppFailsWhilePushingBits(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - dir = filepath.Join(dir, "../../fixtures/example-app") - - requests := []testnet.TestRequest{ - matchResourceRequest, - uploadApplicationRequest, - createProgressEndpoint("running"), - createProgressEndpoint("failed"), - } - _, apiResponse := testUploadApp(t, dir, requests) - assert.False(t, apiResponse.IsSuccessful()) -} - -func testUploadApp(t *testing.T, dir string, requests []testnet.TestRequest) (app cf.Application, apiResponse net.ApiResponse) { - ts, handler := testnet.NewTLSServer(t, requests) - defer ts.Close() - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - gateway := net.NewCloudControllerGateway() - zipper := cf.ApplicationZipper{} - repo := NewCloudControllerApplicationBitsRepository(config, gateway, zipper) - - apiResponse = repo.UploadApp("my-cool-app-guid", dir) - assert.True(t, handler.AllRequestsCalled()) - - return -} diff --git a/src/cf/api/applications.go b/src/cf/api/applications.go deleted file mode 100644 index 61b9a2eaff6..00000000000 --- a/src/cf/api/applications.go +++ /dev/null @@ -1,249 +0,0 @@ -package api - -import ( - "bytes" - "cf" - "cf/configuration" - "cf/net" - "encoding/json" - "fmt" - "io" - "regexp" - "strings" -) - -type PaginatedApplicationResources struct { - Resources []ApplicationResource -} - -type ApplicationResource struct { - Resource - Entity ApplicationEntity -} - -func (resource ApplicationResource) ToFields() (app cf.ApplicationFields) { - app.Guid = resource.Metadata.Guid - app.Name = resource.Entity.Name - app.EnvironmentVars = resource.Entity.EnvironmentJson - app.State = strings.ToLower(resource.Entity.State) - app.InstanceCount = resource.Entity.Instances - app.Memory = uint64(resource.Entity.Memory) - - return -} - -func (resource ApplicationResource) ToModel() (app cf.Application) { - app.ApplicationFields = resource.ToFields() - - for _, routeResource := range resource.Entity.Routes { - app.Routes = append(app.Routes, routeResource.ToModel()) - } - return -} - -type ApplicationEntity struct { - Name string - State string - Instances int - Memory int - Routes []AppRouteResource - EnvironmentJson map[string]string `json:"environment_json"` -} - -type AppRouteResource struct { - Resource - Entity AppRouteEntity -} - -func (resource AppRouteResource) ToFields() (route cf.RouteFields) { - route.Guid = resource.Metadata.Guid - route.Host = resource.Entity.Host - return -} - -func (resource AppRouteResource) ToModel() (route cf.RouteSummary) { - route.RouteFields = resource.ToFields() - route.Domain.Guid = resource.Entity.Domain.Metadata.Guid - route.Domain.Name = resource.Entity.Domain.Entity.Name - return -} - -type AppRouteEntity struct { - Host string - Domain Resource -} - -type ApplicationRepository interface { - FindByName(name string) (app cf.Application, apiResponse net.ApiResponse) - SetEnv(appGuid string, envVars map[string]string) (apiResponse net.ApiResponse) - Create(name, buildpackUrl, stackGuid, command string, memory uint64, instances int) (createdApp cf.Application, apiResponse net.ApiResponse) - Delete(appGuid string) (apiResponse net.ApiResponse) - Rename(appGuid string, newName string) (apiResponse net.ApiResponse) - Scale(app cf.ApplicationFields) (apiResponse net.ApiResponse) - Start(appGuid string) (updatedApp cf.Application, apiResponse net.ApiResponse) - StartWithDifferentBuildpack(appGuid, buildpack string) (updatedApp cf.Application, apiResponse net.ApiResponse) - Stop(appGuid string) (updatedApp cf.Application, apiResponse net.ApiResponse) -} - -type CloudControllerApplicationRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerApplicationRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerApplicationRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerApplicationRepository) FindByName(name string) (app cf.Application, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s/apps?q=name%s&inline-relations-depth=1", repo.config.Target, repo.config.SpaceFields.Guid, "%3A"+name) - appResources := new(PaginatedApplicationResources) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, appResources) - if apiResponse.IsNotSuccessful() { - return - } - - if len(appResources.Resources) == 0 { - apiResponse = net.NewNotFoundApiResponse("%s %s not found", "App", name) - return - } - - res := appResources.Resources[0] - app = res.ToModel() - return -} -func (repo CloudControllerApplicationRepository) SetEnv(appGuid string, envVars map[string]string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s", repo.config.Target, appGuid) - - type setEnvReqBody struct { - EnvJson map[string]string `json:"environment_json"` - } - - body := setEnvReqBody{EnvJson: envVars} - - jsonBytes, err := json.Marshal(body) - if err != nil { - apiResponse = net.NewApiResponseWithError("Error creating json", err) - return - } - - apiResponse = repo.gateway.UpdateResource(path, repo.config.AccessToken, bytes.NewReader(jsonBytes)) - return -} - -func (repo CloudControllerApplicationRepository) Create(name, buildpackUrl, stackGuid, command string, memory uint64, instances int) (createdApp cf.Application, apiResponse net.ApiResponse) { - apiResponse = validateApplicationName(name) - if apiResponse.IsNotSuccessful() { - return - } - - path := fmt.Sprintf("%s/v2/apps", repo.config.Target) - data := fmt.Sprintf( - `{"space_guid":"%s","name":"%s","instances":%d,"buildpack":%s,"memory":%d,"stack_guid":%s,"command":%s}`, - repo.config.SpaceFields.Guid, - name, - instances, - stringOrNull(buildpackUrl), - memory, - stringOrNull(stackGuid), - stringOrNull(command), - ) - - resource := new(ApplicationResource) - apiResponse = repo.gateway.CreateResourceForResponse(path, repo.config.AccessToken, strings.NewReader(data), resource) - if apiResponse.IsNotSuccessful() { - return - } - - createdApp = resource.ToModel() - return -} - -func (repo CloudControllerApplicationRepository) Delete(appGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s?recursive=true", repo.config.Target, appGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} - -func (repo CloudControllerApplicationRepository) Rename(appGuid, newName string) (apiResponse net.ApiResponse) { - apiResponse = validateApplicationName(newName) - if apiResponse.IsNotSuccessful() { - return - } - - data := fmt.Sprintf(`{"name":"%s"}`, newName) - apiResponse = repo.updateApp(appGuid, strings.NewReader(data)) - return -} - -func (repo CloudControllerApplicationRepository) Scale(app cf.ApplicationFields) (apiResponse net.ApiResponse) { - values := map[string]interface{}{} - if app.DiskQuota > 0 { - values["disk_quota"] = app.DiskQuota - } - if app.InstanceCount > 0 { - values["instances"] = app.InstanceCount - } - if app.Memory > 0 { - values["memory"] = app.Memory - } - - bodyBytes, err := json.Marshal(values) - if err != nil { - return net.NewApiResponseWithError("Error generating body", err) - } - - apiResponse = repo.updateApp(app.Guid, bytes.NewReader(bodyBytes)) - return -} - -func (repo CloudControllerApplicationRepository) updateApp(appGuid string, body io.ReadSeeker) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s", repo.config.Target, appGuid) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, body) -} - -func validateApplicationName(name string) (apiResponse net.ApiResponse) { - reg := regexp.MustCompile("^[0-9a-zA-Z\\-_]*$") - if !reg.MatchString(name) { - apiResponse = net.NewApiResponseWithMessage("App name is invalid: name can only contain letters, numbers, underscores and hyphens") - } - - return -} - -func (repo CloudControllerApplicationRepository) Start(appGuid string) (updatedApp cf.Application, apiResponse net.ApiResponse) { - return repo.startOrStopApp(appGuid, map[string]interface{}{"state": "STARTED"}) -} - -func (repo CloudControllerApplicationRepository) StartWithDifferentBuildpack(appGuid, buildpack string) (updatedApp cf.Application, apiResponse net.ApiResponse) { - updates := map[string]interface{}{ - "state": "STARTED", - "buildpack": buildpack, - } - return repo.startOrStopApp(appGuid, updates) -} - -func (repo CloudControllerApplicationRepository) Stop(appGuid string) (updatedApp cf.Application, apiResponse net.ApiResponse) { - return repo.startOrStopApp(appGuid, map[string]interface{}{"state": "STOPPED"}) -} - -func (repo CloudControllerApplicationRepository) startOrStopApp(appGuid string, updates map[string]interface{}) (updatedApp cf.Application, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s?inline-relations-depth=2", repo.config.Target, appGuid) - - updates["console"] = true - - body, err := json.Marshal(updates) - if err != nil { - apiResponse = net.NewApiResponseWithError("Could not serialize app updates.", err) - return - } - - resource := new(ApplicationResource) - apiResponse = repo.gateway.UpdateResourceForResponse(path, repo.config.AccessToken, bytes.NewReader(body), resource) - if apiResponse.IsNotSuccessful() { - return - } - - updatedApp = resource.ToModel() - return -} diff --git a/src/cf/api/applications_test.go b/src/cf/api/applications_test.go deleted file mode 100644 index 0c12cee776a..00000000000 --- a/src/cf/api/applications_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var singleAppResponse = testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{ - "resources": [ - { - "metadata": { - "guid": "app1-guid" - }, - "entity": { - "name": "App1", - "environment_json": { - "foo": "bar", - "baz": "boom" - }, - "memory": 128, - "instances": 1, - "state": "STOPPED", - "routes": [ - { - "metadata": { - "guid": "app1-route-guid" - }, - "entity": { - "host": "app1", - "domain": { - "metadata": { - "guid": "domain1-guid" - }, - "entity": { - "name": "cfapps.io" - } - } - } - } - ] - } - } - ] -}`} - -var findAppRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/apps?q=name%3AApp1&inline-relations-depth=1", - Response: singleAppResponse, -}) - -func TestFindByName(t *testing.T) { - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{findAppRequest}) - defer ts.Close() - - app, apiResponse := repo.FindByName("App1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, app.Name, "App1") - assert.Equal(t, app.Guid, "app1-guid") - assert.Equal(t, app.Memory, uint64(128)) - assert.Equal(t, app.InstanceCount, 1) - assert.Equal(t, app.EnvironmentVars, map[string]string{"foo": "bar", "baz": "boom"}) - assert.Equal(t, app.Routes[0].Host, "app1") - assert.Equal(t, app.Routes[0].Domain.Name, "cfapps.io") -} - -func TestFindByNameWhenAppIsNotFound(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(findAppRequest) - request.Response = testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`} - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{request}) - defer ts.Close() - - _, apiResponse := repo.FindByName("App1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestSetEnv(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/app1-guid", - Matcher: testnet.RequestBodyMatcher(`{"environment_json":{"DATABASE_URL":"mysql://example.com/my-db"}}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{request}) - defer ts.Close() - - apiResponse := repo.SetEnv("app1-guid", map[string]string{"DATABASE_URL": "mysql://example.com/my-db"}) - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -var createApplicationResponse = ` -{ - "metadata": { - "guid": "my-cool-app-guid" - }, - "entity": { - "name": "my-cool-app" - } -}` - -var createApplicationRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/apps", - Matcher: testnet.RequestBodyMatcher(`{"space_guid":"my-space-guid","name":"my-cool-app","instances":3,"buildpack":"buildpack-url","memory":2048,"stack_guid":"some-stack-guid","command":"some-command"}`), - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: createApplicationResponse}, -}) - -func TestCreateApplication(t *testing.T) { - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{createApplicationRequest}) - defer ts.Close() - - createdApp, apiResponse := repo.Create("my-cool-app", "buildpack-url", "some-stack-guid", "some-command", 2048, 3) - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - app := cf.Application{} - app.Name = "my-cool-app" - app.Guid = "my-cool-app-guid" - assert.Equal(t, createdApp, app) -} - -func TestCreateApplicationWithoutBuildpackStackOrCommand(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/apps", - Matcher: testnet.RequestBodyMatcher(`{"space_guid":"my-space-guid","name":"my-cool-app","instances":1,"buildpack":null,"memory":128,"stack_guid":null,"command":null}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: createApplicationResponse}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{request}) - defer ts.Close() - - _, apiResponse := repo.Create("my-cool-app", "", "", "", 128, 1) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestCreateRejectsInproperNames(t *testing.T) { - baseRequest := testnet.TestRequest{ - Method: "POST", - Path: "/v2/apps", - Response: testnet.TestResponse{Status: http.StatusCreated, Body: "{}"}, - } - - requests := []testnet.TestRequest{ - baseRequest, - baseRequest, - } - - ts, _, repo := createAppRepo(t, requests) - defer ts.Close() - - createdApp, apiResponse := repo.Create("name with space", "", "", "", 0, 0) - assert.Equal(t, createdApp, cf.Application{}) - assert.Contains(t, apiResponse.Message, "App name is invalid") - - _, apiResponse = repo.Create("name-with-inv@lid-chars!", "", "", "", 0, 0) - assert.True(t, apiResponse.IsNotSuccessful()) - - _, apiResponse = repo.Create("Valid-Name", "", "", "", 0, 0) - assert.True(t, apiResponse.IsSuccessful()) - - _, apiResponse = repo.Create("name_with_numbers_2", "", "", "", 0, 0) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestDeleteApplication(t *testing.T) { - deleteApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/apps/my-cool-app-guid?recursive=true", - Response: testnet.TestResponse{Status: http.StatusOK, Body: ""}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{deleteApplicationRequest}) - defer ts.Close() - - apiResponse := repo.Delete("my-cool-app-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestRename(t *testing.T) { - renameApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-app-guid", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-new-app"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{renameApplicationRequest}) - defer ts.Close() - - apiResponse := repo.Rename("my-app-guid", "my-new-app") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func testScale(t *testing.T, app cf.ApplicationFields, expectedBody string) { - scaleApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-app-guid", - Matcher: testnet.RequestBodyMatcher(expectedBody), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{scaleApplicationRequest}) - defer ts.Close() - - apiResponse := repo.Scale(app) - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestScaleAll(t *testing.T) { - app := cf.ApplicationFields{} - app.Guid = "my-app-guid" - app.DiskQuota = 1024 - app.InstanceCount = 5 - app.Memory = 512 - - testScale(t, app, `{"disk_quota":1024,"instances":5,"memory":512}`) -} - -func TestScaleApplicationDiskQuota(t *testing.T) { - app := cf.ApplicationFields{} - app.Guid = "my-app-guid" - app.DiskQuota = 1024 - - testScale(t, app, `{"disk_quota":1024}`) -} - -func TestScaleApplicationInstances(t *testing.T) { - app := cf.ApplicationFields{} - app.Guid = "my-app-guid" - app.InstanceCount = 5 - - testScale(t, app, `{"instances":5}`) -} - -func TestScaleApplicationMemory(t *testing.T) { - app := cf.ApplicationFields{} - app.Guid = "my-app-guid" - app.Memory = 512 - - testScale(t, app, `{"memory":512}`) -} - -func TestStartApplication(t *testing.T) { - startApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-cool-app-guid?inline-relations-depth=2", - Matcher: testnet.RequestBodyMatcher(`{"console":true,"state":"STARTED"}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` -{ - "metadata": { - "guid": "my-updated-app-guid" - }, - "entity": { - "name": "cli1", - "state": "STARTED" - } -}`}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{startApplicationRequest}) - defer ts.Close() - - updatedApp, apiResponse := repo.Start("my-cool-app-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, "cli1", updatedApp.Name) - assert.Equal(t, "started", updatedApp.State) - assert.Equal(t, "my-updated-app-guid", updatedApp.Guid) -} - -func TestStopApplication(t *testing.T) { - stopApplicationRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-cool-app-guid?inline-relations-depth=2", - Matcher: testnet.RequestBodyMatcher(`{"console":true,"state":"STOPPED"}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` -{ - "metadata": { - "guid": "my-updated-app-guid" - }, - "entity": { - "name": "cli1", - "state": "STOPPED" - } -}`}, - }) - - ts, handler, repo := createAppRepo(t, []testnet.TestRequest{stopApplicationRequest}) - defer ts.Close() - - updatedApp, apiResponse := repo.Stop("my-cool-app-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, "cli1", updatedApp.Name) - assert.Equal(t, "stopped", updatedApp.State) - assert.Equal(t, "my-updated-app-guid", updatedApp.Guid) -} - -func createAppRepo(t *testing.T, requests []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ApplicationRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - SpaceFields: space, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerApplicationRepository(config, gateway) - return -} diff --git a/src/cf/api/authentication.go b/src/cf/api/authentication.go deleted file mode 100644 index 07ae024a7ad..00000000000 --- a/src/cf/api/authentication.go +++ /dev/null @@ -1,105 +0,0 @@ -package api - -import ( - "cf/configuration" - "cf/net" - "cf/terminal" - "encoding/base64" - "fmt" - "net/url" - "os" - "strings" -) - -type AuthenticationRepository interface { - Authenticate(email string, password string) (apiResponse net.ApiResponse) - RefreshAuthToken() (updatedToken string, apiResponse net.ApiResponse) -} - -type UAAAuthenticationRepository struct { - configRepo configuration.ConfigurationRepository - config *configuration.Configuration - gateway net.Gateway -} - -func NewUAAAuthenticationRepository(gateway net.Gateway, configRepo configuration.ConfigurationRepository) (uaa UAAAuthenticationRepository) { - uaa.gateway = gateway - uaa.configRepo = configRepo - uaa.config, _ = configRepo.Get() - return -} - -func (uaa UAAAuthenticationRepository) Authenticate(email string, password string) (apiResponse net.ApiResponse) { - data := url.Values{ - "username": {email}, - "password": {password}, - "grant_type": {"password"}, - "scope": {""}, - } - - apiResponse = uaa.getAuthToken(data) - if apiResponse.IsNotSuccessful() && apiResponse.StatusCode == 401 { - apiResponse.Message = "Password is incorrect, please try again." - } - return -} - -func (uaa UAAAuthenticationRepository) RefreshAuthToken() (updatedToken string, apiResponse net.ApiResponse) { - data := url.Values{ - "refresh_token": {uaa.config.RefreshToken}, - "grant_type": {"refresh_token"}, - "scope": {""}, - } - - apiResponse = uaa.getAuthToken(data) - updatedToken = uaa.config.AccessToken - - if apiResponse.IsError() { - fmt.Printf("%s\n\n", terminal.NotLoggedInText()) - os.Exit(1) - } - - return -} - -func (uaa UAAAuthenticationRepository) getAuthToken(data url.Values) (apiResponse net.ApiResponse) { - type uaaErrorResponse struct { - Code string `json:"error"` - Description string `json:"error_description"` - } - - type AuthenticationResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - RefreshToken string `json:"refresh_token"` - Error uaaErrorResponse `json:"error"` - } - - path := fmt.Sprintf("%s/oauth/token", uaa.config.AuthorizationEndpoint) - request, apiResponse := uaa.gateway.NewRequest("POST", path, "Basic "+base64.StdEncoding.EncodeToString([]byte("cf:")), strings.NewReader(data.Encode())) - if apiResponse.IsNotSuccessful() { - return - } - request.HttpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - response := new(AuthenticationResponse) - _, apiResponse = uaa.gateway.PerformRequestForJSONResponse(request, &response) - - if apiResponse.IsNotSuccessful() { - return - } - - if response.Error.Code != "" { - apiResponse = net.NewApiResponseWithMessage("Authentication Server error: %s", response.Error.Description) - return - } - - uaa.config.AccessToken = fmt.Sprintf("%s %s", response.TokenType, response.AccessToken) - uaa.config.RefreshToken = response.RefreshToken - err := uaa.configRepo.Save() - if err != nil { - apiResponse = net.NewApiResponseWithError("Error setting configuration", err) - } - - return -} diff --git a/src/cf/api/authentication_test.go b/src/cf/api/authentication_test.go deleted file mode 100644 index 04b3f0ef9b2..00000000000 --- a/src/cf/api/authentication_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package api - -import ( - "cf/net" - "encoding/base64" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testconfig "testhelpers/configuration" - testnet "testhelpers/net" - "testing" -) - -var authHeaders = http.Header{ - "accept": {"application/json"}, - "content-type": {"application/x-www-form-urlencoded"}, - "authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("cf:"))}, -} - -var successfulLoginRequest = testnet.TestRequest{ - Method: "POST", - Path: "/oauth/token", - Header: authHeaders, - Matcher: successfulLoginMatcher, - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{ - "access_token": "my_access_token", - "token_type": "BEARER", - "refresh_token": "my_refresh_token", - "scope": "openid", - "expires_in": 98765 -} `}, -} - -var successfulLoginMatcher = func(t *testing.T, request *http.Request) { - err := request.ParseForm() - if err != nil { - assert.Fail(t, "Failed to parse form: %s", err) - return - } - - assert.Equal(t, request.Form.Get("username"), "foo@example.com", "Username did not match.") - assert.Equal(t, request.Form.Get("password"), "bar", "Password did not match.") - assert.Equal(t, request.Form.Get("grant_type"), "password", "Grant type did not match.") - assert.Equal(t, request.Form.Get("scope"), "", "Scope did not mathc.") -} - -func TestSuccessfullyLoggingIn(t *testing.T) { - ts, handler, auth := setupAuthWithEndpoint(t, successfulLoginRequest) - defer ts.Close() - - apiResponse := auth.Authenticate("foo@example.com", "bar") - savedConfig := testconfig.SavedConfiguration - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.Equal(t, savedConfig.AuthorizationEndpoint, ts.URL) - assert.Equal(t, savedConfig.AccessToken, "BEARER my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") -} - -var unsuccessfulLoginRequest = testnet.TestRequest{ - Method: "POST", - Path: "/oauth/token", - Response: testnet.TestResponse{ - Status: http.StatusUnauthorized, - }, -} - -func TestUnsuccessfullyLoggingIn(t *testing.T) { - ts, handler, auth := setupAuthWithEndpoint(t, unsuccessfulLoginRequest) - defer ts.Close() - - apiResponse := auth.Authenticate("foo@example.com", "oops wrong pass") - savedConfig := testconfig.SavedConfiguration - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, apiResponse.Message, "Password is incorrect, please try again.") - assert.Empty(t, savedConfig.AccessToken) -} - -var errorLoginRequest = testnet.TestRequest{ - Method: "POST", - Path: "/oauth/token", - Response: testnet.TestResponse{ - Status: http.StatusInternalServerError, - }, -} - -func TestServerErrorLoggingIn(t *testing.T) { - ts, handler, auth := setupAuthWithEndpoint(t, errorLoginRequest) - defer ts.Close() - - apiResponse := auth.Authenticate("foo@example.com", "bar") - savedConfig := testconfig.SavedConfiguration - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsError()) - assert.Equal(t, apiResponse.Message, "Server error, status code: 500, error code: , message: ") - assert.Empty(t, savedConfig.AccessToken) -} - -var errorMaskedAsSuccessLoginRequest = testnet.TestRequest{ - Method: "POST", - Path: "/oauth/token", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{"error":{"error":"rest_client_error","error_description":"I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io"}} -`}, -} - -func TestLoggingInWithErrorMaskedAsSuccess(t *testing.T) { - ts, handler, auth := setupAuthWithEndpoint(t, errorMaskedAsSuccessLoginRequest) - defer ts.Close() - - apiResponse := auth.Authenticate("foo@example.com", "bar") - savedConfig := testconfig.SavedConfiguration - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsError()) - assert.Equal(t, apiResponse.Message, "Authentication Server error: I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io") - assert.Empty(t, savedConfig.AccessToken) -} - -func setupAuthWithEndpoint(t *testing.T, request testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, auth UAAAuthenticationRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{request}) - - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - config, err := configRepo.Get() - assert.NoError(t, err) - config.AuthorizationEndpoint = ts.URL - config.AccessToken = "" - - gateway := net.NewUAAGateway() - - auth = NewUAAAuthenticationRepository(gateway, configRepo) - return -} diff --git a/src/cf/api/buildpack_bits.go b/src/cf/api/buildpack_bits.go deleted file mode 100644 index 3a6d3599faf..00000000000 --- a/src/cf/api/buildpack_bits.go +++ /dev/null @@ -1,100 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fileutils" - "fmt" - "io" - "mime/multipart" - "os" -) - -type BuildpackBitsRepository interface { - UploadBuildpack(buildpack cf.Buildpack, dir string) (apiResponse net.ApiResponse) -} - -type CloudControllerBuildpackBitsRepository struct { - config *configuration.Configuration - gateway net.Gateway - zipper cf.Zipper -} - -func NewCloudControllerBuildpackBitsRepository(config *configuration.Configuration, gateway net.Gateway, zipper cf.Zipper) (repo CloudControllerBuildpackBitsRepository) { - repo.config = config - repo.gateway = gateway - repo.zipper = zipper - return -} - -func (repo CloudControllerBuildpackBitsRepository) UploadBuildpack(buildpack cf.Buildpack, dir string) (apiResponse net.ApiResponse) { - fileutils.TempFile("buildpack", func(zipFile *os.File, err error) { - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - return - } - err = repo.zipper.Zip(dir, zipFile) - if err != nil { - apiResponse = net.NewApiResponseWithError("Invalid buildpack", err) - return - } - apiResponse = repo.uploadBits(buildpack, zipFile) - if apiResponse.IsNotSuccessful() { - return - } - }) - return -} - -func (repo CloudControllerBuildpackBitsRepository) uploadBits(buildpack cf.Buildpack, zipFile *os.File) (apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/buildpacks/%s/bits", repo.config.Target, buildpack.Guid) - - fileutils.TempFile("requests", func(requestFile *os.File, err error) { - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - return - } - - boundary, err := repo.writeUploadBody(zipFile, requestFile) - if err != nil { - apiResponse = net.NewApiResponseWithError("Error creating upload", err) - return - } - - request, apiResponse := repo.gateway.NewRequest("PUT", url, repo.config.AccessToken, requestFile) - contentType := fmt.Sprintf("multipart/form-data; boundary=%s", boundary) - request.HttpReq.Header.Set("Content-Type", contentType) - if apiResponse.IsNotSuccessful() { - return - } - - apiResponse = repo.gateway.PerformRequest(request) - }) - - return -} - -func (repo CloudControllerBuildpackBitsRepository) writeUploadBody(zipFile *os.File, body *os.File) (boundary string, err error) { - writer := multipart.NewWriter(body) - defer writer.Close() - - boundary = writer.Boundary() - - zipStats, err := zipFile.Stat() - if err != nil { - return - } - - if zipStats.Size() == 0 { - return - } - - part, err := writer.CreateFormFile("buildpack", "buildpack.zip") - if err != nil { - return - } - - _, err = io.Copy(part, zipFile) - return -} diff --git a/src/cf/api/buildpack_bits_test.go b/src/cf/api/buildpack_bits_test.go deleted file mode 100644 index 42812796bd2..00000000000 --- a/src/cf/api/buildpack_bits_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package api - -import ( - "archive/zip" - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "os" - "path/filepath" - testnet "testhelpers/net" - "testing" -) - -var uploadBuildpackRequest = testnet.TestRequest{ - Method: "PUT", - Path: "/v2/buildpacks/my-cool-buildpack-guid/bits", - Matcher: uploadBuildpackBodyMatcher, - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: ` -{ - "metadata":{ - "guid": "my-job-guid" - } -} - `}, -} - -var expectedBuildpackContent = []string{"detect", "compile", "package"} - -var uploadBuildpackBodyMatcher = func(t *testing.T, request *http.Request) { - err := request.ParseMultipartForm(4096) - if err != nil { - assert.Fail(t, "Failed parsing multipart form: %s", err) - return - } - defer request.MultipartForm.RemoveAll() - - assert.Equal(t, len(request.MultipartForm.Value), 0, "Should have 0 values") - assert.Equal(t, len(request.MultipartForm.File), 1, "Wrong number of files") - - files, ok := request.MultipartForm.File["buildpack"] - - assert.True(t, ok, "Buildpack file part not present") - assert.Equal(t, len(files), 1, "Wrong number of files") - - buildpackFile := files[0] - assert.Equal(t, buildpackFile.Filename, "buildpack.zip", "Wrong file name") - - file, err := buildpackFile.Open() - if err != nil { - assert.Fail(t, "Cannot get multipart file: %s", err.Error()) - return - } - - zipReader, err := zip.NewReader(file, 4096) - if err != nil { - assert.Fail(t, "Error reading zip content: %s", err.Error()) - } - - assert.Equal(t, len(zipReader.File), 3, "Wrong number of files in zip") - assert.Equal(t, zipReader.File[1].Mode(), uint32(os.ModePerm)) - -nextFile: - for _, f := range zipReader.File { - for _, expected := range expectedBuildpackContent { - if f.Name == expected { - continue nextFile - } - } - assert.Fail(t, "Missing file: "+f.Name) - } -} - -var defaultBuildpackRequests = []testnet.TestRequest{ - uploadBuildpackRequest, -} - -func TestUploadBuildpackWithInvalidDirectory(t *testing.T) { - config := &configuration.Configuration{} - gateway := net.NewCloudControllerGateway() - - repo := NewCloudControllerBuildpackBitsRepository(config, gateway, cf.ApplicationZipper{}) - buildpack := cf.Buildpack{} - - apiResponse := repo.UploadBuildpack(buildpack, "/foo/bar") - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Contains(t, apiResponse.Message, "Invalid buildpack") -} - -func TestUploadBuildpack(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - dir = filepath.Join(dir, "../../fixtures/example-buildpack") - err = os.Chmod(filepath.Join(dir, "detect"), os.ModePerm) - assert.NoError(t, err) - - _, apiResponse := testUploadBuildpack(t, dir, defaultBuildpackRequests) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestUploadBuildpackWithAZipFile(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - dir = filepath.Join(dir, "../../fixtures/example-buildpack.zip") - - _, apiResponse := testUploadBuildpack(t, dir, defaultBuildpackRequests) - assert.True(t, apiResponse.IsSuccessful()) -} - -func testUploadBuildpack(t *testing.T, dir string, requests []testnet.TestRequest) (buildpack cf.Buildpack, apiResponse net.ApiResponse) { - ts, handler := testnet.NewTLSServer(t, requests) - defer ts.Close() - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - gateway := net.NewCloudControllerGateway() - repo := NewCloudControllerBuildpackBitsRepository(config, gateway, cf.ApplicationZipper{}) - buildpack = cf.Buildpack{} - buildpack.Name = "my-cool-buildpack" - buildpack.Guid = "my-cool-buildpack-guid" - - apiResponse = repo.UploadBuildpack(buildpack, dir) - assert.True(t, handler.AllRequestsCalled()) - return -} diff --git a/src/cf/api/buildpacks.go b/src/cf/api/buildpacks.go deleted file mode 100644 index 43442ad8205..00000000000 --- a/src/cf/api/buildpacks.go +++ /dev/null @@ -1,172 +0,0 @@ -package api - -import ( - "bytes" - "cf" - "cf/configuration" - "cf/net" - "encoding/json" - "fmt" - "net/url" -) - -const ( - buildpacks_path = "/v2/buildpacks" -) - -type PaginatedBuildpackResources struct { - Resources []BuildpackResource - NextUrl string `json:"next_url"` -} - -type BuildpackResource struct { - Resource - Entity BuildpackEntity -} - -type BuildpackEntity struct { - Name string `json:"name"` - Position *int `json:"position,omitempty"` -} - -type BuildpackRepository interface { - FindByName(name string) (buildpack cf.Buildpack, apiResponse net.ApiResponse) - ListBuildpacks(stop chan bool) (buildpacksChan chan []cf.Buildpack, statusChan chan net.ApiResponse) - Create(name string, position *int) (createdBuildpack cf.Buildpack, apiResponse net.ApiResponse) - Delete(buildpackGuid string) (apiResponse net.ApiResponse) - Update(buildpack cf.Buildpack) (updatedBuildpack cf.Buildpack, apiResponse net.ApiResponse) -} - -type CloudControllerBuildpackRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerBuildpackRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerBuildpackRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerBuildpackRepository) ListBuildpacks(stop chan bool) (buildpacksChan chan []cf.Buildpack, statusChan chan net.ApiResponse) { - buildpacksChan = make(chan []cf.Buildpack, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := buildpacks_path - - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - buildpacks []cf.Buildpack - apiResponse net.ApiResponse - ) - buildpacks, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(buildpacksChan) - close(statusChan) - return - } - - if len(buildpacks) > 0 { - buildpacksChan <- buildpacks - } - } - } - close(buildpacksChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerBuildpackRepository) FindByName(name string) (buildpack cf.Buildpack, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s?q=name%%3A%s", buildpacks_path, url.QueryEscape(name)) - buildpacks, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(buildpacks) == 0 { - apiResponse = net.NewNotFoundApiResponse("%s %s not found", "Buildpack", name) - return - } - - buildpack = buildpacks[0] - return -} - -func (repo CloudControllerBuildpackRepository) findNextWithPath(path string) (buildpacks []cf.Buildpack, nextUrl string, apiResponse net.ApiResponse) { - response := new(PaginatedBuildpackResources) - - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, response) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = response.NextUrl - - for _, r := range response.Resources { - buildpacks = append(buildpacks, unmarshallBuildpack(r)) - } - - return -} - -func (repo CloudControllerBuildpackRepository) Create(name string, position *int) (createdBuildpack cf.Buildpack, apiResponse net.ApiResponse) { - path := repo.config.Target + buildpacks_path - entity := BuildpackEntity{Name: name, Position: position} - body, err := json.Marshal(entity) - if err != nil { - apiResponse = net.NewApiResponseWithError("Could not serialize information", err) - return - } - - resource := new(BuildpackResource) - apiResponse = repo.gateway.CreateResourceForResponse(path, repo.config.AccessToken, bytes.NewReader(body), resource) - if apiResponse.IsNotSuccessful() { - return - } - - createdBuildpack = unmarshallBuildpack(*resource) - return -} - -func (repo CloudControllerBuildpackRepository) Delete(buildpackGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s%s/%s", repo.config.Target, buildpacks_path, buildpackGuid) - apiResponse = repo.gateway.DeleteResource(path, repo.config.AccessToken) - return -} - -func (repo CloudControllerBuildpackRepository) Update(buildpack cf.Buildpack) (updatedBuildpack cf.Buildpack, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s%s/%s", repo.config.Target, buildpacks_path, buildpack.Guid) - - entity := BuildpackEntity{buildpack.Name, buildpack.Position} - body, err := json.Marshal(entity) - if err != nil { - apiResponse = net.NewApiResponseWithError("Could not serialize updates.", err) - return - } - - resource := new(BuildpackResource) - apiResponse = repo.gateway.UpdateResourceForResponse(path, repo.config.AccessToken, bytes.NewReader(body), resource) - if apiResponse.IsNotSuccessful() { - return - } - - updatedBuildpack = unmarshallBuildpack(*resource) - return -} - -func unmarshallBuildpack(resource BuildpackResource) (buildpack cf.Buildpack) { - buildpack.Guid = resource.Metadata.Guid - buildpack.Name = resource.Entity.Name - buildpack.Position = resource.Entity.Position - return -} diff --git a/src/cf/api/buildpacks_test.go b/src/cf/api/buildpacks_test.go deleted file mode 100644 index ef8243a2c18..00000000000 --- a/src/cf/api/buildpacks_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestBuildpacksListBuildpacks(t *testing.T) { - firstRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/buildpacks", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "next_url": "/v2/buildpacks?page=2", - "resources": [ - { - "metadata": { - "guid": "buildpack1-guid" - }, - "entity": { - "name": "Buildpack1", - "position" : 1 - } - } - ] - }`}, - }) - - secondRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/buildpacks?page=2", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "resources": [ - { - "metadata": { - "guid": "buildpack2-guid" - }, - "entity": { - "name": "Buildpack2", - "position" : 2 - } - } - ] - }`}, - }) - - ts, handler, repo := createBuildpackRepo(t, firstRequest, secondRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - buildpacksChan, statusChan := repo.ListBuildpacks(stopChan) - - one := 1 - buildpack := cf.Buildpack{} - buildpack.Guid = "buildpack1-guid" - buildpack.Name = "Buildpack1" - buildpack.Position = &one - - two := 2 - buildpack2 := cf.Buildpack{} - buildpack2.Guid = "buildpack2-guid" - buildpack2.Name = "Buildpack2" - buildpack2.Position = &two - - expectedBuildpacks := []cf.Buildpack{buildpack, buildpack2} - - buildpacks := []cf.Buildpack{} - for chunk := range buildpacksChan { - buildpacks = append(buildpacks, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, buildpacks, expectedBuildpacks) - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestBuildpacksListBuildpacksWithNoBuildpacks(t *testing.T) { - emptyBuildpacksRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/buildpacks", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": []}`, - }, - }) - - ts, handler, repo := createBuildpackRepo(t, emptyBuildpacksRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - buildpacksChan, statusChan := repo.ListBuildpacks(stopChan) - - _, ok := <-buildpacksChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -var singleBuildpackResponse = testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": [ - { - "metadata": { - "guid": "buildpack1-guid" - }, - "entity": { - "name": "Buildpack1", - "position": 10 - } - } - ] - }`} - -var findBuildpackRequest = testnet.TestRequest{ - Method: "GET", - Path: "/v2/buildpacks?q=name%3ABuildpack1", - Response: singleBuildpackResponse, -} - -func TestBuildpacksFindByName(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(findBuildpackRequest) - - ts, handler, repo := createBuildpackRepo(t, req) - defer ts.Close() - existingBuildpack := cf.Buildpack{} - existingBuildpack.Guid = "buildpack1-guid" - existingBuildpack.Name = "Buildpack1" - - buildpack, apiResponse := repo.FindByName("Buildpack1") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, buildpack.Name, existingBuildpack.Name) - assert.Equal(t, buildpack.Guid, existingBuildpack.Guid) - assert.Equal(t, *buildpack.Position, 10) -} - -func TestFindByNameWhenBuildpackIsNotFound(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(findBuildpackRequest) - req.Response = testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`} - - ts, handler, repo := createBuildpackRepo(t, req) - defer ts.Close() - - _, apiResponse := repo.FindByName("Buildpack1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestBuildpackCreateRejectsImproperNames(t *testing.T) { - badRequest := testnet.TestRequest{ - Method: "POST", - Path: "/v2/buildpacks", - Response: testnet.TestResponse{ - Status: http.StatusBadRequest, - Body: `{ - "code":290003, - "description":"Buildpack is invalid: [\"name name can only contain alphanumeric characters\"]", - "error_code":"CF-BuildpackInvalid" - }`, - }} - - ts, _, repo := createBuildpackRepo(t, badRequest) - defer ts.Close() - one := 1 - createdBuildpack, apiResponse := repo.Create("name with space", &one) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, createdBuildpack, cf.Buildpack{}) - assert.Equal(t, apiResponse.ErrorCode, "290003") - assert.Contains(t, apiResponse.Message, "Buildpack is invalid") -} - -func TestCreateBuildpackWithPosition(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/buildpacks", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","position":999}`), - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: `{ - "metadata": { - "guid": "my-cool-buildpack-guid" - }, - "entity": { - "name": "my-cool-buildpack", - "position":999 - } - }`}, - }) - - ts, handler, repo := createBuildpackRepo(t, req) - defer ts.Close() - - position := 999 - created, apiResponse := repo.Create("my-cool-buildpack", &position) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.NotNil(t, created.Guid) - assert.Equal(t, "my-cool-buildpack", created.Name) - assert.Equal(t, 999, *created.Position) -} - -func TestDeleteBuildpack(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/buildpacks/my-cool-buildpack-guid", - Response: testnet.TestResponse{ - Status: http.StatusNoContent, - }}) - - ts, handler, repo := createBuildpackRepo(t, req) - defer ts.Close() - - apiResponse := repo.Delete("my-cool-buildpack-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestUpdateBuildpack(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/buildpacks/my-cool-buildpack-guid", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-cool-buildpack","position":555}`), - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: `{ - - "metadata": { - "guid": "my-cool-buildpack-guid" - }, - "entity": { - "name": "my-cool-buildpack", - "position":555 - } - }`}, - }) - - ts, handler, repo := createBuildpackRepo(t, req) - defer ts.Close() - - position := 555 - buildpack := cf.Buildpack{} - buildpack.Name = "my-cool-buildpack" - buildpack.Guid = "my-cool-buildpack-guid" - buildpack.Position = &position - updated, apiResponse := repo.Update(buildpack) - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - - assert.Equal(t, buildpack, updated) -} - -func createBuildpackRepo(t *testing.T, requests ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo BuildpackRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - SpaceFields: space, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerBuildpackRepository(config, gateway) - return -} diff --git a/src/cf/api/domains.go b/src/cf/api/domains.go deleted file mode 100644 index 9ee3170c3a1..00000000000 --- a/src/cf/api/domains.go +++ /dev/null @@ -1,232 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedDomainResources struct { - NextUrl string `json:"next_url"` - Resources []DomainResource -} - -type DomainResource struct { - Resource - Entity DomainEntity -} - -func (resource DomainResource) ToFields() (fields cf.DomainFields) { - fields.Name = resource.Entity.Name - fields.Guid = resource.Metadata.Guid - fields.OwningOrganizationGuid = resource.Entity.OwningOrganizationGuid - fields.Shared = fields.OwningOrganizationGuid == "" - return -} - -func (resource DomainResource) ToModel() (domain cf.Domain) { - domain.DomainFields = resource.ToFields() - - for _, spaceResource := range resource.Entity.Spaces { - domain.Spaces = append(domain.Spaces, spaceResource.ToFields()) - } - - return -} - -type DomainEntity struct { - Name string - OwningOrganizationGuid string `json:"owning_organization_guid"` - Spaces []SpaceResource -} - -type DomainRepository interface { - FindDefaultAppDomain() (domain cf.Domain, apiResponse net.ApiResponse) - ListDomainsForOrg(orgGuid string, stop chan bool) (domainsChan chan []cf.Domain, statusChan chan net.ApiResponse) - FindByName(name string) (domain cf.Domain, apiResponse net.ApiResponse) - FindByNameInCurrentSpace(name string) (domain cf.Domain, apiResponse net.ApiResponse) - FindByNameInOrg(name string, owningOrgGuid string) (domain cf.Domain, apiResponse net.ApiResponse) - Create(domainName string, owningOrgGuid string) (createdDomain cf.DomainFields, apiResponse net.ApiResponse) - CreateSharedDomain(domainName string) (apiResponse net.ApiResponse) - Delete(domainGuid string) (apiResponse net.ApiResponse) - Map(domainGuid string, spaceGuid string) (apiResponse net.ApiResponse) - Unmap(domainGuid string, spaceGuid string) (apiResponse net.ApiResponse) -} - -type CloudControllerDomainRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerDomainRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerDomainRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerDomainRepository) FindDefaultAppDomain() (domain cf.Domain, apiResponse net.ApiResponse) { - sharedDomains, _, apiResponse := repo.findNextWithPath("/v2/domains?inline-relations-depth=1") - if apiResponse.IsNotSuccessful() { - return - } - - if len(sharedDomains) > 0 { - domain = sharedDomains[0] - } else { - apiResponse = net.NewNotFoundApiResponse("No default domain exists") - } - - return -} - -func (repo CloudControllerDomainRepository) ListDomainsForOrg(orgGuid string, stop chan bool) (domainsChan chan []cf.Domain, statusChan chan net.ApiResponse) { - domainsChan = make(chan []cf.Domain, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := "/v2/domains?inline-relations-depth=1" - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - allDomains []cf.Domain - domainsToReturn []cf.Domain - apiResponse net.ApiResponse - ) - - allDomains, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(domainsChan) - close(statusChan) - return - } - - for _, d := range allDomains { - if repo.isOrgDomain(orgGuid, d.DomainFields) { - domainsToReturn = append(domainsToReturn, d) - } - } - - if len(domainsToReturn) > 0 { - domainsChan <- domainsToReturn - } - } - } - close(domainsChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerDomainRepository) isOrgDomain(orgGuid string, domain cf.DomainFields) bool { - return orgGuid == domain.OwningOrganizationGuid || domain.Shared -} - -func (repo CloudControllerDomainRepository) findNextWithPath(path string) (domains []cf.Domain, nextUrl string, apiResponse net.ApiResponse) { - domainResources := new(PaginatedDomainResources) - - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, domainResources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = domainResources.NextUrl - for _, r := range domainResources.Resources { - domains = append(domains, r.ToModel()) - } - - return -} - -func (repo CloudControllerDomainRepository) FindByName(name string) (domain cf.Domain, apiResponse net.ApiResponse) { - path := fmt.Sprintf("/v2/domains?inline-relations-depth=1&q=name%%3A%s", name) - domains, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(domains) > 0 { - domain = domains[0] - } else { - apiResponse = net.NewNotFoundApiResponse("Domain %s not found", name) - } - return -} - -func (repo CloudControllerDomainRepository) FindByNameInCurrentSpace(name string) (domain cf.Domain, apiResponse net.ApiResponse) { - spacePath := fmt.Sprintf("/v2/spaces/%s/domains?inline-relations-depth=1&q=name%%3A%s", repo.config.SpaceFields.Guid, name) - return repo.findOneWithPaths(spacePath, name) -} - -func (repo CloudControllerDomainRepository) FindByNameInOrg(name string, orgGuid string) (domain cf.Domain, apiResponse net.ApiResponse) { - orgPath := fmt.Sprintf("/v2/organizations/%s/domains?inline-relations-depth=1&q=name%%3A%s", orgGuid, name) - return repo.findOneWithPaths(orgPath, name) -} - -func (repo CloudControllerDomainRepository) findOneWithPaths(scopedPath, name string) (domain cf.Domain, apiResponse net.ApiResponse) { - domains, _, apiResponse := repo.findNextWithPath(scopedPath) - if apiResponse.IsNotSuccessful() { - return - } - - if len(domains) == 0 { - sharedPath := fmt.Sprintf("/v2/domains?inline-relations-depth=1&q=name%%3A%s", name) - domains, _, apiResponse = repo.findNextWithPath(sharedPath) - if apiResponse.IsNotSuccessful() { - return - } - - if len(domains) == 0 || !domains[0].Shared { - apiResponse = net.NewNotFoundApiResponse("Domain %s not found", name) - return - } - } - - domain = domains[0] - return -} - -func (repo CloudControllerDomainRepository) Create(domainName string, owningOrgGuid string) (createdDomain cf.DomainFields, apiResponse net.ApiResponse) { - path := repo.config.Target + "/v2/domains" - data := fmt.Sprintf( - `{"name":"%s","wildcard":true,"owning_organization_guid":"%s"}`, domainName, owningOrgGuid, - ) - - resource := new(DomainResource) - apiResponse = repo.gateway.CreateResourceForResponse(path, repo.config.AccessToken, strings.NewReader(data), resource) - if apiResponse.IsNotSuccessful() { - return - } - - createdDomain = resource.ToFields() - return -} - -func (repo CloudControllerDomainRepository) CreateSharedDomain(domainName string) (apiResponse net.ApiResponse) { - path := repo.config.Target + "/v2/domains" - data := fmt.Sprintf(`{"name":"%s","wildcard":true}`, domainName) - return repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(data)) -} - -func (repo CloudControllerDomainRepository) Delete(domainGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/domains/%s?recursive=true", repo.config.Target, domainGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} - -func (repo CloudControllerDomainRepository) Map(domainGuid string, spaceGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s/domains/%s", repo.config.Target, spaceGuid, domainGuid) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, nil) -} - -func (repo CloudControllerDomainRepository) Unmap(domainGuid string, spaceGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s/domains/%s", repo.config.Target, spaceGuid, domainGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} diff --git a/src/cf/api/domains_test.go b/src/cf/api/domains_test.go deleted file mode 100644 index c32ee6185b8..00000000000 --- a/src/cf/api/domains_test.go +++ /dev/null @@ -1,562 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var firstPageDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ - "next_url": "/v2/domains?inline-relations-depth=1&page=2", - "resources": [ - { - "metadata": { - "guid": "domain1-guid" - }, - "entity": { - "name": "example.com", - "owning_organization_guid": "my-org-guid", - "wildcard": true, - "spaces": [ - { - "metadata": { "guid": "my-space-guid" }, - "entity": { "name": "my-space" } - } - ] - } - }, - { - "metadata": { - "guid": "domain2-guid" - }, - "entity": { - "name": "some-shared.example.com", - "owning_organization_guid": null, - "wildcard": true, - "spaces": [ - { - "metadata": { "guid": "my-space-guid" }, - "entity": { "name": "my-space" } - } - ] - } - } - ]}`}, -}) - -var secondPageDomainsRequest = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1&page=2", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { - "guid": "not-in-my-org-domain-guid" - }, - "entity": { - "name": "example.com", - "owning_organization_guid": "not-my-org-guid", - "wildcard": true, - "spaces": [] - } - }, - { - "metadata": { - "guid": "domain3-guid" - }, - "entity": { - "name": "example.com", - "owning_organization_guid": "my-org-guid", - "wildcard": true, - "spaces": [ - { - "metadata": { "guid": "my-space-guid" }, - "entity": { "name": "my-space" } - } - ] - } - } - ]}`}, -}) - -func TestDomainListDomainsForOrg(t *testing.T) { - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{firstPageDomainsRequest, secondPageDomainsRequest}) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - domainsChan, statusChan := repo.ListDomainsForOrg("my-org-guid", stopChan) - - domains := []cf.Domain{} - for chunk := range domainsChan { - domains = append(domains, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, len(domains), 3) - assert.Equal(t, domains[0].Guid, "domain1-guid") - assert.Equal(t, domains[1].Guid, "domain2-guid") - assert.Equal(t, domains[2].Guid, "domain3-guid") - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) - -} - -func TestDomainListDomainsForOrgWithNoDomains(t *testing.T) { - emptyDomainsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [] }`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{emptyDomainsRequest}) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - domainsChan, statusChan := repo.ListDomainsForOrg("my-org-guid", stopChan) - - domains := []cf.Domain{} - for chunk := range domainsChan { - domains = append(domains, chunk...) - } - - _, ok := <-domainsChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestDomainFindDefault(t *testing.T) { - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "shared-domain-guid" }, - "entity": { - "name": "shared-domain.cf-app.com", - "owning_organization_guid": null - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{sharedDomainsReq}) - defer ts.Close() - - domain, apiResponse := repo.FindDefaultAppDomain() - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, domain.Name, "shared-domain.cf-app.com") - assert.Equal(t, domain.Guid, "shared-domain-guid") -} - -func TestDomainFindByName(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "domain2-guid" }, - "entity": { "name": "domain2.cf-app.com" } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - domain, apiResponse := repo.FindByName("domain2.cf-app.com") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, domain.Name, "domain2.cf-app.com") - assert.Equal(t, domain.Guid, "domain2-guid") -} - -func TestDomainFindByNameInCurrentSpace(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "domain2-guid" }, - "entity": { "name": "domain2.cf-app.com" } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - domain, apiResponse := repo.FindByNameInCurrentSpace("domain2.cf-app.com") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, domain.Name, "domain2.cf-app.com") - assert.Equal(t, domain.Guid, "domain2-guid") -} - -func TestDomainFindByNameInCurrentSpaceWhenNotFound(t *testing.T) { - spaceDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{spaceDomainsReq, sharedDomainsReq}) - defer ts.Close() - - _, apiResponse := repo.FindByNameInCurrentSpace("domain2.cf-app.com") - assert.True(t, handler.AllRequestsCalled()) - - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestDomainFindByNameInCurrentSpaceWhenFoundAsSharedDomain(t *testing.T) { - spaceDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "shared-domain-guid" }, - "entity": { - "name": "shared-domain.cf-app.com", - "owning_organization_guid": null - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{spaceDomainsReq, sharedDomainsReq}) - defer ts.Close() - - domain, apiResponse := repo.FindByNameInCurrentSpace("domain2.cf-app.com") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, domain.Name, "shared-domain.cf-app.com") - assert.Equal(t, domain.Guid, "shared-domain-guid") -} - -func TestDomainFindByNameInCurrentSpaceWhenFoundInDomainsButNotShared(t *testing.T) { - spaceDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "some-domain-guid" }, - "entity": { - "name": "some.cf-app.com", - "owning_organization_guid": "some-org-guid" - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{spaceDomainsReq, sharedDomainsReq}) - defer ts.Close() - - _, apiResponse := repo.FindByNameInCurrentSpace("domain2.cf-app.com") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestDomainFindByNameInOrg(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "my-domain-guid" }, - "entity": { - "name": "my-example.com", - "owning_organization_guid": "my-org-guid", - "wildcard": true, - "spaces": [ - { - "metadata": { "guid": "my-space-guid" }, - "entity": { "name": "my-space" } - } - ] - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - domain, apiResponse := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - - assert.Equal(t, domain.Name, "my-example.com") - assert.Equal(t, domain.Guid, "my-domain-guid") - assert.False(t, domain.Shared) - assert.Equal(t, domain.Spaces[0].Name, "my-space") -} - -func TestDomainFindByNameInOrgWhenNotFoundOnBothEndpoints(t *testing.T) { - orgDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{orgDomainsReq, sharedDomainsReq}) - defer ts.Close() - - _, apiResponse := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestDomainFindByNameInOrgWhenFoundAsSharedDomain(t *testing.T) { - orgDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "shared-domain-guid" }, - "entity": { - "name": "shared-example.com", - "owning_organization_guid": null, - "wildcard": true, - "spaces": [] - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{orgDomainsReq, sharedDomainsReq}) - defer ts.Close() - - domain, apiResponse := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - - assert.Equal(t, domain.Name, "shared-example.com") - assert.Equal(t, domain.Guid, "shared-domain-guid") - assert.True(t, domain.Shared) -} - -func TestDomainFindByNameInOrgWhenFoundInDomainsButNotShared(t *testing.T) { - orgDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - sharedDomainsReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "shared-domain-guid" }, - "entity": { - "name": "shared-example.com", - "owning_organization_guid": "some-other-org-guid", - "wildcard": true, - "spaces": [] - } - } - ]}`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{orgDomainsReq, sharedDomainsReq}) - defer ts.Close() - - _, apiResponse := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestCreateDomain(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/domains", - Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","wildcard":true,"owning_organization_guid":"org-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: `{ - "metadata": { "guid": "abc-123" }, - "entity": { "name": "example.com" } - }`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - createdDomain, apiResponse := repo.Create("example.com", "org-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, createdDomain.Guid, "abc-123") -} - -func TestShareDomain(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/domains", - Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","wildcard":true}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` { - "metadata": { "guid": "abc-123" }, - "entity": { "name": "example.com" } - }`}, - }) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.CreateSharedDomain("example.com") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func deleteDomainReq(statusCode int) testnet.TestRequest { - return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/domains/my-domain-guid?recursive=true", - Response: testnet.TestResponse{Status: statusCode}, - }) -} - -func TestDeleteDomainSuccess(t *testing.T) { - req := deleteDomainReq(http.StatusOK) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.Delete("my-domain-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestDeleteDomainFailure(t *testing.T) { - req := deleteDomainReq(http.StatusBadRequest) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.Delete("my-domain-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) -} - -func mapDomainReq(statusCode int) testnet.TestRequest { - return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/spaces/my-space-guid/domains/my-domain-guid", - Response: testnet.TestResponse{Status: statusCode}, - }) -} - -func TestMapDomainSuccess(t *testing.T) { - req := mapDomainReq(http.StatusOK) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.Map("my-domain-guid", "my-space-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestMapDomainWhenServerError(t *testing.T) { - req := mapDomainReq(http.StatusBadRequest) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.Map("my-domain-guid", "my-space-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) -} - -func unmapDomainReq(statusCode int) testnet.TestRequest { - return testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/spaces/my-space-guid/domains/my-domain-guid", - Response: testnet.TestResponse{Status: statusCode}, - }) -} - -func TestUnmapDomainSuccess(t *testing.T) { - req := unmapDomainReq(http.StatusOK) - - ts, handler, repo := createDomainRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.Unmap("my-domain-guid", "my-space-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createDomainRepo(t *testing.T, reqs []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo DomainRepository) { - ts, handler = testnet.NewTLSServer(t, reqs) - org := cf.OrganizationFields{} - org.Guid = "my-org-guid" - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - SpaceFields: space, - OrganizationFields: org, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerDomainRepository(config, gateway) - return -} diff --git a/src/cf/api/endpoints.go b/src/cf/api/endpoints.go deleted file mode 100644 index 2a0c31445f3..00000000000 --- a/src/cf/api/endpoints.go +++ /dev/null @@ -1,139 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "regexp" - "strings" -) - -const ( - authEndpointPrefix = "login" - uaaEndpointPrefix = "uaa" -) - -type EndpointRepository interface { - UpdateEndpoint(endpoint string) (finalEndpoint string, apiResponse net.ApiResponse) - GetEndpoint(name cf.EndpointType) (endpoint string, apiResponse net.ApiResponse) -} - -type RemoteEndpointRepository struct { - config *configuration.Configuration - gateway net.Gateway - configRepo configuration.ConfigurationRepository -} - -func NewEndpointRepository(config *configuration.Configuration, gateway net.Gateway, configRepo configuration.ConfigurationRepository) (repo RemoteEndpointRepository) { - repo.config = config - repo.gateway = gateway - repo.configRepo = configRepo - return -} - -func (repo RemoteEndpointRepository) UpdateEndpoint(endpoint string) (finalEndpoint string, apiResponse net.ApiResponse) { - endpointMissingScheme := !strings.HasPrefix(endpoint, "https://") && !strings.HasPrefix(endpoint, "http://") - - if endpointMissingScheme { - finalEndpoint = "https://" + endpoint - apiResponse = repo.doUpdateEndpoint(finalEndpoint) - - if apiResponse.IsNotSuccessful() { - finalEndpoint = "http://" + endpoint - apiResponse = repo.doUpdateEndpoint(finalEndpoint) - } - return - } - - finalEndpoint = endpoint - - apiResponse = repo.doUpdateEndpoint(finalEndpoint) - - return -} - -func (repo RemoteEndpointRepository) doUpdateEndpoint(endpoint string) (apiResponse net.ApiResponse) { - request, apiResponse := repo.gateway.NewRequest("GET", endpoint+"/v2/info", "", nil) - if apiResponse.IsNotSuccessful() { - return - } - - type infoResponse struct { - ApiVersion string `json:"api_version"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - } - - serverResponse := new(infoResponse) - _, apiResponse = repo.gateway.PerformRequestForJSONResponse(request, &serverResponse) - if apiResponse.IsNotSuccessful() { - return - } - - if endpoint != repo.config.Target { - repo.configRepo.ClearSession() - } - - repo.config.Target = endpoint - repo.config.ApiVersion = serverResponse.ApiVersion - repo.config.AuthorizationEndpoint = serverResponse.AuthorizationEndpoint - - err := repo.configRepo.Save() - if err != nil { - apiResponse = net.NewApiResponseWithMessage(err.Error()) - } - return -} - -func (repo RemoteEndpointRepository) GetEndpoint(name cf.EndpointType) (endpoint string, apiResponse net.ApiResponse) { - switch name { - case cf.CloudControllerEndpointKey: - return repo.cloudControllerEndpoint() - case cf.UaaEndpointKey: - return repo.uaaControllerEndpoint() - case cf.LoggregatorEndpointKey: - return repo.loggregatorEndpoint() - } - - apiResponse = net.NewNotFoundApiResponse("Endpoint type %s is unkown", string(name)) - - return -} - -func (repo RemoteEndpointRepository) cloudControllerEndpoint() (endpoint string, apiResponse net.ApiResponse) { - if repo.config.Target == "" { - apiResponse = net.NewApiResponseWithMessage("Endpoint missing from config file") - return - } - - endpoint = repo.config.Target - return -} - -func (repo RemoteEndpointRepository) uaaControllerEndpoint() (endpoint string, apiResponse net.ApiResponse) { - if repo.config.AuthorizationEndpoint == "" { - apiResponse = net.NewApiResponseWithMessage("Endpoint missing from config file") - return - } - - endpoint = strings.Replace(repo.config.AuthorizationEndpoint, authEndpointPrefix, uaaEndpointPrefix, 1) - - return -} - -func (repo RemoteEndpointRepository) loggregatorEndpoint() (endpoint string, apiResponse net.ApiResponse) { - if repo.config.Target == "" { - apiResponse = net.NewApiResponseWithMessage("Endpoint missing from config file") - return - } - - re := regexp.MustCompile(`^http(s?)://[^\.]+\.(.+)\/?`) - - endpoint = re.ReplaceAllString(repo.config.Target, "ws${1}://loggregator.${2}") - - if endpoint[0:3] == "wss" { - endpoint = endpoint + ":4443" - } else { - endpoint = endpoint + ":80" - } - return -} diff --git a/src/cf/api/endpoints_test.go b/src/cf/api/endpoints_test.go deleted file mode 100644 index 5e741e233e1..00000000000 --- a/src/cf/api/endpoints_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "strings" - testconfig "testhelpers/configuration" - "testing" -) - -var validApiInfoEndpoint = func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v2/info" { - w.WriteHeader(http.StatusNotFound) - return - } - - infoResponse := ` -{ - "name": "vcap", - "build": "2222", - "support": "http://support.cloudfoundry.com", - "version": 2, - "description": "Cloud Foundry sponsored by Pivotal", - "authorization_endpoint": "https://login.example.com", - "api_version": "42.0.0" -} ` - fmt.Fprintln(w, infoResponse) -} - -func TestUpdateEndpointWhenUrlIsValidHttpsInfoEndpoint(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - configRepo.Login() - - ts, repo := createEndpointRepoForUpdate(configRepo, validApiInfoEndpoint) - defer ts.Close() - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - - config, _ := configRepo.Get() - config.OrganizationFields = org - config.SpaceFields = space - - repo.UpdateEndpoint(ts.URL) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.AccessToken, "") - assert.Equal(t, savedConfig.AuthorizationEndpoint, "https://login.example.com") - assert.Equal(t, savedConfig.Target, ts.URL) - assert.Equal(t, savedConfig.ApiVersion, "42.0.0") - assert.False(t, savedConfig.HasOrganization()) - assert.False(t, savedConfig.HasSpace()) -} - -func TestUpdateEndpointWhenUrlIsAlreadyTargeted(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - configRepo.Login() - - ts, repo := createEndpointRepoForUpdate(configRepo, validApiInfoEndpoint) - defer ts.Close() - - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - - config, _ := configRepo.Get() - config.Target = ts.URL - config.AccessToken = "some access token" - config.RefreshToken = "some refresh token" - config.OrganizationFields = org - config.SpaceFields = space - - repo.UpdateEndpoint(ts.URL) - - assert.Equal(t, config.OrganizationFields, org) - assert.Equal(t, config.SpaceFields, space) - assert.Equal(t, config.AccessToken, "some access token") - assert.Equal(t, config.RefreshToken, "some refresh token") -} - -func TestUpdateEndpointWhenUrlIsMissingSchemeAndHttpsEndpointExists(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - configRepo.Login() - - ts, repo := createEndpointRepoForUpdate(configRepo, validApiInfoEndpoint) - defer ts.Close() - - schemelessURL := strings.Replace(ts.URL, "https://", "", 1) - endpoint, apiResponse := repo.UpdateEndpoint(schemelessURL) - assert.Equal(t, "https://"+schemelessURL, endpoint) - - assert.True(t, apiResponse.IsSuccessful()) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.AccessToken, "") - assert.Equal(t, savedConfig.AuthorizationEndpoint, "https://login.example.com") - assert.Equal(t, savedConfig.Target, ts.URL) - assert.Equal(t, savedConfig.ApiVersion, "42.0.0") -} - -func TestUpdateEndpointWhenUrlIsMissingSchemeAndHttpEndpointExists(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - configRepo.Login() - - ts, repo := createInsecureEndpointRepoForUpdate(configRepo, validApiInfoEndpoint) - defer ts.Close() - - schemelessURL := strings.Replace(ts.URL, "http://", "", 1) - - endpoint, apiResponse := repo.UpdateEndpoint(schemelessURL) - assert.Equal(t, "http://"+schemelessURL, endpoint) - - assert.True(t, apiResponse.IsSuccessful()) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.AccessToken, "") - assert.Equal(t, savedConfig.AuthorizationEndpoint, "https://login.example.com") - assert.Equal(t, savedConfig.Target, ts.URL) - assert.Equal(t, savedConfig.ApiVersion, "42.0.0") -} - -var notFoundApiEndpoint = func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) -} - -func TestUpdateEndpointWhenEndpointReturns404(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Login() - - ts, repo := createEndpointRepoForUpdate(configRepo, notFoundApiEndpoint) - defer ts.Close() - - _, apiResponse := repo.UpdateEndpoint(ts.URL) - - assert.True(t, apiResponse.IsNotSuccessful()) -} - -var invalidJsonResponseApiEndpoint = func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `Foo`) -} - -func TestUpdateEndpointWhenEndpointReturnsInvalidJson(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Login() - - ts, repo := createEndpointRepoForUpdate(configRepo, invalidJsonResponseApiEndpoint) - defer ts.Close() - - _, apiResponse := repo.UpdateEndpoint(ts.URL) - - assert.True(t, apiResponse.IsNotSuccessful()) -} - -func createEndpointRepoForUpdate(configRepo testconfig.FakeConfigRepository, endpoint func(w http.ResponseWriter, r *http.Request)) (ts *httptest.Server, repo EndpointRepository) { - if endpoint != nil { - ts = httptest.NewTLSServer(http.HandlerFunc(endpoint)) - } - return ts, makeRepo(configRepo) -} - -func createInsecureEndpointRepoForUpdate(configRepo testconfig.FakeConfigRepository, endpoint func(w http.ResponseWriter, r *http.Request)) (ts *httptest.Server, repo EndpointRepository) { - if endpoint != nil { - ts = httptest.NewServer(http.HandlerFunc(endpoint)) - } - return ts, makeRepo(configRepo) -} - -func makeRepo(configRepo testconfig.FakeConfigRepository) (repo EndpointRepository) { - config, _ := configRepo.Get() - gateway := net.NewCloudControllerGateway() - return NewEndpointRepository(config, gateway, configRepo) -} - -func TestGetEndpointForCloudController(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - config := &configuration.Configuration{ - Target: "http://api.example.com", - } - - repo := NewEndpointRepository(config, net.NewCloudControllerGateway(), configRepo) - - endpoint, apiResponse := repo.GetEndpoint(cf.CloudControllerEndpointKey) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, endpoint, "http://api.example.com") -} - -func TestGetEndpointForLoggregatorSecure(t *testing.T) { - config := &configuration.Configuration{ - Target: "https://foo.run.pivotal.io", - } - - repo := createEndpointRepoForGet(config) - - endpoint, apiResponse := repo.GetEndpoint(cf.LoggregatorEndpointKey) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, endpoint, "wss://loggregator.run.pivotal.io:4443") -} - -func TestGetEndpointForLoggregatorInsecure(t *testing.T) { - - config := &configuration.Configuration{ - Target: "http://bar.run.pivotal.io", - } - - repo := createEndpointRepoForGet(config) - - endpoint, apiResponse := repo.GetEndpoint(cf.LoggregatorEndpointKey) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, endpoint, "ws://loggregator.run.pivotal.io:80") -} - -func createEndpointRepoForGet(config *configuration.Configuration) (repo EndpointRepository) { - configRepo := testconfig.FakeConfigRepository{} - repo = NewEndpointRepository(config, net.NewCloudControllerGateway(), configRepo) - return -} diff --git a/src/cf/api/helpers.go b/src/cf/api/helpers.go deleted file mode 100644 index 6cf11425a5d..00000000000 --- a/src/cf/api/helpers.go +++ /dev/null @@ -1,11 +0,0 @@ -package api - -import "fmt" - -func stringOrNull(s string) string { - if s == "" { - return "null" - } - - return fmt.Sprintf(`"%s"`, s) -} diff --git a/src/cf/api/log_message_queue.go b/src/cf/api/log_message_queue.go deleted file mode 100644 index ddf4709b6b0..00000000000 --- a/src/cf/api/log_message_queue.go +++ /dev/null @@ -1,65 +0,0 @@ -package api - -import ( - "github.com/cloudfoundry/loggregatorlib/logmessage" - "time" -) - -const MAX_INT64 int64 = 1<<63 - 1 - -type Item struct { - message *logmessage.Message - timestampWhenOutputtable int64 - index int -} - -type SortedMessageQueue struct { - items []*Item - printTimeBuffer time.Duration -} - -func (pq *SortedMessageQueue) PushMessage(message *logmessage.Message) { - item := &Item{message: message, timestampWhenOutputtable: time.Now().Add(pq.printTimeBuffer).UnixNano()} - pq.items = append(pq.items, item) - pq.insertionSort() -} - -func (pq *SortedMessageQueue) PopMessage() *logmessage.Message { - if len(pq.items) == 0 { - return nil - } - - var item *Item - item = pq.items[0] - pq.items = pq.items[1:len(pq.items)] - - return item.message -} - -func (pq *SortedMessageQueue) NextTimestamp() int64 { - currentQueue := pq.items - n := len(currentQueue) - if n == 0 { - return MAX_INT64 - } - item := currentQueue[0] - return item.timestampWhenOutputtable -} - -func (pq SortedMessageQueue) less(i, j int) bool { - return *pq.items[i].message.GetLogMessage().Timestamp < *pq.items[j].message.GetLogMessage().Timestamp -} - -func (pq SortedMessageQueue) swap(i, j int) { - pq.items[i], pq.items[j] = pq.items[j], pq.items[i] - pq.items[i].index = i - pq.items[j].index = j -} - -func (pq SortedMessageQueue) insertionSort() { - for i := 0 + 1; i < len(pq.items); i++ { - for j := i; j > 0 && pq.less(j, j-1); j-- { - pq.swap(j, j-1) - } - } -} diff --git a/src/cf/api/log_message_queue_test.go b/src/cf/api/log_message_queue_test.go deleted file mode 100644 index 750bbdd9fbc..00000000000 --- a/src/cf/api/log_message_queue_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package api - -import ( - "code.google.com/p/gogoprotobuf/proto" - "fmt" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/stretchr/testify/assert" - "math/rand" - "testing" - "time" -) - -func TestPriorityQueue(t *testing.T) { - pq := newSortedMessageQueue(10 * time.Millisecond) - - msg3 := logMessageWithTime(t, "message 3", int64(130)) - pq.PushMessage(msg3) - msg2 := logMessageWithTime(t, "message 2", int64(120)) - pq.PushMessage(msg2) - msg4 := logMessageWithTime(t, "message 4", int64(140)) - pq.PushMessage(msg4) - msg1 := logMessageWithTime(t, "message 1", int64(110)) - pq.PushMessage(msg1) - - assert.Equal(t, getMsgString(pq.PopMessage()), getMsgString(msg1)) - assert.Equal(t, getMsgString(pq.PopMessage()), getMsgString(msg2)) - assert.Equal(t, getMsgString(pq.PopMessage()), getMsgString(msg3)) - assert.Equal(t, getMsgString(pq.PopMessage()), getMsgString(msg4)) -} - -func TestPopOnEmptyQueue(t *testing.T) { - pq := newSortedMessageQueue(10 * time.Millisecond) - - var msg *logmessage.Message - msg = nil - assert.Equal(t, pq.PopMessage(), msg) -} - -func TestNextTimestamp(t *testing.T) { - pq := newSortedMessageQueue(5 * time.Second) - - assert.Equal(t, pq.NextTimestamp(), MAX_INT64) - - msg2 := logMessageWithTime(t, "message 2", int64(130)) - pq.PushMessage(msg2) - timeNowWhenInsertingMessage1 := time.Now() - - time.Sleep(50 * time.Millisecond) - - msg1 := logMessageWithTime(t, "message 1", int64(100)) - pq.PushMessage(msg1) - - allowedDelta := (20 * time.Microsecond).Nanoseconds() - - timeWhenOutputtable := time.Now().Add(5 * time.Second).UnixNano() - assert.True(t, pq.NextTimestamp()-timeWhenOutputtable < allowedDelta) - assert.True(t, pq.NextTimestamp()-timeWhenOutputtable > -allowedDelta) - - pq.PopMessage() - - timeWhenOutputtable = timeNowWhenInsertingMessage1.Add(5 * time.Second).UnixNano() - assert.True(t, pq.NextTimestamp()-timeWhenOutputtable < allowedDelta) - assert.True(t, pq.NextTimestamp()-timeWhenOutputtable > -allowedDelta) -} - -func TestStableSort(t *testing.T) { - pq := newSortedMessageQueue(10 * time.Millisecond) - - msg1 := logMessageWithTime(t, "message first", int64(109)) - pq.PushMessage(msg1) - - for i := 1; i < 1000; i++ { - msg := logMessageWithTime(t, fmt.Sprintf("message %s", i), int64(110)) - pq.PushMessage(msg) - } - msg2 := logMessageWithTime(t, "message last", int64(111)) - pq.PushMessage(msg2) - - assert.Equal(t, getMsgString(pq.PopMessage()), "message first") - - for i := 1; i < 1000; i++ { - assert.Equal(t, getMsgString(pq.PopMessage()), fmt.Sprintf("message %s", i)) - } - - assert.Equal(t, getMsgString(pq.PopMessage()), "message last") -} - -func BenchmarkPushMessages(b *testing.B) { - r := rand.New(rand.NewSource(99)) - pq := newSortedMessageQueue(10 * time.Millisecond) - for i := 0; i < b.N; i++ { - msg := logMessageForBenchmark(b, fmt.Sprintf("message %s", i), r.Int63()) - pq.PushMessage(msg) - } -} - -func logMessageWithTime(t *testing.T, messageString string, timestamp int64) *logmessage.Message { - data, err := proto.Marshal(generateMessage(messageString, timestamp)) - assert.NoError(t, err) - message, err := logmessage.ParseMessage(data) - assert.NoError(t, err) - - return message -} - -func logMessageForBenchmark(b *testing.B, messageString string, timestamp int64) *logmessage.Message { - data, _ := proto.Marshal(generateMessage(messageString, timestamp)) - message, _ := logmessage.ParseMessage(data) - return message -} - -func generateMessage(messageString string, timestamp int64) *logmessage.LogMessage { - messageType := logmessage.LogMessage_OUT - sourceType := logmessage.LogMessage_DEA - return &logmessage.LogMessage{ - Message: []byte(messageString), - AppId: proto.String("my-app-guid"), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(timestamp), - } -} - -func getMsgString(message *logmessage.Message) string { - return string(message.GetLogMessage().GetMessage()) -} - -func newSortedMessageQueue(printTimeBuffer time.Duration) *SortedMessageQueue { - return &SortedMessageQueue{printTimeBuffer: printTimeBuffer} -} diff --git a/src/cf/api/logs.go b/src/cf/api/logs.go deleted file mode 100644 index 9f3110b322f..00000000000 --- a/src/cf/api/logs.go +++ /dev/null @@ -1,156 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/terminal" - "cf/trace" - "code.google.com/p/go.net/websocket" - "crypto/tls" - "errors" - "fmt" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "time" -) - -const LogBufferSize = 1024 - -type LogsRepository interface { - RecentLogsFor(appGuid string, onConnect func(), logChan chan *logmessage.Message) (err error) - TailLogsFor(appGuid string, onConnect func(), logChan chan *logmessage.Message, stopLoggingChan chan bool, printInterval time.Duration) (err error) -} - -type LoggregatorLogsRepository struct { - config *configuration.Configuration - endpointRepo EndpointRepository -} - -func NewLoggregatorLogsRepository(config *configuration.Configuration, endpointRepo EndpointRepository) (repo LoggregatorLogsRepository) { - repo.config = config - repo.endpointRepo = endpointRepo - return -} - -func (repo LoggregatorLogsRepository) RecentLogsFor(appGuid string, onConnect func(), logChan chan *logmessage.Message) (err error) { - host, apiResponse := repo.endpointRepo.GetEndpoint(cf.LoggregatorEndpointKey) - if apiResponse.IsNotSuccessful() { - err = errors.New(apiResponse.Message) - return - } - - location := host + fmt.Sprintf("/dump/?app=%s", appGuid) - stopLoggingChan := make(chan bool) - defer close(stopLoggingChan) - - return repo.connectToWebsocket(location, onConnect, logChan, stopLoggingChan, 0*time.Nanosecond) -} - -func (repo LoggregatorLogsRepository) TailLogsFor(appGuid string, onConnect func(), logChan chan *logmessage.Message, stopLoggingChan chan bool, printTimeBuffer time.Duration) error { - host, apiResponse := repo.endpointRepo.GetEndpoint(cf.LoggregatorEndpointKey) - if apiResponse.IsNotSuccessful() { - return errors.New(apiResponse.Message) - } - location := host + fmt.Sprintf("/tail/?app=%s", appGuid) - return repo.connectToWebsocket(location, onConnect, logChan, stopLoggingChan, printTimeBuffer) -} - -func (repo LoggregatorLogsRepository) connectToWebsocket(location string, onConnect func(), outputChan chan *logmessage.Message, stopLoggingChan chan bool, printTimeBuffer time.Duration) (err error) { - trace.Logger.Printf("\n%s %s\n", terminal.HeaderColor("CONNECTING TO WEBSOCKET:"), location) - - config, err := websocket.NewConfig(location, "http://localhost") - if err != nil { - return - } - - config.Header.Add("Authorization", repo.config.AccessToken) - config.TlsConfig = &tls.Config{InsecureSkipVerify: true} - - ws, err := websocket.DialConfig(config) - if err != nil { - return - } - defer ws.Close() - - onConnect() - - inputChan := make(chan *logmessage.Message, LogBufferSize) - defer close(inputChan) - stopInputChan := make(chan bool, 1) - - messageQueue := repo.createMessageSorter(inputChan, printTimeBuffer) - - go repo.sendKeepAlive(ws) - go func() { - defer close(stopInputChan) - repo.listenForMessages(ws, inputChan, stopInputChan) - }() - - return repo.makeAndStartMessageSorter(messageQueue, outputChan, stopLoggingChan, stopInputChan) -} - -func (repo LoggregatorLogsRepository) createMessageSorter(inputChan <-chan *logmessage.Message, printTimeBuffer time.Duration) (messageQueue *SortedMessageQueue) { - messageQueue = &SortedMessageQueue{printTimeBuffer: printTimeBuffer} - go func() { - for msg := range inputChan { - messageQueue.PushMessage(msg) - } - }() - return -} - -func (repo LoggregatorLogsRepository) makeAndStartMessageSorter(messageQueue *SortedMessageQueue, outputChan chan *logmessage.Message, stopLoggingChan <-chan bool, stopInputChan <-chan bool) (err error) { - flushLastMessages := func() { - for { - msg := messageQueue.PopMessage() - if msg == nil { - break - } - outputChan <- msg - } - } - -OutputLoop: - for { - select { - case <-stopInputChan: - flushLastMessages() - break OutputLoop - case <-stopLoggingChan: - flushLastMessages() - break OutputLoop - case <-time.After(10 * time.Millisecond): - for messageQueue.NextTimestamp() < time.Now().UnixNano() { - msg := messageQueue.PopMessage() - outputChan <- msg - } - } - } - return -} - -func (repo LoggregatorLogsRepository) sendKeepAlive(ws *websocket.Conn) { - for { - websocket.Message.Send(ws, "I'm alive!") - time.Sleep(25 * time.Second) - } -} - -func (repo LoggregatorLogsRepository) listenForMessages(ws *websocket.Conn, msgChan chan<- *logmessage.Message, stopInputChan chan<- bool) { - defer func() { - stopInputChan <- true - }() - - for { - var data []byte - err := websocket.Message.Receive(ws, &data) - if err != nil { - break - } - - msg, msgErr := logmessage.ParseMessage(data) - if msgErr != nil { - continue - } - msgChan <- msg - } -} diff --git a/src/cf/api/logs_test.go b/src/cf/api/logs_test.go deleted file mode 100644 index 7be92af706e..00000000000 --- a/src/cf/api/logs_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "code.google.com/p/go.net/websocket" - "code.google.com/p/gogoprotobuf/proto" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/stretchr/testify/assert" - "net/http/httptest" - "strings" - testapi "testhelpers/api" - "testing" - "time" -) - -func TestRecentLogsFor(t *testing.T) { - - messagesSent := [][]byte{ - marshalledLogMessageWithTime(t, "My message", int64(3000)), - } - - websocketEndpoint := func(conn *websocket.Conn) { - request := conn.Request() - assert.Equal(t, request.URL.Path, "/dump/") - assert.Equal(t, request.URL.RawQuery, "app=my-app-guid") - assert.Equal(t, request.Method, "GET") - assert.Contains(t, request.Header.Get("Authorization"), "BEARER my_access_token") - - for _, msg := range messagesSent { - conn.Write(msg) - } - time.Sleep(time.Duration(2) * time.Second) - conn.Close() - } - websocketServer := httptest.NewTLSServer(websocket.Handler(websocketEndpoint)) - defer websocketServer.Close() - - expectedMessage, err := logmessage.ParseMessage(messagesSent[0]) - assert.NoError(t, err) - - config := &configuration.Configuration{AccessToken: "BEARER my_access_token", Target: "https://localhost"} - - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.LoggregatorEndpointKey: strings.Replace(websocketServer.URL, "https", "wss", 1), - }} - - logsRepo := NewLoggregatorLogsRepository(config, endpointRepo) - - connected := false - onConnect := func() { - connected = true - } - - logChan := make(chan *logmessage.Message, 1000) - - err = logsRepo.RecentLogsFor("my-app-guid", onConnect, logChan) - close(logChan) - - dumpedMessages := []*logmessage.Message{} - for msg := range logChan { - dumpedMessages = append(dumpedMessages, msg) - } - - assert.NoError(t, err) - - assert.Equal(t, len(dumpedMessages), 1) - assert.Equal(t, dumpedMessages[0].GetShortSourceTypeName(), expectedMessage.GetShortSourceTypeName()) - assert.Equal(t, dumpedMessages[0].GetLogMessage().GetMessage(), expectedMessage.GetLogMessage().GetMessage()) - assert.Equal(t, dumpedMessages[0].GetLogMessage().GetMessageType(), expectedMessage.GetLogMessage().GetMessageType()) -} - -func TestTailsLogsFor(t *testing.T) { - - messagesSent := [][]byte{ - marshalledLogMessageWithTime(t, "My message 3", int64(300000)), - marshalledLogMessageWithTime(t, "My message 1", int64(100000)), - marshalledLogMessageWithTime(t, "My message 2", int64(200000)), - } - - websocketEndpoint := func(conn *websocket.Conn) { - request := conn.Request() - assert.Equal(t, request.URL.Path, "/tail/") - assert.Equal(t, request.URL.RawQuery, "app=my-app-guid") - assert.Equal(t, request.Method, "GET") - assert.Contains(t, request.Header.Get("Authorization"), "BEARER my_access_token") - - for _, msg := range messagesSent { - conn.Write(msg) - } - time.Sleep(time.Duration(200) * time.Millisecond) - conn.Close() - } - websocketServer := httptest.NewTLSServer(websocket.Handler(websocketEndpoint)) - defer websocketServer.Close() - - config := &configuration.Configuration{AccessToken: "BEARER my_access_token", Target: "https://localhost"} - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.LoggregatorEndpointKey: strings.Replace(websocketServer.URL, "https", "wss", 1), - }} - - logsRepo := NewLoggregatorLogsRepository(config, endpointRepo) - - connected := false - onConnect := func() { - connected = true - } - - tailedMessages := []*logmessage.Message{} - - logChan := make(chan *logmessage.Message, 1000) - - controlChan := make(chan bool) - - logsRepo.TailLogsFor("my-app-guid", onConnect, logChan, controlChan, time.Duration(1)) - close(logChan) - - for msg := range logChan { - tailedMessages = append(tailedMessages, msg) - } - - assert.True(t, connected) - - assert.Equal(t, len(tailedMessages), 3) - - tailedMessage := tailedMessages[0] - actualMessage, err := proto.Marshal(tailedMessage.GetLogMessage()) - assert.NoError(t, err) - assert.Equal(t, actualMessage, messagesSent[1]) - - tailedMessage = tailedMessages[1] - actualMessage, err = proto.Marshal(tailedMessage.GetLogMessage()) - assert.NoError(t, err) - assert.Equal(t, actualMessage, messagesSent[2]) - - tailedMessage = tailedMessages[2] - actualMessage, err = proto.Marshal(tailedMessage.GetLogMessage()) - assert.NoError(t, err) - assert.Equal(t, actualMessage, messagesSent[0]) -} - -func TestMessageOutputTimesDuringNormalFlow(t *testing.T) { - - startTime := time.Now() - messagesSent := [][]byte{ - marshalledLogMessageWithTime(t, "My message 1", startTime.Add(-9*time.Second).UnixNano()), - marshalledLogMessageWithTime(t, "My message 2", startTime.Add(-2*time.Second).UnixNano()), - marshalledLogMessageWithTime(t, "My message 3", startTime.Add(-1*time.Second).UnixNano()), - } - - websocketEndpoint := func(conn *websocket.Conn) { - request := conn.Request() - assert.Equal(t, request.URL.Path, "/tail/") - assert.Equal(t, request.URL.RawQuery, "app=my-app-guid") - assert.Equal(t, request.Method, "GET") - assert.Contains(t, request.Header.Get("Authorization"), "BEARER my_access_token") - - for _, msg := range messagesSent { - conn.Write(msg) - time.Sleep(200 * time.Millisecond) - } - time.Sleep(1 * time.Second) - conn.Close() - } - websocketServer := httptest.NewTLSServer(websocket.Handler(websocketEndpoint)) - defer websocketServer.Close() - - config := &configuration.Configuration{AccessToken: "BEARER my_access_token", Target: "https://localhost"} - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.LoggregatorEndpointKey: strings.Replace(websocketServer.URL, "https", "wss", 1), - }} - - logsRepo := NewLoggregatorLogsRepository(config, endpointRepo) - - logChan := make(chan *logmessage.Message, 1000) - controlChan := make(chan bool) - - go func() { - defer close(logChan) - logsRepo.TailLogsFor("my-app-guid", func() {}, logChan, controlChan, time.Duration(1*time.Second)) - }() - - for msg := range logChan { - - timeWhenOutputtable := startTime.Add(1 * time.Second).UnixNano() - timeNow := time.Now().UnixNano() - - switch string(msg.GetLogMessage().Message) { - case "My message 1": - assert.True(t, (timeNow-timeWhenOutputtable) < (50*time.Millisecond).Nanoseconds()) - assert.True(t, (timeNow-timeWhenOutputtable) > (10*time.Millisecond).Nanoseconds()) - case "My message 2": - assert.True(t, (timeNow-timeWhenOutputtable) < (250*time.Millisecond).Nanoseconds()) - assert.True(t, (timeNow-timeWhenOutputtable) > (200*time.Millisecond).Nanoseconds()) - case "My message 3": - assert.True(t, (timeNow-timeWhenOutputtable) < (450*time.Millisecond).Nanoseconds()) - assert.True(t, (timeNow-timeWhenOutputtable) > (400*time.Millisecond).Nanoseconds()) - } - } -} - -func TestMessageOutputWhenFlushingAfterServerDeath(t *testing.T) { - - startTime := time.Now() - messagesSent := [][]byte{ - marshalledLogMessageWithTime(t, "My message 1", startTime.Add(-9*time.Second).UnixNano()), - marshalledLogMessageWithTime(t, "My message 2", startTime.Add(-2*time.Second).UnixNano()), - marshalledLogMessageWithTime(t, "My message 3", startTime.Add(-1*time.Second).UnixNano()), - } - - websocketEndpoint := func(conn *websocket.Conn) { - request := conn.Request() - assert.Equal(t, request.URL.Path, "/tail/") - assert.Equal(t, request.URL.RawQuery, "app=my-app-guid") - assert.Equal(t, request.Method, "GET") - assert.Contains(t, request.Header.Get("Authorization"), "BEARER my_access_token") - - for _, msg := range messagesSent { - conn.Write(msg) - time.Sleep(200 * time.Millisecond) - } - conn.Close() - } - websocketServer := httptest.NewTLSServer(websocket.Handler(websocketEndpoint)) - defer websocketServer.Close() - - config := &configuration.Configuration{AccessToken: "BEARER my_access_token", Target: "https://localhost"} - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.LoggregatorEndpointKey: strings.Replace(websocketServer.URL, "https", "wss", 1), - }} - - logsRepo := NewLoggregatorLogsRepository(config, endpointRepo) - - firstMessageTime := time.Now().Add(-10 * time.Second).UnixNano() - - logChan := make(chan *logmessage.Message, 1000) - controlChan := make(chan bool) - - go func() { - defer close(logChan) - logsRepo.TailLogsFor("my-app-guid", func() {}, logChan, controlChan, time.Duration(1*time.Second)) - }() - - for msg := range logChan { - switch string(msg.GetLogMessage().Message) { - case "My message 1": - firstMessageTime = time.Now().UnixNano() - case "My message 2": - timeNow := time.Now().UnixNano() - delta := timeNow - firstMessageTime - assert.True(t, delta < (5*time.Millisecond).Nanoseconds()) - assert.True(t, delta >= 0) - case "My message 3": - timeNow := time.Now().UnixNano() - delta := timeNow - firstMessageTime - assert.True(t, delta < (5*time.Millisecond).Nanoseconds()) - assert.True(t, delta >= 0) - } - } -} - -func marshalledLogMessageWithTime(t *testing.T, messageString string, timestamp int64) []byte { - messageType := logmessage.LogMessage_OUT - sourceType := logmessage.LogMessage_DEA - protoMessage := &logmessage.LogMessage{ - Message: []byte(messageString), - AppId: proto.String("my-app-guid"), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(timestamp), - } - - message, err := proto.Marshal(protoMessage) - assert.NoError(t, err) - - return message -} diff --git a/src/cf/api/organizations.go b/src/cf/api/organizations.go deleted file mode 100644 index 77a17aa27f5..00000000000 --- a/src/cf/api/organizations.go +++ /dev/null @@ -1,156 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedOrganizationResources struct { - Resources []OrganizationResource - NextUrl string `json:"next_url"` -} - -type OrganizationResource struct { - Resource - Entity OrganizationEntity -} - -func (resource OrganizationResource) ToFields() (fields cf.OrganizationFields) { - fields.Name = resource.Entity.Name - fields.Guid = resource.Metadata.Guid - return -} - -func (resource OrganizationResource) ToModel() (org cf.Organization) { - org.OrganizationFields = resource.ToFields() - - spaces := []cf.SpaceFields{} - for _, s := range resource.Entity.Spaces { - spaces = append(spaces, s.ToFields()) - } - org.Spaces = spaces - - domains := []cf.DomainFields{} - for _, d := range resource.Entity.Domains { - domains = append(domains, d.ToFields()) - } - org.Domains = domains - - return -} - -type OrganizationEntity struct { - Name string - Spaces []SpaceResource - Domains []DomainResource -} - -type OrganizationRepository interface { - ListOrgs(stop chan bool) (orgsChan chan []cf.Organization, statusChan chan net.ApiResponse) - FindByName(name string) (org cf.Organization, apiResponse net.ApiResponse) - Create(name string) (apiResponse net.ApiResponse) - Rename(orgGuid string, name string) (apiResponse net.ApiResponse) - Delete(orgGuid string) (apiResponse net.ApiResponse) -} - -type CloudControllerOrganizationRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerOrganizationRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerOrganizationRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerOrganizationRepository) ListOrgs(stop chan bool) (orgsChan chan []cf.Organization, statusChan chan net.ApiResponse) { - orgsChan = make(chan []cf.Organization, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := "/v2/organizations" - - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - organizations []cf.Organization - apiResponse net.ApiResponse - ) - organizations, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(orgsChan) - close(statusChan) - return - } - - if len(organizations) > 0 { - orgsChan <- organizations - } - } - } - close(orgsChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerOrganizationRepository) findNextWithPath(path string) (orgs []cf.Organization, nextUrl string, apiResponse net.ApiResponse) { - orgResources := new(PaginatedOrganizationResources) - - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, orgResources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = orgResources.NextUrl - - for _, r := range orgResources.Resources { - orgs = append(orgs, r.ToModel()) - } - return -} - -func (repo CloudControllerOrganizationRepository) FindByName(name string) (org cf.Organization, apiResponse net.ApiResponse) { - path := fmt.Sprintf("/v2/organizations?q=name%s&inline-relations-depth=1", "%3A"+strings.ToLower(name)) - - orgs, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(orgs) == 0 { - apiResponse = net.NewNotFoundApiResponse("Org %s not found", name) - return - } - - org = orgs[0] - return -} - -func (repo CloudControllerOrganizationRepository) Create(name string) (apiResponse net.ApiResponse) { - url := repo.config.Target + "/v2/organizations" - data := fmt.Sprintf(`{"name":"%s"}`, name) - return repo.gateway.CreateResource(url, repo.config.AccessToken, strings.NewReader(data)) -} - -func (repo CloudControllerOrganizationRepository) Rename(orgGuid string, name string) (apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/organizations/%s", repo.config.Target, orgGuid) - data := fmt.Sprintf(`{"name":"%s"}`, name) - return repo.gateway.UpdateResource(url, repo.config.AccessToken, strings.NewReader(data)) -} - -func (repo CloudControllerOrganizationRepository) Delete(orgGuid string) (apiResponse net.ApiResponse) { - url := fmt.Sprintf("%s/v2/organizations/%s?recursive=true", repo.config.Target, orgGuid) - return repo.gateway.DeleteResource(url, repo.config.AccessToken) -} diff --git a/src/cf/api/organizations_test.go b/src/cf/api/organizations_test.go deleted file mode 100644 index 59055ff6dd3..00000000000 --- a/src/cf/api/organizations_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestOrganizationsListOrgs(t *testing.T) { - firstPageOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ - "next_url": "/v2/organizations?page=2", - "resources": [ - { - "metadata": { "guid": "org1-guid" }, - "entity": { "name": "Org1" } - }, - { - "metadata": { "guid": "org2-guid" }, - "entity": { "name": "Org2" } - } - ]}`}, - }) - - secondPageOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations?page=2", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { "guid": "org3-guid" }, - "entity": { "name": "Org3" } - } - ]}`}, - }) - - ts, handler, repo := createOrganizationRepo(t, firstPageOrgsRequest, secondPageOrgsRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - orgsChan, statusChan := repo.ListOrgs(stopChan) - - orgs := []cf.Organization{} - for chunk := range orgsChan { - orgs = append(orgs, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, len(orgs), 3) - assert.Equal(t, orgs[0].Guid, "org1-guid") - assert.Equal(t, orgs[1].Guid, "org2-guid") - assert.Equal(t, orgs[2].Guid, "org3-guid") - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) - -} - -func TestOrganizationsListOrgsWithNoOrgs(t *testing.T) { - emptyOrgsRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - ts, handler, repo := createOrganizationRepo(t, emptyOrgsRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - orgsChan, statusChan := repo.ListOrgs(stopChan) - - _, ok := <-orgsChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestOrganizationsFindByName(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations?q=name%3Aorg1&inline-relations-depth=1", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [{ - "metadata": { "guid": "org1-guid" }, - "entity": { - "name": "Org1", - "spaces": [{ - "metadata": { "guid": "space1-guid" }, - "entity": { "name": "Space1" } - }], - "domains": [{ - "metadata": { "guid": "domain1-guid" }, - "entity": { "name": "cfapps.io" } - }] - } - }]}`}, - }) - - ts, handler, repo := createOrganizationRepo(t, req) - defer ts.Close() - existingOrg := cf.Organization{} - existingOrg.Guid = "org1-guid" - existingOrg.Name = "Org1" - - org, apiResponse := repo.FindByName("Org1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - - assert.Equal(t, org.Name, existingOrg.Name) - assert.Equal(t, org.Guid, existingOrg.Guid) - assert.Equal(t, len(org.Spaces), 1) - assert.Equal(t, org.Spaces[0].Name, "Space1") - assert.Equal(t, org.Spaces[0].Guid, "space1-guid") - assert.Equal(t, len(org.Domains), 1) - assert.Equal(t, org.Domains[0].Name, "cfapps.io") - assert.Equal(t, org.Domains[0].Guid, "domain1-guid") -} - -func TestOrganizationsFindByNameWhenDoesNotExist(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations?q=name%3Aorg1&inline-relations-depth=1", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - ts, handler, repo := createOrganizationRepo(t, req) - defer ts.Close() - - _, apiResponse := repo.FindByName("org1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestCreateOrganization(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/organizations", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-org"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createOrganizationRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("my-org") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestRenameOrganization(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/organizations/my-org-guid", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-new-org"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createOrganizationRepo(t, req) - defer ts.Close() - - apiResponse := repo.Rename("my-org-guid", "my-new-org") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestDeleteOrganization(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/organizations/my-org-guid?recursive=true", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createOrganizationRepo(t, req) - defer ts.Close() - - apiResponse := repo.Delete("my-org-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createOrganizationRepo(t *testing.T, reqs ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo OrganizationRepository) { - ts, handler = testnet.NewTLSServer(t, reqs) - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerOrganizationRepository(config, gateway) - return -} diff --git a/src/cf/api/paginated_resources.go b/src/cf/api/paginated_resources.go deleted file mode 100644 index 4a2a5074751..00000000000 --- a/src/cf/api/paginated_resources.go +++ /dev/null @@ -1,19 +0,0 @@ -package api - -type PaginatedResources struct { - Resources []Resource -} - -type Resource struct { - Metadata Metadata - Entity Entity -} - -type Metadata struct { - Guid string - Url string -} - -type Entity struct { - Name string -} diff --git a/src/cf/api/password.go b/src/cf/api/password.go deleted file mode 100644 index df50e8082a9..00000000000 --- a/src/cf/api/password.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "net/url" - "strings" -) - -type PasswordRepository interface { - GetScore(password string) (string, net.ApiResponse) - UpdatePassword(old string, new string) net.ApiResponse -} - -type CloudControllerPasswordRepository struct { - config *configuration.Configuration - gateway net.Gateway - endpointRepo EndpointRepository -} - -func NewCloudControllerPasswordRepository(config *configuration.Configuration, gateway net.Gateway, endpointRepo EndpointRepository) (repo CloudControllerPasswordRepository) { - repo.config = config - repo.gateway = gateway - repo.endpointRepo = endpointRepo - return -} - -type ScoreResponse struct { - Score int - RequiredScore int -} - -func (repo CloudControllerPasswordRepository) GetScore(password string) (score string, apiResponse net.ApiResponse) { - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - scorePath := fmt.Sprintf("%s/password/score", uaaEndpoint) - scoreBody := url.Values{ - "password": []string{password}, - } - - scoreRequest, apiResponse := repo.gateway.NewRequest("POST", scorePath, repo.config.AccessToken, strings.NewReader(scoreBody.Encode())) - if apiResponse.IsNotSuccessful() { - return - } - scoreRequest.HttpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") - scoreResponse := ScoreResponse{} - - _, apiResponse = repo.gateway.PerformRequestForJSONResponse(scoreRequest, &scoreResponse) - if apiResponse.IsNotSuccessful() { - return - } - - score = translateScoreResponse(scoreResponse) - return -} - -func (repo CloudControllerPasswordRepository) UpdatePassword(old string, new string) (apiResponse net.ApiResponse) { - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - path := fmt.Sprintf("%s/Users/%s/password", uaaEndpoint, repo.config.UserGuid()) - body := fmt.Sprintf(`{"password":"%s","oldPassword":"%s"}`, new, old) - - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func translateScoreResponse(response ScoreResponse) string { - if response.Score == 10 { - return "strong" - } - - if response.Score >= response.RequiredScore { - return "good" - } - - return "weak" -} diff --git a/src/cf/api/password_test.go b/src/cf/api/password_test.go deleted file mode 100644 index 44c3686dea8..00000000000 --- a/src/cf/api/password_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testconfig "testhelpers/configuration" - testnet "testhelpers/net" - "testing" -) - -func TestGetScore(t *testing.T) { - testScore(t, `{"score":5,"requiredScore":5}`, "good") - testScore(t, `{"score":10,"requiredScore":5}`, "strong") - testScore(t, `{"score":4,"requiredScore":5}`, "weak") -} - -func testScore(t *testing.T, scoreBody string, expectedScore string) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/password/score", - Matcher: testnet.RequestBodyMatcherWithContentType("password=new-password", "application/x-www-form-urlencoded"), - Response: testnet.TestResponse{Status: http.StatusOK, Body: scoreBody}, - }) - - accessToken := "BEARER my_access_token" - scoreServer, handler, repo := createPasswordRepo(t, req, accessToken) - defer scoreServer.Close() - - score, apiResponse := repo.GetScore("new-password") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, score, expectedScore) -} - -func TestUpdatePassword(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/Users/my-user-guid/password", - Matcher: testnet.RequestBodyMatcher(`{"password":"new-password","oldPassword":"old-password"}`), - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - accessToken, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{UserGuid: "my-user-guid"}) - assert.NoError(t, err) - - passwordUpdateServer, handler, repo := createPasswordRepo(t, req, accessToken) - defer passwordUpdateServer.Close() - - apiResponse := repo.UpdatePassword("old-password", "new-password") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createPasswordRepo(t *testing.T, req testnet.TestRequest, accessToken string) (passwordServer *httptest.Server, handler *testnet.TestHandler, repo PasswordRepository) { - passwordServer, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.UaaEndpointKey: passwordServer.URL, - }} - - config := &configuration.Configuration{ - AccessToken: accessToken, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerPasswordRepository(config, gateway, endpointRepo) - return -} diff --git a/src/cf/api/quotas.go b/src/cf/api/quotas.go deleted file mode 100644 index 05f8fd80a81..00000000000 --- a/src/cf/api/quotas.go +++ /dev/null @@ -1,89 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedQuotaResources struct { - Resources []QuotaResource -} - -type QuotaResource struct { - Resource - Entity QuotaEntity -} - -func (resource QuotaResource) ToFields() (quota cf.QuotaFields) { - quota.Guid = resource.Metadata.Guid - quota.Name = resource.Entity.Name - quota.MemoryLimit = resource.Entity.MemoryLimit - return -} - -type QuotaEntity struct { - Name string - MemoryLimit uint64 `json:"memory_limit"` -} - -type QuotaRepository interface { - FindAll() (quotas []cf.QuotaFields, apiResponse net.ApiResponse) - FindByName(name string) (quota cf.QuotaFields, apiResponse net.ApiResponse) - Update(orgGuid, quotaGuid string) (apiResponse net.ApiResponse) -} - -type CloudControllerQuotaRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerQuotaRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerQuotaRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerQuotaRepository) findAllWithPath(path string) (quotas []cf.QuotaFields, apiResponse net.ApiResponse) { - resources := new(PaginatedQuotaResources) - - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - for _, r := range resources.Resources { - quotas = append(quotas, r.ToFields()) - } - - return -} - -func (repo CloudControllerQuotaRepository) FindAll() (quotas []cf.QuotaFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/quota_definitions", repo.config.Target) - return repo.findAllWithPath(path) -} - -func (repo CloudControllerQuotaRepository) FindByName(name string) (quota cf.QuotaFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/quota_definitions?q=name%%3A%s", repo.config.Target, name) - quotas, apiResponse := repo.findAllWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(quotas) == 0 { - apiResponse = net.NewNotFoundApiResponse("Quota %s not found", name) - return - } - - quota = quotas[0] - return -} - -func (repo CloudControllerQuotaRepository) Update(orgGuid, quotaGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/organizations/%s", repo.config.Target, orgGuid) - data := fmt.Sprintf(`{"quota_definition_guid":"%s"}`, quotaGuid) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(data)) -} diff --git a/src/cf/api/quotas_test.go b/src/cf/api/quotas_test.go deleted file mode 100644 index ce935e7be60..00000000000 --- a/src/cf/api/quotas_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestFindQuotaByName(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/quota_definitions?q=name%3Amy-quota", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": [ - { - "metadata": { "guid": "my-quota-guid" }, - "entity": { "name": "my-remote-quota", "memory_limit": 1024 } - } - ]}`}, - }) - - ts, handler, repo := createQuotaRepo(t, req) - defer ts.Close() - - quota, apiResponse := repo.FindByName("my-quota") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - expectedQuota := cf.QuotaFields{} - expectedQuota.Guid = "my-quota-guid" - expectedQuota.Name = "my-remote-quota" - expectedQuota.MemoryLimit = 1024 - assert.Equal(t, quota, expectedQuota) -} - -func TestUpdateQuota(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/organizations/my-org-guid", - Matcher: testnet.RequestBodyMatcher(`{"quota_definition_guid":"my-quota-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createQuotaRepo(t, req) - defer ts.Close() - - apiResponse := repo.Update("my-org-guid", "my-quota-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createQuotaRepo(t *testing.T, req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo QuotaRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerQuotaRepository(config, gateway) - return -} diff --git a/src/cf/api/repository_locator.go b/src/cf/api/repository_locator.go deleted file mode 100644 index 6870b9ff2d1..00000000000 --- a/src/cf/api/repository_locator.go +++ /dev/null @@ -1,174 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" -) - -type RepositoryLocator struct { - authRepo AuthenticationRepository - endpointRepo RemoteEndpointRepository - organizationRepo CloudControllerOrganizationRepository - quotaRepo CloudControllerQuotaRepository - spaceRepo CloudControllerSpaceRepository - appRepo CloudControllerApplicationRepository - appBitsRepo CloudControllerApplicationBitsRepository - appSummaryRepo CloudControllerAppSummaryRepository - appInstancesRepo CloudControllerAppInstancesRepository - appEventsRepo CloudControllerAppEventsRepository - appFilesRepo CloudControllerAppFilesRepository - domainRepo CloudControllerDomainRepository - routeRepo CloudControllerRouteRepository - stackRepo CloudControllerStackRepository - serviceRepo CloudControllerServiceRepository - serviceBindingRepo CloudControllerServiceBindingRepository - serviceSummaryRepo CloudControllerServiceSummaryRepository - userRepo CloudControllerUserRepository - passwordRepo CloudControllerPasswordRepository - logsRepo LoggregatorLogsRepository - authTokenRepo CloudControllerServiceAuthTokenRepository - serviceBrokerRepo CloudControllerServiceBrokerRepository - userProvidedServiceInstanceRepo CCUserProvidedServiceInstanceRepository - buildpackRepo CloudControllerBuildpackRepository - buildpackBitsRepo CloudControllerBuildpackBitsRepository -} - -func NewRepositoryLocator(config *configuration.Configuration, configRepo configuration.ConfigurationRepository, gatewaysByName map[string]net.Gateway) (loc RepositoryLocator) { - authGateway := gatewaysByName["auth"] - cloudControllerGateway := gatewaysByName["cloud-controller"] - uaaGateway := gatewaysByName["uaa"] - - loc.authRepo = NewUAAAuthenticationRepository(authGateway, configRepo) - - // ensure gateway refreshers are set before passing them by value to repositories - cloudControllerGateway.SetTokenRefresher(loc.authRepo) - uaaGateway.SetTokenRefresher(loc.authRepo) - - loc.appBitsRepo = NewCloudControllerApplicationBitsRepository(config, cloudControllerGateway, cf.ApplicationZipper{}) - loc.appEventsRepo = NewCloudControllerAppEventsRepository(config, cloudControllerGateway) - loc.appFilesRepo = NewCloudControllerAppFilesRepository(config, cloudControllerGateway) - loc.appRepo = NewCloudControllerApplicationRepository(config, cloudControllerGateway) - loc.appSummaryRepo = NewCloudControllerAppSummaryRepository(config, cloudControllerGateway) - loc.appInstancesRepo = NewCloudControllerAppInstancesRepository(config, cloudControllerGateway) - loc.authTokenRepo = NewCloudControllerServiceAuthTokenRepository(config, cloudControllerGateway) - loc.domainRepo = NewCloudControllerDomainRepository(config, cloudControllerGateway) - loc.endpointRepo = NewEndpointRepository(config, cloudControllerGateway, configRepo) - loc.logsRepo = NewLoggregatorLogsRepository(config, loc.endpointRepo) - loc.organizationRepo = NewCloudControllerOrganizationRepository(config, cloudControllerGateway) - loc.passwordRepo = NewCloudControllerPasswordRepository(config, uaaGateway, loc.endpointRepo) - loc.quotaRepo = NewCloudControllerQuotaRepository(config, cloudControllerGateway) - loc.routeRepo = NewCloudControllerRouteRepository(config, cloudControllerGateway, loc.domainRepo) - loc.stackRepo = NewCloudControllerStackRepository(config, cloudControllerGateway) - loc.serviceRepo = NewCloudControllerServiceRepository(config, cloudControllerGateway) - loc.serviceBindingRepo = NewCloudControllerServiceBindingRepository(config, cloudControllerGateway) - loc.serviceBrokerRepo = NewCloudControllerServiceBrokerRepository(config, cloudControllerGateway) - loc.serviceSummaryRepo = NewCloudControllerServiceSummaryRepository(config, cloudControllerGateway) - loc.spaceRepo = NewCloudControllerSpaceRepository(config, cloudControllerGateway) - loc.userProvidedServiceInstanceRepo = NewCCUserProvidedServiceInstanceRepository(config, cloudControllerGateway) - loc.userRepo = NewCloudControllerUserRepository(config, uaaGateway, cloudControllerGateway, loc.endpointRepo) - loc.buildpackRepo = NewCloudControllerBuildpackRepository(config, cloudControllerGateway) - loc.buildpackBitsRepo = NewCloudControllerBuildpackBitsRepository(config, cloudControllerGateway, cf.ApplicationZipper{}) - - return -} - -func (locator RepositoryLocator) GetAuthenticationRepository() AuthenticationRepository { - return locator.authRepo -} - -func (locator RepositoryLocator) GetEndpointRepository() EndpointRepository { - return locator.endpointRepo -} - -func (locator RepositoryLocator) GetOrganizationRepository() OrganizationRepository { - return locator.organizationRepo -} - -func (locator RepositoryLocator) GetQuotaRepository() QuotaRepository { - return locator.quotaRepo -} - -func (locator RepositoryLocator) GetSpaceRepository() SpaceRepository { - return locator.spaceRepo -} - -func (locator RepositoryLocator) GetApplicationRepository() ApplicationRepository { - return locator.appRepo -} - -func (locator RepositoryLocator) GetApplicationBitsRepository() ApplicationBitsRepository { - return locator.appBitsRepo -} - -func (locator RepositoryLocator) GetAppSummaryRepository() AppSummaryRepository { - return locator.appSummaryRepo -} - -func (locator RepositoryLocator) GetAppInstancesRepository() AppInstancesRepository { - return locator.appInstancesRepo -} - -func (locator RepositoryLocator) GetAppEventsRepository() AppEventsRepository { - return locator.appEventsRepo -} - -func (locator RepositoryLocator) GetAppFilesRepository() AppFilesRepository { - return locator.appFilesRepo -} - -func (locator RepositoryLocator) GetDomainRepository() DomainRepository { - return locator.domainRepo -} - -func (locator RepositoryLocator) GetRouteRepository() RouteRepository { - return locator.routeRepo -} - -func (locator RepositoryLocator) GetStackRepository() StackRepository { - return locator.stackRepo -} - -func (locator RepositoryLocator) GetServiceRepository() ServiceRepository { - return locator.serviceRepo -} - -func (locator RepositoryLocator) GetServiceBindingRepository() ServiceBindingRepository { - return locator.serviceBindingRepo -} - -func (locator RepositoryLocator) GetServiceSummaryRepository() ServiceSummaryRepository { - return locator.serviceSummaryRepo -} - -func (locator RepositoryLocator) GetUserRepository() UserRepository { - return locator.userRepo -} - -func (locator RepositoryLocator) GetPasswordRepository() PasswordRepository { - return locator.passwordRepo -} - -func (locator RepositoryLocator) GetLogsRepository() LogsRepository { - return locator.logsRepo -} - -func (locator RepositoryLocator) GetServiceAuthTokenRepository() ServiceAuthTokenRepository { - return locator.authTokenRepo -} - -func (locator RepositoryLocator) GetServiceBrokerRepository() ServiceBrokerRepository { - return locator.serviceBrokerRepo -} - -func (locator RepositoryLocator) GetUserProvidedServiceInstanceRepository() UserProvidedServiceInstanceRepository { - return locator.userProvidedServiceInstanceRepo -} - -func (locator RepositoryLocator) GetBuildpackRepository() BuildpackRepository { - return locator.buildpackRepo -} - -func (locator RepositoryLocator) GetBuildpackBitsRepository() BuildpackBitsRepository { - return locator.buildpackBitsRepo -} diff --git a/src/cf/api/routes.go b/src/cf/api/routes.go deleted file mode 100644 index 2741ccf52d3..00000000000 --- a/src/cf/api/routes.go +++ /dev/null @@ -1,187 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedRouteResources struct { - Resources []RouteResource `json:"resources"` - NextUrl string `json:"next_url"` -} - -type RouteResource struct { - Resource - Entity RouteEntity -} - -func (resource RouteResource) ToFields() (fields cf.RouteFields) { - fields.Guid = resource.Metadata.Guid - fields.Host = resource.Entity.Host - return -} -func (resource RouteResource) ToModel() (route cf.Route) { - route.RouteFields = resource.ToFields() - route.Domain = resource.Entity.Domain.ToFields() - route.Space = resource.Entity.Space.ToFields() - for _, appResource := range resource.Entity.Apps { - route.Apps = append(route.Apps, appResource.ToFields()) - } - return -} - -type RouteEntity struct { - Host string - Domain DomainResource - Space SpaceResource - Apps []ApplicationResource -} - -type RouteRepository interface { - ListRoutes(stop chan bool) (routesChan chan []cf.Route, statusChan chan net.ApiResponse) - FindByHost(host string) (route cf.Route, apiResponse net.ApiResponse) - FindByHostAndDomain(host, domain string) (route cf.Route, apiResponse net.ApiResponse) - Create(host, domainGuid string) (createdRoute cf.RouteFields, apiResponse net.ApiResponse) - CreateInSpace(host, domainGuid, spaceGuid string) (createdRoute cf.RouteFields, apiResponse net.ApiResponse) - Bind(routeGuid, appGuid string) (apiResponse net.ApiResponse) - Unbind(routeGuid, appGuid string) (apiResponse net.ApiResponse) - Delete(routeGuid string) (apiResponse net.ApiResponse) -} - -type CloudControllerRouteRepository struct { - config *configuration.Configuration - gateway net.Gateway - domainRepo DomainRepository -} - -func NewCloudControllerRouteRepository(config *configuration.Configuration, gateway net.Gateway, domainRepo DomainRepository) (repo CloudControllerRouteRepository) { - repo.config = config - repo.gateway = gateway - repo.domainRepo = domainRepo - return -} - -func (repo CloudControllerRouteRepository) ListRoutes(stop chan bool) (routesChan chan []cf.Route, statusChan chan net.ApiResponse) { - routesChan = make(chan []cf.Route, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := fmt.Sprintf("/v2/routes?inline-relations-depth=1") - - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - routes []cf.Route - apiResponse net.ApiResponse - ) - routes, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(routesChan) - close(statusChan) - return - } - - if len(routes) > 0 { - routesChan <- routes - } - } - } - close(routesChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerRouteRepository) FindByHost(host string) (route cf.Route, apiResponse net.ApiResponse) { - path := fmt.Sprintf("/v2/routes?inline-relations-depth=1&q=host%s", "%3A"+host) - return repo.findOneWithPath(path) -} - -func (repo CloudControllerRouteRepository) FindByHostAndDomain(host, domainName string) (route cf.Route, apiResponse net.ApiResponse) { - domain, apiResponse := repo.domainRepo.FindByName(domainName) - if apiResponse.IsNotSuccessful() { - return - } - - path := fmt.Sprintf("/v2/routes?inline-relations-depth=1&q=host%%3A%s%%3Bdomain_guid%%3A%s", host, domain.Guid) - route, apiResponse = repo.findOneWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - route.Domain = domain.DomainFields - return -} - -func (repo CloudControllerRouteRepository) findOneWithPath(path string) (route cf.Route, apiResponse net.ApiResponse) { - routes, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(routes) == 0 { - apiResponse = net.NewNotFoundApiResponse("Route not found") - return - } - - route = routes[0] - return -} - -func (repo CloudControllerRouteRepository) findNextWithPath(path string) (routes []cf.Route, nextUrl string, apiResponse net.ApiResponse) { - routesResources := new(PaginatedRouteResources) - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, routesResources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = routesResources.NextUrl - - for _, routeResponse := range routesResources.Resources { - routes = append(routes, routeResponse.ToModel()) - } - return -} - -func (repo CloudControllerRouteRepository) Create(host, domainGuid string) (createdRoute cf.RouteFields, apiResponse net.ApiResponse) { - return repo.CreateInSpace(host, domainGuid, repo.config.SpaceFields.Guid) -} - -func (repo CloudControllerRouteRepository) CreateInSpace(host, domainGuid, spaceGuid string) (createdRoute cf.RouteFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/routes", repo.config.Target) - data := fmt.Sprintf(`{"host":"%s","domain_guid":"%s","space_guid":"%s"}`, host, domainGuid, spaceGuid) - - resource := new(RouteResource) - apiResponse = repo.gateway.CreateResourceForResponse(path, repo.config.AccessToken, strings.NewReader(data), resource) - if apiResponse.IsNotSuccessful() { - return - } - - createdRoute = resource.ToFields() - return -} - -func (repo CloudControllerRouteRepository) Bind(routeGuid, appGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s/routes/%s", repo.config.Target, appGuid, routeGuid) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, nil) -} - -func (repo CloudControllerRouteRepository) Unbind(routeGuid, appGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/apps/%s/routes/%s", repo.config.Target, appGuid, routeGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} - -func (repo CloudControllerRouteRepository) Delete(routeGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/routes/%s", repo.config.Target, routeGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} diff --git a/src/cf/api/routes_test.go b/src/cf/api/routes_test.go deleted file mode 100644 index b4b53291ae9..00000000000 --- a/src/cf/api/routes_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var firstPageRoutesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "next_url": "/v2/routes?inline-relations-depth=1&page=2", - "resources": [ - { - "metadata": { - "guid": "route-1-guid" - }, - "entity": { - "host": "route-1-host", - "domain": { - "metadata": { - "guid": "domain-1-guid" - }, - "entity": { - "name": "cfapps.io" - } - }, - "space": { - "metadata": { - "guid": "space-1-guid" - }, - "entity": { - "name": "space-1" - } - }, - "apps": [ - { - "metadata": { - "guid": "app-1-guid" - }, - "entity": { - "name": "app-1" - } - } - ] - } - } - ] -}`} - -var secondPageRoutesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "resources": [ - { - "metadata": { - "guid": "route-2-guid" - }, - "entity": { - "host": "route-2-host", - "domain": { - "metadata": { - "guid": "domain-2-guid" - }, - "entity": { - "name": "example.com" - } - }, - "space": { - "metadata": { - "guid": "space-2-guid" - }, - "entity": { - "name": "space-2" - } - }, - "apps": [ - { - "metadata": { - "guid": "app-2-guid" - }, - "entity": { - "name": "app-2" - } - }, - { - "metadata": { - "guid": "app-3-guid" - }, - "entity": { - "name": "app-3" - } - } - ] - } - } - ] -}`} - -func TestRoutesListRoutes(t *testing.T) { - firstRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?inline-relations-depth=1", - Response: firstPageRoutesResponse, - }) - - secondRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?inline-relations-depth=1&page=2", - Response: secondPageRoutesResponse, - }) - - ts, handler, repo, _ := createRoutesRepo(t, firstRequest, secondRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - routesChan, statusChan := repo.ListRoutes(stopChan) - - routes := []cf.Route{} - for chunk := range routesChan { - routes = append(routes, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, len(routes), 2) - assert.Equal(t, routes[0].Guid, "route-1-guid") - assert.Equal(t, routes[1].Guid, "route-2-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestRoutesListRoutesWithNoRoutes(t *testing.T) { - emptyRoutesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?inline-relations-depth=1", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, emptyRoutesRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - routesChan, statusChan := repo.ListRoutes(stopChan) - - _, ok := <-routesChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -var findRouteByHostResponse = testnet.TestResponse{Status: http.StatusCreated, Body: ` -{ "resources": [ - { - "metadata": { - "guid": "my-route-guid" - }, - "entity": { - "host": "my-cool-app" - } - } -]}`} - -func TestFindByHost(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?q=host%3Amy-cool-app", - Response: findRouteByHostResponse, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - route, apiResponse := repo.FindByHost("my-cool-app") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, route.Host, "my-cool-app") - assert.Equal(t, route.Guid, "my-route-guid") -} - -func TestFindByHostWhenHostIsNotFound(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?q=host%3Amy-cool-app", - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` { "resources": [ ]}`}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - _, apiResponse := repo.FindByHost("my-cool-app") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) -} - -func TestFindByHostAndDomain(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?q=host%3Amy-cool-app%3Bdomain_guid%3Amy-domain-guid", - Response: findRouteByHostResponse, - }) - - ts, handler, repo, domainRepo := createRoutesRepo(t, request) - defer ts.Close() - - domain := cf.Domain{} - domain.Guid = "my-domain-guid" - domainRepo.FindByNameDomain = domain - - route, apiResponse := repo.FindByHostAndDomain("my-cool-app", "my-domain.com") - - assert.False(t, apiResponse.IsNotSuccessful()) - assert.True(t, handler.AllRequestsCalled()) - assert.Equal(t, domainRepo.FindByNameName, "my-domain.com") - assert.Equal(t, route.Host, "my-cool-app") - assert.Equal(t, route.Guid, "my-route-guid") - assert.Equal(t, route.Domain.Guid, domain.Guid) -} - -func TestFindByHostAndDomainWhenRouteIsNotFound(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/routes?q=host%3Amy-cool-app%3Bdomain_guid%3Amy-domain-guid", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [ ] }`}, - }) - - ts, handler, repo, domainRepo := createRoutesRepo(t, request) - defer ts.Close() - - domain := cf.Domain{} - domain.Guid = "my-domain-guid" - domainRepo.FindByNameDomain = domain - - _, apiResponse := repo.FindByHostAndDomain("my-cool-app", "my-domain.com") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestCreateInSpace(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/routes", - Matcher: testnet.RequestBodyMatcher(`{"host":"my-cool-app","domain_guid":"my-domain-guid","space_guid":"my-space-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` -{ - "metadata": { "guid": "my-route-guid" }, - "entity": { "host": "my-cool-app" } -}`}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - createdRoute, apiResponse := repo.CreateInSpace("my-cool-app", "my-domain-guid", "my-space-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, createdRoute.Guid, "my-route-guid") -} - -func TestCreateRoute(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/routes", - Matcher: testnet.RequestBodyMatcher(`{"host":"my-cool-app","domain_guid":"my-domain-guid","space_guid":"my-space-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` -{ - "metadata": { "guid": "my-route-guid" }, - "entity": { "host": "my-cool-app" } -}`}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - createdRoute, apiResponse := repo.Create("my-cool-app", "my-domain-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - - assert.Equal(t, createdRoute.Guid, "my-route-guid") -} - -func TestBind(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/apps/my-cool-app-guid/routes/my-cool-route-guid", - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Bind("my-cool-route-guid", "my-cool-app-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestUnbind(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/apps/my-cool-app-guid/routes/my-cool-route-guid", - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Unbind("my-cool-route-guid", "my-cool-app-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestDelete(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/routes/my-cool-route-guid", - Response: testnet.TestResponse{Status: http.StatusCreated, Body: ""}, - }) - - ts, handler, repo, _ := createRoutesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Delete("my-cool-route-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func createRoutesRepo(t *testing.T, requests ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo CloudControllerRouteRepository, domainRepo *testapi.FakeDomainRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - SpaceFields: space, - } - - gateway := net.NewCloudControllerGateway() - domainRepo = &testapi.FakeDomainRepository{} - - repo = NewCloudControllerRouteRepository(config, gateway, domainRepo) - return -} diff --git a/src/cf/api/service_auth_tokens.go b/src/cf/api/service_auth_tokens.go deleted file mode 100644 index e70ff8ae19e..00000000000 --- a/src/cf/api/service_auth_tokens.go +++ /dev/null @@ -1,98 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedAuthTokenResources struct { - Resources []AuthTokenResource -} - -type AuthTokenResource struct { - Resource - Entity AuthTokenEntity -} - -type AuthTokenEntity struct { - Label string - Provider string -} - -type ServiceAuthTokenRepository interface { - FindAll() (authTokens []cf.ServiceAuthTokenFields, apiResponse net.ApiResponse) - FindByLabelAndProvider(label, provider string) (authToken cf.ServiceAuthTokenFields, apiResponse net.ApiResponse) - Create(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) - Update(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) - Delete(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) -} - -type CloudControllerServiceAuthTokenRepository struct { - gateway net.Gateway - config *configuration.Configuration -} - -func NewCloudControllerServiceAuthTokenRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerServiceAuthTokenRepository) { - repo.gateway = gateway - repo.config = config - return -} - -func (repo CloudControllerServiceAuthTokenRepository) FindAll() (authTokens []cf.ServiceAuthTokenFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_auth_tokens", repo.config.Target) - return repo.findAllWithPath(path) -} - -func (repo CloudControllerServiceAuthTokenRepository) FindByLabelAndProvider(label, provider string) (authToken cf.ServiceAuthTokenFields, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_auth_tokens?q=label:%s;provider:%s", repo.config.Target, label, provider) - authTokens, apiResponse := repo.findAllWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(authTokens) == 0 { - apiResponse = net.NewNotFoundApiResponse("Service Auth Token %s %s not found", label, provider) - return - } - - authToken = authTokens[0] - return -} - -func (repo CloudControllerServiceAuthTokenRepository) findAllWithPath(path string) (authTokens []cf.ServiceAuthTokenFields, apiResponse net.ApiResponse) { - resources := new(PaginatedAuthTokenResources) - - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - for _, resource := range resources.Resources { - authTokens = append(authTokens, cf.ServiceAuthTokenFields{ - Guid: resource.Metadata.Guid, - Label: resource.Entity.Label, - Provider: resource.Entity.Provider, - }) - } - return -} - -func (repo CloudControllerServiceAuthTokenRepository) Create(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) { - body := fmt.Sprintf(`{"label":"%s","provider":"%s","token":"%s"}`, authToken.Label, authToken.Provider, authToken.Token) - path := fmt.Sprintf("%s/v2/service_auth_tokens", repo.config.Target) - return repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceAuthTokenRepository) Delete(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_auth_tokens/%s", repo.config.Target, authToken.Guid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} - -func (repo CloudControllerServiceAuthTokenRepository) Update(authToken cf.ServiceAuthTokenFields) (apiResponse net.ApiResponse) { - body := fmt.Sprintf(`{"token":"%s"}`, authToken.Token) - path := fmt.Sprintf("%s/v2/service_auth_tokens/%s", repo.config.Target, authToken.Guid) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} diff --git a/src/cf/api/service_auth_tokens_test.go b/src/cf/api/service_auth_tokens_test.go deleted file mode 100644 index c1d84d6b4ab..00000000000 --- a/src/cf/api/service_auth_tokens_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestServiceAuthCreate(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_auth_tokens", - Matcher: testnet.RequestBodyMatcher(`{"label":"a label","provider":"a provider","token":"a token"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - authToken := cf.ServiceAuthTokenFields{} - authToken.Label = "a label" - authToken.Provider = "a provider" - authToken.Token = "a token" - apiResponse := repo.Create(authToken) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestServiceAuthFindAll(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_auth_tokens", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ "resources": [ - { - "metadata": { - "guid": "mysql-core-guid" - }, - "entity": { - "label": "mysql", - "provider": "mysql-core" - } - }, - { - "metadata": { - "guid": "postgres-core-guid" - }, - "entity": { - "label": "postgres", - "provider": "postgres-core" - } - } - ]}`}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - - authTokens, apiResponse := repo.FindAll() - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, len(authTokens), 2) - - assert.Equal(t, authTokens[0].Label, "mysql") - assert.Equal(t, authTokens[0].Provider, "mysql-core") - assert.Equal(t, authTokens[0].Guid, "mysql-core-guid") - - assert.Equal(t, authTokens[1].Label, "postgres") - assert.Equal(t, authTokens[1].Provider, "postgres-core") - assert.Equal(t, authTokens[1].Guid, "postgres-core-guid") -} - -func TestServiceAuthFindByLabelAndProvider(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_auth_tokens?q=label:a-label;provider:a-provider", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": [{ - "metadata": { "guid": "mysql-core-guid" }, - "entity": { - "label": "mysql", - "provider": "mysql-core" - } - }]}`}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - - serviceAuthToken, apiResponse := repo.FindByLabelAndProvider("a-label", "a-provider") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - authToken2 := cf.ServiceAuthTokenFields{} - authToken2.Guid = "mysql-core-guid" - authToken2.Label = "mysql" - authToken2.Provider = "mysql-core" - assert.Equal(t, serviceAuthToken, authToken2) -} - -func TestServiceAuthFindByLabelAndProviderWhenNotFound(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_auth_tokens?q=label:a-label;provider:a-provider", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": []}`}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - - _, apiResponse := repo.FindByLabelAndProvider("a-label", "a-provider") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestServiceAuthUpdate(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/service_auth_tokens/mysql-core-guid", - Matcher: testnet.RequestBodyMatcher(`{"token":"a value"}`), - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - authToken3 := cf.ServiceAuthTokenFields{} - authToken3.Guid = "mysql-core-guid" - authToken3.Token = "a value" - apiResponse := repo.Update(authToken3) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestServiceAuthDelete(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/service_auth_tokens/mysql-core-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createServiceAuthTokenRepo(t, req) - defer ts.Close() - authToken4 := cf.ServiceAuthTokenFields{} - authToken4.Guid = "mysql-core-guid" - apiResponse := repo.Delete(authToken4) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func createServiceAuthTokenRepo(t *testing.T, request testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceAuthTokenRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{request}) - - config := &configuration.Configuration{ - Target: ts.URL, - AccessToken: "BEARER my_access_token", - } - gateway := net.NewCloudControllerGateway() - - repo = NewCloudControllerServiceAuthTokenRepository(config, gateway) - return -} diff --git a/src/cf/api/service_bindings.go b/src/cf/api/service_bindings.go deleted file mode 100644 index 8fcc350b08c..00000000000 --- a/src/cf/api/service_bindings.go +++ /dev/null @@ -1,54 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type ServiceBindingRepository interface { - Create(instanceGuid, appGuid string) (apiResponse net.ApiResponse) - Delete(instance cf.ServiceInstance, appGuid string) (found bool, apiResponse net.ApiResponse) -} - -type CloudControllerServiceBindingRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerServiceBindingRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerServiceBindingRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerServiceBindingRepository) Create(instanceGuid, appGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_bindings", repo.config.Target) - body := fmt.Sprintf( - `{"app_guid":"%s","service_instance_guid":"%s"}`, - appGuid, instanceGuid, - ) - return repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceBindingRepository) Delete(instance cf.ServiceInstance, appGuid string) (found bool, apiResponse net.ApiResponse) { - var path string - - for _, binding := range instance.ServiceBindings { - if binding.AppGuid == appGuid { - path = repo.config.Target + binding.Url - break - } - } - - if path == "" { - return - } else { - found = true - } - - apiResponse = repo.gateway.DeleteResource(path, repo.config.AccessToken) - return -} diff --git a/src/cf/api/service_bindings_test.go b/src/cf/api/service_bindings_test.go deleted file mode 100644 index 2477d2f2875..00000000000 --- a/src/cf/api/service_bindings_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestCreateServiceBinding(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_bindings", - Matcher: testnet.RequestBodyMatcher(`{"app_guid":"my-app-guid","service_instance_guid":"my-service-instance-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createServiceBindingRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("my-service-instance-guid", "my-app-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestCreateServiceBindingIfError(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_bindings", - Matcher: testnet.RequestBodyMatcher(`{"app_guid":"my-app-guid","service_instance_guid":"my-service-instance-guid"}`), - Response: testnet.TestResponse{ - Status: http.StatusBadRequest, - Body: `{"code":90003,"description":"The app space binding to service is taken: 7b959018-110a-4913-ac0a-d663e613cdea 346bf237-7eef-41a7-b892-68fb08068f09"}`, - }, - }) - - ts, handler, repo := createServiceBindingRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("my-service-instance-guid", "my-app-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, apiResponse.ErrorCode, "90003") -} - -var deleteBindingReq = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/service_bindings/service-binding-2-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, -}) - -func TestDeleteServiceBinding(t *testing.T) { - ts, handler, repo := createServiceBindingRepo(t, deleteBindingReq) - defer ts.Close() - - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - - binding := cf.ServiceBindingFields{} - binding.Url = "/v2/service_bindings/service-binding-1-guid" - binding.AppGuid = "app-1-guid" - binding2 := cf.ServiceBindingFields{} - binding2.Url = "/v2/service_bindings/service-binding-2-guid" - binding2.AppGuid = "app-2-guid" - serviceInstance.ServiceBindings = []cf.ServiceBindingFields{binding, binding2} - - found, apiResponse := repo.Delete(serviceInstance, "app-2-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.True(t, found) -} - -func TestDeleteServiceBindingWhenBindingDoesNotExist(t *testing.T) { - ts, handler, repo := createServiceBindingRepo(t, deleteBindingReq) - defer ts.Close() - - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - - found, apiResponse := repo.Delete(serviceInstance, "app-2-guid") - - assert.False(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.False(t, found) -} - -func createServiceBindingRepo(t *testing.T, req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceBindingRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - SpaceFields: space, - Target: ts.URL, - } - - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerServiceBindingRepository(config, gateway) - return -} diff --git a/src/cf/api/service_brokers.go b/src/cf/api/service_brokers.go deleted file mode 100644 index 8cd14c01f72..00000000000 --- a/src/cf/api/service_brokers.go +++ /dev/null @@ -1,154 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedServiceBrokerResources struct { - ServiceBrokers []ServiceBrokerResource `json:"resources"` - NextUrl string `json:"next_url"` -} - -type ServiceBrokerResource struct { - Resource - Entity ServiceBrokerEntity -} - -func (resource ServiceBrokerResource) ToFields() (fields cf.ServiceBroker) { - fields.Name = resource.Entity.Name - fields.Guid = resource.Metadata.Guid - fields.Url = resource.Entity.Url - fields.Username = resource.Entity.Username - fields.Password = resource.Entity.Password - return -} - -type ServiceBrokerEntity struct { - Guid string - Name string - Password string `json:"auth_password"` - Username string `json:"auth_username"` - Url string `json:"broker_url"` -} - -type ServiceBrokerRepository interface { - ListServiceBrokers(stop chan bool) (serviceBrokersChan chan []cf.ServiceBroker, statusChan chan net.ApiResponse) - FindByName(name string) (serviceBroker cf.ServiceBroker, apiResponse net.ApiResponse) - Create(name, url, username, password string) (apiResponse net.ApiResponse) - Update(serviceBroker cf.ServiceBroker) (apiResponse net.ApiResponse) - Rename(guid, name string) (apiResponse net.ApiResponse) - Delete(guid string) (apiResponse net.ApiResponse) -} - -type CloudControllerServiceBrokerRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerServiceBrokerRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerServiceBrokerRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerServiceBrokerRepository) ListServiceBrokers(stop chan bool) (serviceBrokersChan chan []cf.ServiceBroker, statusChan chan net.ApiResponse) { - serviceBrokersChan = make(chan []cf.ServiceBroker, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := "/v2/service_brokers" - - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - serviceBrokers []cf.ServiceBroker - apiResponse net.ApiResponse - ) - serviceBrokers, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(serviceBrokersChan) - close(statusChan) - return - } - - if len(serviceBrokers) > 0 { - serviceBrokersChan <- serviceBrokers - } - } - } - close(serviceBrokersChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerServiceBrokerRepository) FindByName(name string) (serviceBroker cf.ServiceBroker, apiResponse net.ApiResponse) { - path := fmt.Sprintf("/v2/service_brokers?q=name%%3A%s", name) - serviceBrokers, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(serviceBrokers) == 0 { - apiResponse = net.NewNotFoundApiResponse("Service Broker %s not found", name) - return - } - - serviceBroker = serviceBrokers[0] - return -} - -func (repo CloudControllerServiceBrokerRepository) findNextWithPath(path string) (serviceBrokers []cf.ServiceBroker, nextUrl string, apiResponse net.ApiResponse) { - resources := new(PaginatedServiceBrokerResources) - - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = resources.NextUrl - - for _, resource := range resources.ServiceBrokers { - serviceBrokers = append(serviceBrokers, resource.ToFields()) - } - return -} - -func (repo CloudControllerServiceBrokerRepository) Create(name, url, username, password string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_brokers", repo.config.Target) - body := fmt.Sprintf( - `{"name":"%s","broker_url":"%s","auth_username":"%s","auth_password":"%s"}`, name, url, username, password, - ) - return repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceBrokerRepository) Update(serviceBroker cf.ServiceBroker) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_brokers/%s", repo.config.Target, serviceBroker.Guid) - body := fmt.Sprintf( - `{"broker_url":"%s","auth_username":"%s","auth_password":"%s"}`, - serviceBroker.Url, serviceBroker.Username, serviceBroker.Password, - ) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceBrokerRepository) Rename(guid, name string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_brokers/%s", repo.config.Target, guid) - body := fmt.Sprintf(`{"name":"%s"}`, name) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceBrokerRepository) Delete(guid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_brokers/%s", repo.config.Target, guid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} diff --git a/src/cf/api/service_brokers_test.go b/src/cf/api/service_brokers_test.go deleted file mode 100644 index 28de0be260a..00000000000 --- a/src/cf/api/service_brokers_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestServiceBrokersListServiceBrokers(t *testing.T) { - firstRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_brokers", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "next_url": "/v2/service_brokers?page=2", - "resources": [ - { - "metadata": { - "guid":"found-guid-1" - }, - "entity": { - "name": "found-name-1", - "broker_url": "http://found.example.com-1", - "auth_username": "found-username-1", - "auth_password": "found-password-1" - } - } - ] - }`, - }, - }) - - secondRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_brokers?page=2", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "resources": [ - { - "metadata": { - "guid":"found-guid-2" - }, - "entity": { - "name": "found-name-2", - "broker_url": "http://found.example.com-2", - "auth_username": "found-username-2", - "auth_password": "found-password-2" - } - } - ] - }`, - }, - }) - - ts, handler, repo := createServiceBrokerRepo(t, firstRequest, secondRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - serviceBrokersChan, statusChan := repo.ListServiceBrokers(stopChan) - - serviceBrokers := []cf.ServiceBroker{} - for chunk := range serviceBrokersChan { - serviceBrokers = append(serviceBrokers, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, len(serviceBrokers), 2) - assert.Equal(t, serviceBrokers[0].Guid, "found-guid-1") - assert.Equal(t, serviceBrokers[1].Guid, "found-guid-2") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestServiceBrokersListServiceBrokersWithNoServiceBrokers(t *testing.T) { - emptyServiceBrokersRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_brokers", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": []}`, - }, - }) - - ts, handler, repo := createServiceBrokerRepo(t, emptyServiceBrokersRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - serviceBrokersChan, statusChan := repo.ListServiceBrokers(stopChan) - - _, ok := <-serviceBrokersChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestFindServiceBrokerByName(t *testing.T) { - responseBody := `{ - "resources": [ - { - "metadata": { - "guid":"found-guid" - }, - "entity": { - "name": "found-name", - "broker_url": "http://found.example.com", - "auth_username": "found-username", - "auth_password": "found-password" - } - } - ] -}` - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_brokers?q=name%3Amy-broker", - Response: testnet.TestResponse{Status: http.StatusOK, Body: responseBody}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - - foundBroker, apiResponse := repo.FindByName("my-broker") - expectedBroker := cf.ServiceBroker{} - expectedBroker.Name = "found-name" - expectedBroker.Url = "http://found.example.com" - expectedBroker.Username = "found-username" - expectedBroker.Password = "found-password" - expectedBroker.Guid = "found-guid" - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, foundBroker, expectedBroker) -} - -func TestFindServiceBrokerByNameWheNotFound(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/service_brokers?q=name%3Amy-broker", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [ ] }`}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - - _, apiResponse := repo.FindByName("my-broker") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotFound()) - assert.Equal(t, apiResponse.Message, "Service Broker my-broker not found") -} - -func TestCreateServiceBroker(t *testing.T) { - expectedReqBody := `{"name":"foobroker","broker_url":"http://example.com","auth_username":"foouser","auth_password":"password"}` - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_brokers", - Matcher: testnet.RequestBodyMatcher(expectedReqBody), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("foobroker", "http://example.com", "foouser", "password") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestUpdateServiceBroker(t *testing.T) { - expectedReqBody := `{"broker_url":"http://update.example.com","auth_username":"update-foouser","auth_password":"update-password"}` - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/service_brokers/my-guid", - Matcher: testnet.RequestBodyMatcher(expectedReqBody), - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - serviceBroker := cf.ServiceBroker{} - serviceBroker.Guid = "my-guid" - serviceBroker.Name = "foobroker" - serviceBroker.Url = "http://update.example.com" - serviceBroker.Username = "update-foouser" - serviceBroker.Password = "update-password" - - apiResponse := repo.Update(serviceBroker) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestRenameServiceBroker(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/service_brokers/my-guid", - Matcher: testnet.RequestBodyMatcher(`{"name":"update-foobroker"}`), - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - - apiResponse := repo.Rename("my-guid", "update-foobroker") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestDeleteServiceBroker(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/service_brokers/my-guid", - Response: testnet.TestResponse{Status: http.StatusNoContent}, - }) - - ts, handler, repo := createServiceBrokerRepo(t, req) - defer ts.Close() - - apiResponse := repo.Delete("my-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func createServiceBrokerRepo(t *testing.T, requests ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceBrokerRepository) { - ts, handler = testnet.NewTLSServer(t, requests) - - config := &configuration.Configuration{ - Target: ts.URL, - AccessToken: "BEARER my_access_token", - } - - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerServiceBrokerRepository(config, gateway) - return -} diff --git a/src/cf/api/service_summary.go b/src/cf/api/service_summary.go deleted file mode 100644 index fe5f428f5c1..00000000000 --- a/src/cf/api/service_summary.go +++ /dev/null @@ -1,103 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" -) - -type ServiceInstancesSummaries struct { - Apps []ServiceInstanceSummaryApp - ServiceInstances []ServiceInstanceSummary `json:"services"` -} - -func (resource ServiceInstancesSummaries) ToModels() (instances []cf.ServiceInstance) { - for _, instanceSummary := range resource.ServiceInstances { - applicationNames := resource.findApplicationNamesForInstance(instanceSummary.Name) - - planSummary := instanceSummary.ServicePlan - servicePlan := cf.ServicePlanFields{} - servicePlan.Name = planSummary.Name - servicePlan.Guid = planSummary.Guid - - offeringSummary := planSummary.ServiceOffering - serviceOffering := cf.ServiceOfferingFields{} - serviceOffering.Label = offeringSummary.Label - serviceOffering.Provider = offeringSummary.Provider - serviceOffering.Version = offeringSummary.Version - - instance := cf.ServiceInstance{} - instance.Name = instanceSummary.Name - instance.ApplicationNames = applicationNames - instance.ServicePlan = servicePlan - instance.ServiceOffering = serviceOffering - - instances = append(instances, instance) - } - - return -} - -func (resource ServiceInstancesSummaries) findApplicationNamesForInstance(instanceName string) (applicationNames []string) { - for _, app := range resource.Apps { - for _, name := range app.ServiceNames { - if name == instanceName { - applicationNames = append(applicationNames, app.Name) - } - } - } - - return -} - -type ServiceInstanceSummaryApp struct { - Name string - ServiceNames []string `json:"service_names"` -} - -type ServiceInstanceSummary struct { - Name string - ServicePlan ServicePlanSummary `json:"service_plan"` -} - -type ServicePlanSummary struct { - Name string - Guid string - ServiceOffering ServiceOfferingSummary `json:"service"` -} - -type ServiceOfferingSummary struct { - Label string - Provider string - Version string -} - -type ServiceSummaryRepository interface { - GetSummariesInCurrentSpace() (instances []cf.ServiceInstance, apiResponse net.ApiResponse) -} - -type CloudControllerServiceSummaryRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerServiceSummaryRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerServiceSummaryRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerServiceSummaryRepository) GetSummariesInCurrentSpace() (instances []cf.ServiceInstance, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s/summary", repo.config.Target, repo.config.SpaceFields.Guid) - resource := new(ServiceInstancesSummaries) - - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resource) - if apiResponse.IsNotSuccessful() { - return - } - - instances = resource.ToModels() - - return -} diff --git a/src/cf/api/service_summary_test.go b/src/cf/api/service_summary_test.go deleted file mode 100644 index ea3f412e11a..00000000000 --- a/src/cf/api/service_summary_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var serviceInstanceSummariesResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "apps":[ - { - "name":"app1", - "service_names":[ - "my-service-instance" - ] - },{ - "name":"app2", - "service_names":[ - "my-service-instance" - ] - } - ], - "services": [ - { - "guid": "my-service-instance-guid", - "name": "my-service-instance", - "bound_app_count": 2, - "service_plan": { - "guid": "service-plan-guid", - "name": "spark", - "service": { - "guid": "service-offering-guid", - "label": "cleardb", - "provider": "cleardb-provider", - "version": "n/a" - } - } - } - ] -}`} - -func TestServiceSummaryGetSummariesInCurrentSpace(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/summary", - Response: serviceInstanceSummariesResponse, - }) - - ts, handler, repo := createServiceSummaryRepo(t, req) - defer ts.Close() - - serviceInstances, apiResponse := repo.GetSummariesInCurrentSpace() - assert.True(t, handler.AllRequestsCalled()) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, 1, len(serviceInstances)) - - instance1 := serviceInstances[0] - assert.Equal(t, instance1.Name, "my-service-instance") - assert.Equal(t, instance1.ServicePlan.Name, "spark") - assert.Equal(t, instance1.ServiceOffering.Label, "cleardb") - assert.Equal(t, instance1.ServiceOffering.Label, "cleardb") - assert.Equal(t, instance1.ServiceOffering.Provider, "cleardb-provider") - assert.Equal(t, instance1.ServiceOffering.Version, "n/a") - assert.Equal(t, len(instance1.ApplicationNames), 2) - assert.Equal(t, instance1.ApplicationNames[0], "app1") - assert.Equal(t, instance1.ApplicationNames[1], "app2") -} - -func createServiceSummaryRepo(t *testing.T, req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceSummaryRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - SpaceFields: space, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerServiceSummaryRepository(config, gateway) - return -} diff --git a/src/cf/api/services.go b/src/cf/api/services.go deleted file mode 100644 index fcc9e40b4d1..00000000000 --- a/src/cf/api/services.go +++ /dev/null @@ -1,213 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedServiceOfferingResources struct { - Resources []ServiceOfferingResource -} - -type ServiceOfferingResource struct { - Metadata Metadata - Entity ServiceOfferingEntity -} - -func (resource ServiceOfferingResource) ToFields() (fields cf.ServiceOfferingFields) { - fields.Label = resource.Entity.Label - fields.Version = resource.Entity.Version - fields.Provider = resource.Entity.Provider - fields.Description = resource.Entity.Description - fields.Guid = resource.Metadata.Guid - fields.DocumentationUrl = resource.Entity.DocumentationUrl - return -} - -func (resource ServiceOfferingResource) ToModel() (offering cf.ServiceOffering) { - offering.ServiceOfferingFields = resource.ToFields() - for _, p := range resource.Entity.ServicePlans { - servicePlan := cf.ServicePlanFields{} - servicePlan.Name = p.Entity.Name - servicePlan.Guid = p.Metadata.Guid - offering.Plans = append(offering.Plans, servicePlan) - } - return offering -} - -type ServiceOfferingEntity struct { - Label string - Version string - Description string - DocumentationUrl string `json:"documentation_url"` - Provider string - ServicePlans []ServicePlanResource `json:"service_plans"` -} - -type ServicePlanResource struct { - Metadata Metadata - Entity ServicePlanEntity -} - -func (resource ServicePlanResource) ToFields() (fields cf.ServicePlanFields) { - fields.Guid = resource.Metadata.Guid - fields.Name = resource.Entity.Name - return -} - -type ServicePlanEntity struct { - Name string - ServiceOffering ServiceOfferingResource `json:"service"` -} - -type PaginatedServiceInstanceResources struct { - Resources []ServiceInstanceResource -} - -type ServiceInstanceResource struct { - Metadata Metadata - Entity ServiceInstanceEntity -} - -func (resource ServiceInstanceResource) ToFields() (fields cf.ServiceInstanceFields) { - fields.Guid = resource.Metadata.Guid - fields.Name = resource.Entity.Name - return -} - -func (resource ServiceInstanceResource) ToModel() (instance cf.ServiceInstance) { - instance.ServiceInstanceFields = resource.ToFields() - instance.ServicePlan = resource.Entity.ServicePlan.ToFields() - instance.ServiceOffering = resource.Entity.ServicePlan.Entity.ServiceOffering.ToFields() - - instance.ServiceBindings = []cf.ServiceBindingFields{} - for _, bindingResource := range resource.Entity.ServiceBindings { - instance.ServiceBindings = append(instance.ServiceBindings, bindingResource.ToFields()) - } - return -} - -type ServiceInstanceEntity struct { - Name string - ServiceBindings []ServiceBindingResource `json:"service_bindings"` - ServicePlan ServicePlanResource `json:"service_plan"` -} - -type ServiceBindingResource struct { - Metadata Metadata - Entity ServiceBindingEntity -} - -func (resource ServiceBindingResource) ToFields() (fields cf.ServiceBindingFields) { - fields.Url = resource.Metadata.Url - fields.Guid = resource.Metadata.Guid - fields.AppGuid = resource.Entity.AppGuid - return -} - -type ServiceBindingEntity struct { - AppGuid string `json:"app_guid"` -} - -type ServiceRepository interface { - GetServiceOfferings() (offerings []cf.ServiceOffering, apiResponse net.ApiResponse) - FindInstanceByName(name string) (instance cf.ServiceInstance, apiResponse net.ApiResponse) - CreateServiceInstance(name, planGuid string) (identicalAlreadyExists bool, apiResponse net.ApiResponse) - RenameService(instance cf.ServiceInstance, newName string) (apiResponse net.ApiResponse) - DeleteService(instance cf.ServiceInstance) (apiResponse net.ApiResponse) -} - -type CloudControllerServiceRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerServiceRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerServiceRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerServiceRepository) GetServiceOfferings() (offerings []cf.ServiceOffering, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/services?inline-relations-depth=1", repo.config.Target) - spaceGuid := repo.config.SpaceFields.Guid - - if spaceGuid != "" { - path = fmt.Sprintf("%s/v2/spaces/%s/services?inline-relations-depth=1", repo.config.Target, spaceGuid) - } - - resources := new(PaginatedServiceOfferingResources) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - for _, r := range resources.Resources { - offerings = append(offerings, r.ToModel()) - } - - return -} - -func (repo CloudControllerServiceRepository) FindInstanceByName(name string) (instance cf.ServiceInstance, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s/service_instances?return_user_provided_service_instances=true&q=name%s&inline-relations-depth=2", repo.config.Target, repo.config.SpaceFields.Guid, "%3A"+name) - - resources := new(PaginatedServiceInstanceResources) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - if len(resources.Resources) == 0 { - apiResponse = net.NewNotFoundApiResponse("Service instance %s not found", name) - return - } - - resource := resources.Resources[0] - instance = resource.ToModel() - return -} - -func (repo CloudControllerServiceRepository) CreateServiceInstance(name, planGuid string) (identicalAlreadyExists bool, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/service_instances", repo.config.Target) - data := fmt.Sprintf( - `{"name":"%s","service_plan_guid":"%s","space_guid":"%s"}`, - name, planGuid, repo.config.SpaceFields.Guid, - ) - - apiResponse = repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(data)) - - if apiResponse.IsNotSuccessful() && apiResponse.ErrorCode == cf.SERVICE_INSTANCE_NAME_TAKEN { - - serviceInstance, findInstanceApiResponse := repo.FindInstanceByName(name) - - if !findInstanceApiResponse.IsNotSuccessful() && - serviceInstance.ServicePlan.Guid == planGuid { - apiResponse = net.ApiResponse{} - identicalAlreadyExists = true - return - } - } - return -} - -func (repo CloudControllerServiceRepository) RenameService(instance cf.ServiceInstance, newName string) (apiResponse net.ApiResponse) { - body := fmt.Sprintf(`{"name":"%s"}`, newName) - path := fmt.Sprintf("%s/v2/service_instances/%s", repo.config.Target, instance.Guid) - - if instance.IsUserProvided() { - path = fmt.Sprintf("%s/v2/user_provided_service_instances/%s", repo.config.Target, instance.Guid) - } - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerServiceRepository) DeleteService(instance cf.ServiceInstance) (apiResponse net.ApiResponse) { - if len(instance.ServiceBindings) > 0 { - return net.NewApiResponseWithMessage("Cannot delete service instance, apps are still bound to it") - } - path := fmt.Sprintf("%s/v2/service_instances/%s", repo.config.Target, instance.Guid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} diff --git a/src/cf/api/services_test.go b/src/cf/api/services_test.go deleted file mode 100644 index 9aea61ee6b2..00000000000 --- a/src/cf/api/services_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -var multipleOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "resources": [ - { - "metadata": { - "guid": "offering-1-guid" - }, - "entity": { - "label": "Offering 1", - "provider": "Offering 1 provider", - "description": "Offering 1 description", - "version" : "1.0", - "service_plans": [ - { - "metadata": {"guid": "offering-1-plan-1-guid"}, - "entity": {"name": "Offering 1 Plan 1"} - }, - { - "metadata": {"guid": "offering-1-plan-2-guid"}, - "entity": {"name": "Offering 1 Plan 2"} - } - ] - } - }, - { - "metadata": { - "guid": "offering-2-guid" - }, - "entity": { - "label": "Offering 2", - "provider": "Offering 2 provider", - "description": "Offering 2 description", - "version" : "1.5", - "service_plans": [ - { - "metadata": {"guid": "offering-2-plan-1-guid"}, - "entity": {"name": "Offering 2 Plan 1"} - } - ] - } - } - ] -}`} - -func testGetServiceOfferings(t *testing.T, req testnet.TestRequest, config *configuration.Configuration) { - ts, handler, repo := createServiceRepoWithConfig(t, []testnet.TestRequest{req}, config) - defer ts.Close() - - offerings, apiResponse := repo.GetServiceOfferings() - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, 2, len(offerings)) - - firstOffering := offerings[0] - assert.Equal(t, firstOffering.Label, "Offering 1") - assert.Equal(t, firstOffering.Version, "1.0") - assert.Equal(t, firstOffering.Description, "Offering 1 description") - assert.Equal(t, firstOffering.Provider, "Offering 1 provider") - assert.Equal(t, firstOffering.Guid, "offering-1-guid") - assert.Equal(t, len(firstOffering.Plans), 2) - - plan := firstOffering.Plans[0] - assert.Equal(t, plan.Name, "Offering 1 Plan 1") - assert.Equal(t, plan.Guid, "offering-1-plan-1-guid") - - secondOffering := offerings[1] - assert.Equal(t, secondOffering.Label, "Offering 2") - assert.Equal(t, secondOffering.Guid, "offering-2-guid") - assert.Equal(t, len(secondOffering.Plans), 1) -} - -func TestGetServiceOfferingsWhenNotTargetingASpace(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/services?inline-relations-depth=1", - Response: multipleOfferingsResponse, - }) - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - } - testGetServiceOfferings(t, req, config) -} - -func TestGetServiceOfferingsWhenTargetingASpace(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/services?inline-relations-depth=1", - Response: multipleOfferingsResponse, - }) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - SpaceFields: space, - } - testGetServiceOfferings(t, req, config) -} - -func TestCreateServiceInstance(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_instances", - Matcher: testnet.RequestBodyMatcher(`{"name":"instance-name","service_plan_guid":"plan-guid","space_guid":"my-space-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - identicalAlreadyExists, apiResponse := repo.CreateServiceInstance("instance-name", "plan-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, identicalAlreadyExists, false) -} - -func TestCreateServiceInstanceWhenIdenticalServiceAlreadyExists(t *testing.T) { - errorReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_instances", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"plan-guid","space_guid":"my-space-guid"}`), - Response: testnet.TestResponse{ - Status: http.StatusBadRequest, - Body: `{"code":60002,"description":"The service instance name is taken: my-service"}`, - }, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{errorReq, findServiceInstanceReq}) - defer ts.Close() - - identicalAlreadyExists, apiResponse := repo.CreateServiceInstance("my-service", "plan-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, identicalAlreadyExists, true) -} - -func TestCreateServiceInstanceWhenDifferentServiceAlreadyExists(t *testing.T) { - errorReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/service_instances", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"different-plan-guid","space_guid":"my-space-guid"}`), - Response: testnet.TestResponse{ - Status: http.StatusBadRequest, - Body: `{"code":60002,"description":"The service instance name is taken: my-service"}`, - }, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{errorReq, findServiceInstanceReq}) - defer ts.Close() - - identicalAlreadyExists, apiResponse := repo.CreateServiceInstance("my-service", "different-plan-guid") - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, identicalAlreadyExists, false) -} - -var findServiceInstanceReq = testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": [ - { - "metadata": { - "guid": "my-service-instance-guid" - }, - "entity": { - "name": "my-service", - "service_bindings": [ - { - "metadata": { - "guid": "service-binding-1-guid", - "url": "/v2/service_bindings/service-binding-1-guid" - }, - "entity": { - "app_guid": "app-1-guid" - } - }, - { - "metadata": { - "guid": "service-binding-2-guid", - "url": "/v2/service_bindings/service-binding-2-guid" - }, - "entity": { - "app_guid": "app-2-guid" - } - } - ], - "service_plan": { - "metadata": { - "guid": "plan-guid" - }, - "entity": { - "name": "plan-name", - "service": { - "metadata": { - "guid": "service-guid" - }, - "entity": { - "label": "mysql", - "description": "MySQL database", - "documentation_url": "http://info.example.com" - } - } - } - } - } - } - ]}`}}) - -func TestFindInstanceByName(t *testing.T) { - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{findServiceInstanceReq}) - defer ts.Close() - - instance, apiResponse := repo.FindInstanceByName("my-service") - - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, instance.Name, "my-service") - assert.Equal(t, instance.Guid, "my-service-instance-guid") - assert.Equal(t, instance.ServiceOffering.Label, "mysql") - assert.Equal(t, instance.ServiceOffering.DocumentationUrl, "http://info.example.com") - assert.Equal(t, instance.ServiceOffering.Description, "MySQL database") - assert.Equal(t, instance.ServicePlan.Name, "plan-name") - assert.Equal(t, len(instance.ServiceBindings), 2) - - binding := instance.ServiceBindings[0] - assert.Equal(t, binding.Url, "/v2/service_bindings/service-binding-1-guid") - assert.Equal(t, binding.Guid, "service-binding-1-guid") - assert.Equal(t, binding.AppGuid, "app-1-guid") -} - -func TestFindInstanceByNameForNonExistentService(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`}, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - _, apiResponse := repo.FindInstanceByName("my-service") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestDeleteServiceWithoutServiceBindings(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/service_instances/my-service-instance-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - apiResponse := repo.DeleteService(serviceInstance) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestDeleteServiceWithServiceBindings(t *testing.T) { - _, _, repo := createServiceRepo(t, []testnet.TestRequest{}) - - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - - binding := cf.ServiceBindingFields{} - binding.Url = "/v2/service_bindings/service-binding-1-guid" - binding.AppGuid = "app-1-guid" - - binding2 := cf.ServiceBindingFields{} - binding2.Url = "/v2/service_bindings/service-binding-2-guid" - binding2.AppGuid = "app-2-guid" - - serviceInstance.ServiceBindings = []cf.ServiceBindingFields{binding, binding2} - - apiResponse := repo.DeleteService(serviceInstance) - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, apiResponse.Message, "Cannot delete service instance, apps are still bound to it") -} - -func TestRenameService(t *testing.T) { - path := "/v2/service_instances/my-service-instance-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - - plan := cf.ServicePlanFields{} - plan.Guid = "some-plan-guid" - serviceInstance.ServicePlan = plan - - testRenameService(t, path, serviceInstance) -} - -func TestRenameServiceWhenServiceIsUserProvided(t *testing.T) { - path := "/v2/user_provided_service_instances/my-service-instance-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Guid = "my-service-instance-guid" - testRenameService(t, path, serviceInstance) -} - -func testRenameService(t *testing.T, endpointPath string, serviceInstance cf.ServiceInstance) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: endpointPath, - Matcher: testnet.RequestBodyMatcher(`{"name":"new-name"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createServiceRepo(t, []testnet.TestRequest{req}) - defer ts.Close() - - apiResponse := repo.RenameService(serviceInstance, "new-name") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createServiceRepo(t *testing.T, reqs []testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceRepository) { - space2 := cf.SpaceFields{} - space2.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - SpaceFields: space2, - } - return createServiceRepoWithConfig(t, reqs, config) -} - -func createServiceRepoWithConfig(t *testing.T, reqs []testnet.TestRequest, config *configuration.Configuration) (ts *httptest.Server, handler *testnet.TestHandler, repo ServiceRepository) { - if len(reqs) > 0 { - ts, handler = testnet.NewTLSServer(t, reqs) - config.Target = ts.URL - } - - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerServiceRepository(config, gateway) - return -} diff --git a/src/cf/api/spaces.go b/src/cf/api/spaces.go deleted file mode 100644 index be8970783ad..00000000000 --- a/src/cf/api/spaces.go +++ /dev/null @@ -1,162 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "strings" -) - -type PaginatedSpaceResources struct { - Resources []SpaceResource - NextUrl string `json:"next_url"` -} - -type SpaceResource struct { - Metadata Metadata - Entity SpaceEntity -} - -func (resource SpaceResource) ToFields() (fields cf.SpaceFields) { - fields.Guid = resource.Metadata.Guid - fields.Name = resource.Entity.Name - return -} - -func (resource SpaceResource) ToModel() (space cf.Space) { - space.SpaceFields = resource.ToFields() - for _, app := range resource.Entity.Applications { - space.Applications = append(space.Applications, app.ToFields()) - } - - for _, domainResource := range resource.Entity.Domains { - space.Domains = append(space.Domains, domainResource.ToFields()) - } - - for _, serviceResource := range resource.Entity.ServiceInstances { - space.ServiceInstances = append(space.ServiceInstances, serviceResource.ToFields()) - } - - space.Organization = resource.Entity.Organization.ToFields() - return -} - -type SpaceEntity struct { - Name string - Organization OrganizationResource - Applications []ApplicationResource `json:"apps"` - Domains []DomainResource - ServiceInstances []ServiceInstanceResource `json:"service_instances"` -} - -type SpaceRepository interface { - ListSpaces(stop chan bool) (spacesChan chan []cf.Space, statusChan chan net.ApiResponse) - FindByName(name string) (space cf.Space, apiResponse net.ApiResponse) - FindByNameInOrg(name, orgGuid string) (space cf.Space, apiResponse net.ApiResponse) - Create(name string) (apiResponse net.ApiResponse) - Rename(spaceGuid, newName string) (apiResponse net.ApiResponse) - Delete(spaceGuid string) (apiResponse net.ApiResponse) -} - -type CloudControllerSpaceRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerSpaceRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerSpaceRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerSpaceRepository) ListSpaces(stop chan bool) (spacesChan chan []cf.Space, statusChan chan net.ApiResponse) { - spacesChan = make(chan []cf.Space, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - path := fmt.Sprintf("/v2/organizations/%s/spaces", repo.config.OrganizationFields.Guid) - - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - spaces []cf.Space - apiResponse net.ApiResponse - ) - spaces, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(spacesChan) - close(statusChan) - return - } - - if len(spaces) > 0 { - spacesChan <- spaces - } - } - } - close(spacesChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerSpaceRepository) FindByName(name string) (space cf.Space, apiResponse net.ApiResponse) { - return repo.FindByNameInOrg(name, repo.config.OrganizationFields.Guid) -} - -func (repo CloudControllerSpaceRepository) FindByNameInOrg(name, orgGuid string) (space cf.Space, apiResponse net.ApiResponse) { - path := fmt.Sprintf("/v2/organizations/%s/spaces?q=name%%3A%s&inline-relations-depth=1", orgGuid, strings.ToLower(name)) - - spaces, _, apiResponse := repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(spaces) == 0 { - apiResponse = net.NewNotFoundApiResponse("%s %s not found", "Space", name) - return - } - - space = spaces[0] - return -} - -func (repo CloudControllerSpaceRepository) findNextWithPath(path string) (spaces []cf.Space, nextUrl string, apiResponse net.ApiResponse) { - resources := new(PaginatedSpaceResources) - apiResponse = repo.gateway.GetResource(repo.config.Target+path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = resources.NextUrl - - for _, r := range resources.Resources { - spaces = append(spaces, r.ToModel()) - } - return -} - -func (repo CloudControllerSpaceRepository) Create(name string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces", repo.config.Target) - body := fmt.Sprintf(`{"name":"%s","organization_guid":"%s"}`, name, repo.config.OrganizationFields.Guid) - return repo.gateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerSpaceRepository) Rename(spaceGuid, newName string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s", repo.config.Target, spaceGuid) - body := fmt.Sprintf(`{"name":"%s"}`, newName) - return repo.gateway.UpdateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerSpaceRepository) Delete(spaceGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/spaces/%s?recursive=true", repo.config.Target, spaceGuid) - return repo.gateway.DeleteResource(path, repo.config.AccessToken) -} diff --git a/src/cf/api/spaces_test.go b/src/cf/api/spaces_test.go deleted file mode 100644 index b499eeb19c7..00000000000 --- a/src/cf/api/spaces_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestSpacesListSpaces(t *testing.T) { - firstPageSpacesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/some-org-guid/spaces", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "next_url": "/v2/organizations/some-org-guid/spaces?page=2", - "resources": [ - { - "metadata": { - "guid": "acceptance-space-guid" - }, - "entity": { - "name": "acceptance" - } - } - ] - }`}}) - - secondPageSpacesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/some-org-guid/spaces?page=2", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "resources": [ - { - "metadata": { - "guid": "staging-space-guid" - }, - "entity": { - "name": "staging" - } - } - ] - }`}}) - - ts, handler, repo := createSpacesRepo(t, firstPageSpacesRequest, secondPageSpacesRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - spacesChan, statusChan := repo.ListSpaces(stopChan) - - spaces := []cf.Space{} - for chunk := range spacesChan { - spaces = append(spaces, chunk...) - } - apiResponse := <-statusChan - - assert.Equal(t, spaces[0].Guid, "acceptance-space-guid") - assert.Equal(t, spaces[1].Guid, "staging-space-guid") - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestSpacesListSpacesWithNoSpaces(t *testing.T) { - emptySpacesRequest := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/organizations/some-org-guid/spaces", - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{"resources": []}`, - }, - }) - - ts, handler, repo := createSpacesRepo(t, emptySpacesRequest) - defer ts.Close() - - stopChan := make(chan bool) - defer close(stopChan) - spacesChan, statusChan := repo.ListSpaces(stopChan) - - _, ok := <-spacesChan - apiResponse := <-statusChan - - assert.False(t, ok) - assert.True(t, apiResponse.IsSuccessful()) - assert.True(t, handler.AllRequestsCalled()) -} - -func TestSpacesFindByName(t *testing.T) { - testSpacesFindByNameWithOrg(t, - "some-org-guid", - func(repo SpaceRepository, spaceName string) (cf.Space, net.ApiResponse) { - return repo.FindByName(spaceName) - }, - ) -} - -func TestSpacesFindByNameInOrg(t *testing.T) { - testSpacesFindByNameWithOrg(t, - "another-org-guid", - func(repo SpaceRepository, spaceName string) (cf.Space, net.ApiResponse) { - return repo.FindByNameInOrg(spaceName, "another-org-guid") - }, - ) -} - -func testSpacesFindByNameWithOrg(t *testing.T, orgGuid string, findByName func(SpaceRepository, string) (cf.Space, net.ApiResponse)) { - findSpaceByNameResponse := testnet.TestResponse{ - Status: http.StatusOK, - Body: ` -{ - "resources": [ - { - "metadata": { - "guid": "space1-guid" - }, - "entity": { - "name": "Space1", - "organization_guid": "org1-guid", - "organization": { - "metadata": { - "guid": "org1-guid" - }, - "entity": { - "name": "Org1" - } - }, - "apps": [ - { - "metadata": { - "guid": "app1-guid" - }, - "entity": { - "name": "app1" - } - }, - { - "metadata": { - "guid": "app2-guid" - }, - "entity": { - "name": "app2" - } - } - ], - "domains": [ - { - "metadata": { - "guid": "domain1-guid" - }, - "entity": { - "name": "domain1" - } - } - ], - "service_instances": [ - { - "metadata": { - "guid": "service1-guid" - }, - "entity": { - "name": "service1" - } - } - ] - } - } - ] -}`} - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: fmt.Sprintf("/v2/organizations/%s/spaces?q=name%%3Aspace1&inline-relations-depth=1", orgGuid), - Response: findSpaceByNameResponse, - }) - - ts, handler, repo := createSpacesRepo(t, request) - defer ts.Close() - - space, apiResponse := findByName(repo, "Space1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, space.Name, "Space1") - assert.Equal(t, space.Guid, "space1-guid") - - assert.Equal(t, space.Organization.Guid, "org1-guid") - - assert.Equal(t, len(space.Applications), 2) - assert.Equal(t, space.Applications[0].Guid, "app1-guid") - assert.Equal(t, space.Applications[1].Guid, "app2-guid") - - assert.Equal(t, len(space.Domains), 1) - assert.Equal(t, space.Domains[0].Guid, "domain1-guid") - - assert.Equal(t, len(space.ServiceInstances), 1) - assert.Equal(t, space.ServiceInstances[0].Guid, "service1-guid") - - assert.True(t, apiResponse.IsSuccessful()) - return -} - -func TestSpacesDidNotFindByName(t *testing.T) { - testSpacesDidNotFindByNameWithOrg(t, - "some-org-guid", - func(repo SpaceRepository, spaceName string) (cf.Space, net.ApiResponse) { - return repo.FindByName(spaceName) - }, - ) -} - -func TestSpacesDidNotFindByNameInOrg(t *testing.T) { - testSpacesDidNotFindByNameWithOrg(t, - "another-org-guid", - func(repo SpaceRepository, spaceName string) (cf.Space, net.ApiResponse) { - return repo.FindByNameInOrg(spaceName, "another-org-guid") - }, - ) -} - -func testSpacesDidNotFindByNameWithOrg(t *testing.T, orgGuid string, findByName func(SpaceRepository, string) (cf.Space, net.ApiResponse)) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: fmt.Sprintf("/v2/organizations/%s/spaces?q=name%%3Aspace1&inline-relations-depth=1", orgGuid), - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: ` { "resources": [ ] }`, - }, - }) - - ts, handler, repo := createSpacesRepo(t, request) - defer ts.Close() - - _, apiResponse := findByName(repo, "Space1") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestCreateSpace(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/spaces", - Matcher: testnet.RequestBodyMatcher(`{"name":"space-name","organization_guid":"some-org-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createSpacesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Create("space-name") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestRenameSpace(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/spaces/my-space-guid", - Matcher: testnet.RequestBodyMatcher(`{"name":"new-space-name"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createSpacesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Rename("my-space-guid", "new-space-name") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestDeleteSpace(t *testing.T) { - request := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/spaces/my-space-guid?recursive=true", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - ts, handler, repo := createSpacesRepo(t, request) - defer ts.Close() - - apiResponse := repo.Delete("my-space-guid") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createSpacesRepo(t *testing.T, reqs ...testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo SpaceRepository) { - ts, handler = testnet.NewTLSServer(t, reqs) - org4 := cf.OrganizationFields{} - org4.Guid = "some-org-guid" - - space5 := cf.SpaceFields{} - space5.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - OrganizationFields: org4, - SpaceFields: space5, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerSpaceRepository(config, gateway) - return -} diff --git a/src/cf/api/stacks.go b/src/cf/api/stacks.go deleted file mode 100644 index b91b8157d5a..00000000000 --- a/src/cf/api/stacks.go +++ /dev/null @@ -1,79 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" -) - -type PaginatedStackResources struct { - Resources []StackResource -} - -type StackResource struct { - Resource - Entity StackEntity -} - -func (resource StackResource) ToFields() (fields cf.Stack) { - fields.Guid = resource.Metadata.Guid - fields.Name = resource.Entity.Name - fields.Description = resource.Entity.Description - return -} - -type StackEntity struct { - Name string - Description string -} - -type StackRepository interface { - FindByName(name string) (stack cf.Stack, apiResponse net.ApiResponse) - FindAll() (stacks []cf.Stack, apiResponse net.ApiResponse) -} - -type CloudControllerStackRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCloudControllerStackRepository(config *configuration.Configuration, gateway net.Gateway) (repo CloudControllerStackRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CloudControllerStackRepository) FindByName(name string) (stack cf.Stack, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/stacks?q=name%s", repo.config.Target, "%3A"+name) - stacks, apiResponse := repo.findAllWithPath(path) - if apiResponse.IsNotSuccessful() { - return - } - - if len(stacks) == 0 { - apiResponse = net.NewApiResponseWithMessage("Stack %s not found", name) - return - } - - stack = stacks[0] - return -} - -func (repo CloudControllerStackRepository) FindAll() (stacks []cf.Stack, apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/stacks", repo.config.Target) - return repo.findAllWithPath(path) -} - -func (repo CloudControllerStackRepository) findAllWithPath(path string) (stacks []cf.Stack, apiResponse net.ApiResponse) { - resources := new(PaginatedStackResources) - apiResponse = repo.gateway.GetResource(path, repo.config.AccessToken, resources) - if apiResponse.IsNotSuccessful() { - return - } - - for _, r := range resources.Resources { - stacks = append(stacks, r.ToFields()) - } - return -} diff --git a/src/cf/api/stacks_test.go b/src/cf/api/stacks_test.go deleted file mode 100644 index 6833ca5e7b0..00000000000 --- a/src/cf/api/stacks_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package api - -import ( - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestStacksFindByName(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/stacks?q=name%3Alinux", - Response: testnet.TestResponse{Status: http.StatusOK, Body: ` { "resources": [ - { - "metadata": { "guid": "custom-linux-guid" }, - "entity": { "name": "custom-linux" } - } - ]}`}}) - - ts, handler, repo := createStackRepo(t, req) - defer ts.Close() - - stack, apiResponse := repo.FindByName("linux") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, stack.Name, "custom-linux") - assert.Equal(t, stack.Guid, "custom-linux-guid") - - stack, apiResponse = repo.FindByName("stack that does not exist") - assert.True(t, apiResponse.IsNotSuccessful()) -} - -var allStacksResponse = testnet.TestResponse{Status: http.StatusOK, Body: ` -{ - "resources": [ - { - "metadata": { - "guid": "50688ae5-9bfc-4bf6-a4bf-caadb21a32c6", - "url": "/v2/stacks/50688ae5-9bfc-4bf6-a4bf-caadb21a32c6", - "created_at": "2013-08-31 01:32:40 +0000", - "updated_at": "2013-08-31 01:32:40 +0000" - }, - "entity": { - "name": "lucid64", - "description": "Ubuntu 10.04" - } - }, - { - "metadata": { - "guid": "e8cda251-7ce8-44b9-becb-ba5f5913d8ba", - "url": "/v2/stacks/e8cda251-7ce8-44b9-becb-ba5f5913d8ba", - "created_at": "2013-08-31 01:32:40 +0000", - "updated_at": "2013-08-31 01:32:40 +0000" - }, - "entity": { - "name": "lucid64custom", - "description": "Fake Ubuntu 10.04" - } - } - ] -}`} - -func TestStacksFindAll(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/v2/stacks", - Response: allStacksResponse, - }) - - ts, handler, repo := createStackRepo(t, req) - defer ts.Close() - - stacks, apiResponse := repo.FindAll() - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) - assert.Equal(t, len(stacks), 2) - assert.Equal(t, stacks[0].Name, "lucid64") - assert.Equal(t, stacks[0].Guid, "50688ae5-9bfc-4bf6-a4bf-caadb21a32c6") -} - -func createStackRepo(t *testing.T, req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo StackRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ts.URL, - } - gateway := net.NewCloudControllerGateway() - repo = NewCloudControllerStackRepository(config, gateway) - return -} diff --git a/src/cf/api/user_provided_service_instances.go b/src/cf/api/user_provided_service_instances.go deleted file mode 100644 index 7ac697a08d7..00000000000 --- a/src/cf/api/user_provided_service_instances.go +++ /dev/null @@ -1,69 +0,0 @@ -package api - -import ( - "bytes" - "cf" - "cf/configuration" - "cf/net" - "encoding/json" - "fmt" -) - -type UserProvidedServiceInstanceRepository interface { - Create(name, drainUrl string, params map[string]string) (apiResponse net.ApiResponse) - Update(serviceInstanceFields cf.ServiceInstanceFields) (apiResponse net.ApiResponse) -} - -type CCUserProvidedServiceInstanceRepository struct { - config *configuration.Configuration - gateway net.Gateway -} - -func NewCCUserProvidedServiceInstanceRepository(config *configuration.Configuration, gateway net.Gateway) (repo CCUserProvidedServiceInstanceRepository) { - repo.config = config - repo.gateway = gateway - return -} - -func (repo CCUserProvidedServiceInstanceRepository) Create(name, drainUrl string, params map[string]string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/user_provided_service_instances", repo.config.Target) - - type RequestBody struct { - Name string `json:"name"` - Credentials map[string]string `json:"credentials"` - SpaceGuid string `json:"space_guid"` - SysLogDrainUrl string `json:"syslog_drain_url"` - } - - jsonBytes, err := json.Marshal(RequestBody{ - Name: name, - Credentials: params, - SpaceGuid: repo.config.SpaceFields.Guid, - SysLogDrainUrl: drainUrl, - }) - - if err != nil { - apiResponse = net.NewApiResponseWithError("Error parsing response", err) - return - } - - return repo.gateway.CreateResource(path, repo.config.AccessToken, bytes.NewReader(jsonBytes)) -} - -func (repo CCUserProvidedServiceInstanceRepository) Update(serviceInstanceFields cf.ServiceInstanceFields) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/user_provided_service_instances/%s", repo.config.Target, serviceInstanceFields.Guid) - - type RequestBody struct { - Credentials map[string]string `json:"credentials,omitempty"` - SysLogDrainUrl string `json:"syslog_drain_url,omitempty"` - } - - reqBody := RequestBody{serviceInstanceFields.Params, serviceInstanceFields.SysLogDrainUrl} - jsonBytes, err := json.Marshal(reqBody) - if err != nil { - apiResponse = net.NewApiResponseWithError("Error parsing response", err) - return - } - - return repo.gateway.UpdateResource(path, repo.config.AccessToken, bytes.NewReader(jsonBytes)) -} diff --git a/src/cf/api/user_provided_service_instances_test.go b/src/cf/api/user_provided_service_instances_test.go deleted file mode 100644 index 40f5c5cd49a..00000000000 --- a/src/cf/api/user_provided_service_instances_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func TestCreateUserProvidedServiceInstance(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/user_provided_service_instances", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-custom-service","credentials":{"host":"example.com","password":"secret","user":"me"},"space_guid":"my-space-guid","syslog_drain_url":""}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createUserProvidedServiceInstanceRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("my-custom-service", "", map[string]string{ - "host": "example.com", - "user": "me", - "password": "secret", - }) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestCreateUserProvidedServiceInstanceWithSyslogDrain(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/user_provided_service_instances", - Matcher: testnet.RequestBodyMatcher(`{"name":"my-custom-service","credentials":{"host":"example.com","password":"secret","user":"me"},"space_guid":"my-space-guid","syslog_drain_url":"syslog://example.com"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createUserProvidedServiceInstanceRepo(t, req) - defer ts.Close() - - apiResponse := repo.Create("my-custom-service", "syslog://example.com", map[string]string{ - "host": "example.com", - "user": "me", - "password": "secret", - }) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestUpdateUserProvidedServiceInstance(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/user_provided_service_instances/my-instance-guid", - Matcher: testnet.RequestBodyMatcher(`{"credentials":{"host":"example.com","password":"secret","user":"me"},"syslog_drain_url":"syslog://example.com"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createUserProvidedServiceInstanceRepo(t, req) - defer ts.Close() - - params := map[string]string{ - "host": "example.com", - "user": "me", - "password": "secret", - } - serviceInstance := cf.ServiceInstanceFields{} - serviceInstance.Guid = "my-instance-guid" - serviceInstance.Params = params - serviceInstance.SysLogDrainUrl = "syslog://example.com" - - apiResponse := repo.Update(serviceInstance) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestUpdateUserProvidedServiceInstanceWithOnlyParams(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/user_provided_service_instances/my-instance-guid", - Matcher: testnet.RequestBodyMatcher(`{"credentials":{"host":"example.com","password":"secret","user":"me"}}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createUserProvidedServiceInstanceRepo(t, req) - defer ts.Close() - - params := map[string]string{ - "host": "example.com", - "user": "me", - "password": "secret", - } - serviceInstance := cf.ServiceInstanceFields{} - serviceInstance.Guid = "my-instance-guid" - serviceInstance.Params = params - apiResponse := repo.Update(serviceInstance) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestUpdateUserProvidedServiceInstanceWithOnlySysLogDrainUrl(t *testing.T) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/user_provided_service_instances/my-instance-guid", - Matcher: testnet.RequestBodyMatcher(`{"syslog_drain_url":"syslog://example.com"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - ts, handler, repo := createUserProvidedServiceInstanceRepo(t, req) - defer ts.Close() - serviceInstance := cf.ServiceInstanceFields{} - serviceInstance.Guid = "my-instance-guid" - serviceInstance.SysLogDrainUrl = "syslog://example.com" - apiResponse := repo.Update(serviceInstance) - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func createUserProvidedServiceInstanceRepo(t *testing.T, req testnet.TestRequest) (ts *httptest.Server, handler *testnet.TestHandler, repo UserProvidedServiceInstanceRepository) { - ts, handler = testnet.NewTLSServer(t, []testnet.TestRequest{req}) - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - SpaceFields: space, - Target: ts.URL, - } - - gateway := net.NewCloudControllerGateway() - repo = NewCCUserProvidedServiceInstanceRepository(config, gateway) - return -} diff --git a/src/cf/api/users.go b/src/cf/api/users.go deleted file mode 100644 index 68335681423..00000000000 --- a/src/cf/api/users.go +++ /dev/null @@ -1,323 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - neturl "net/url" - "strings" -) - -type PaginatedUserResources struct { - NextUrl string `json:"next_url"` - Resources []UserResource -} - -type UserResource struct { - Resource - Entity UserEntity -} - -type UserEntity struct { - Entity - Admin bool -} - -var orgRoleToPathMap = map[string]string{ - cf.ORG_MANAGER: "managers", - cf.BILLING_MANAGER: "billing_managers", - cf.ORG_AUDITOR: "auditors", -} - -var spaceRoleToPathMap = map[string]string{ - cf.SPACE_MANAGER: "managers", - cf.SPACE_DEVELOPER: "developers", - cf.SPACE_AUDITOR: "auditors", -} - -type UserRepository interface { - FindByUsername(username string) (user cf.UserFields, apiResponse net.ApiResponse) - ListUsersInOrgForRole(orgGuid string, role string, stop chan bool) (usersChan chan []cf.UserFields, statusChan chan net.ApiResponse) - ListUsersInSpaceForRole(spaceGuid string, role string, stop chan bool) (usersChan chan []cf.UserFields, statusChan chan net.ApiResponse) - Create(username, password string) (apiResponse net.ApiResponse) - Delete(userGuid string) (apiResponse net.ApiResponse) - SetOrgRole(userGuid, orgGuid, role string) (apiResponse net.ApiResponse) - UnsetOrgRole(userGuid, orgGuid, role string) (apiResponse net.ApiResponse) - SetSpaceRole(userGuid, spaceGuid, orgGuid, role string) (apiResponse net.ApiResponse) - UnsetSpaceRole(userGuid, spaceGuid, role string) (apiResponse net.ApiResponse) -} - -type CloudControllerUserRepository struct { - config *configuration.Configuration - uaaGateway net.Gateway - ccGateway net.Gateway - endpointRepo EndpointRepository -} - -func NewCloudControllerUserRepository(config *configuration.Configuration, uaaGateway net.Gateway, ccGateway net.Gateway, endpointRepo EndpointRepository) (repo CloudControllerUserRepository) { - repo.config = config - repo.uaaGateway = uaaGateway - repo.ccGateway = ccGateway - repo.endpointRepo = endpointRepo - return -} - -func (repo CloudControllerUserRepository) FindByUsername(username string) (user cf.UserFields, apiResponse net.ApiResponse) { - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - usernameFilter := neturl.QueryEscape(fmt.Sprintf(`userName Eq "%s"`, username)) - path := fmt.Sprintf("%s/Users?attributes=id,userName&filter=%s", uaaEndpoint, usernameFilter) - - users, apiResponse := repo.updateOrFindUsersWithUAAPath([]cf.UserFields{}, path) - if len(users) == 0 { - apiResponse = net.NewNotFoundApiResponse("UserFields %s not found", username) - return - } - - user = users[0] - return -} - -func (repo CloudControllerUserRepository) ListUsersInOrgForRole(orgGuid string, roleName string, stop chan bool) (usersChan chan []cf.UserFields, statusChan chan net.ApiResponse) { - path := fmt.Sprintf("/v2/organizations/%s/%s", orgGuid, orgRoleToPathMap[roleName]) - return repo.listUsersForRole(path, roleName, stop) -} - -func (repo CloudControllerUserRepository) ListUsersInSpaceForRole(spaceGuid string, roleName string, stop chan bool) (usersChan chan []cf.UserFields, statusChan chan net.ApiResponse) { - path := fmt.Sprintf("/v2/spaces/%s/%s", spaceGuid, spaceRoleToPathMap[roleName]) - return repo.listUsersForRole(path, roleName, stop) -} - -func (repo CloudControllerUserRepository) listUsersForRole(path string, roleName string, stop chan bool) (usersChan chan []cf.UserFields, statusChan chan net.ApiResponse) { - usersChan = make(chan []cf.UserFields, 4) - statusChan = make(chan net.ApiResponse, 1) - - go func() { - loop: - for path != "" { - select { - case <-stop: - break loop - default: - var ( - users []cf.UserFields - apiResponse net.ApiResponse - ) - - users, path, apiResponse = repo.findNextWithPath(path) - if apiResponse.IsNotSuccessful() { - statusChan <- apiResponse - close(usersChan) - close(statusChan) - return - } - - if len(users) > 0 { - usersChan <- users - } - } - } - close(usersChan) - close(statusChan) - cf.WaitForClose(stop) - }() - - return -} - -func (repo CloudControllerUserRepository) findNextWithPath(path string) (users []cf.UserFields, nextUrl string, apiResponse net.ApiResponse) { - paginatedResources := new(PaginatedUserResources) - - apiResponse = repo.ccGateway.GetResource(repo.config.Target+path, repo.config.AccessToken, paginatedResources) - if apiResponse.IsNotSuccessful() { - return - } - - nextUrl = paginatedResources.NextUrl - - if len(paginatedResources.Resources) == 0 { - return - } - - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - guidFilters := []string{} - for _, r := range paginatedResources.Resources { - users = append(users, cf.UserFields{Guid: r.Metadata.Guid, IsAdmin: r.Entity.Admin}) - guidFilters = append(guidFilters, fmt.Sprintf(`Id eq "%s"`, r.Metadata.Guid)) - } - filter := strings.Join(guidFilters, " or ") - url := fmt.Sprintf("%s/Users?attributes=id,userName&filter=%s", uaaEndpoint, neturl.QueryEscape(filter)) - - users, apiResponse = repo.updateOrFindUsersWithUAAPath(users, url) - return -} - -func (repo CloudControllerUserRepository) updateOrFindUsersWithUAAPath(ccUsers []cf.UserFields, path string) (updatedUsers []cf.UserFields, apiResponse net.ApiResponse) { - type uaaUserResource struct { - Id string - Username string - } - type uaaUserResources struct { - Resources []uaaUserResource - } - - uaaResponse := new(uaaUserResources) - apiResponse = repo.uaaGateway.GetResource(path, repo.config.AccessToken, uaaResponse) - if apiResponse.IsNotSuccessful() { - return - } - - for _, uaaResource := range uaaResponse.Resources { - var ccUserFields cf.UserFields - - for _, u := range ccUsers { - if u.Guid == uaaResource.Id { - ccUserFields = u - break - } - } - - updatedUsers = append(updatedUsers, cf.UserFields{ - Guid: uaaResource.Id, - Username: uaaResource.Username, - IsAdmin: ccUserFields.IsAdmin, - }) - } - return -} - -func (repo CloudControllerUserRepository) Create(username, password string) (apiResponse net.ApiResponse) { - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - path := fmt.Sprintf("%s/Users", uaaEndpoint) - body := fmt.Sprintf(`{ - "userName": "%s", - "emails": [{"value":"%s"}], - "password": "%s", - "name": {"givenName":"%s", "familyName":"%s"} -}`, - username, - username, - password, - username, - username, - ) - request, apiResponse := repo.uaaGateway.NewRequest("POST", path, repo.config.AccessToken, strings.NewReader(body)) - if apiResponse.IsNotSuccessful() { - return - } - - type uaaUserFields struct { - Id string - } - createUserResponse := &uaaUserFields{} - - _, apiResponse = repo.uaaGateway.PerformRequestForJSONResponse(request, createUserResponse) - if apiResponse.IsNotSuccessful() { - return - } - - path = fmt.Sprintf("%s/v2/users", repo.config.Target) - body = fmt.Sprintf(`{"guid":"%s"}`, createUserResponse.Id) - return repo.ccGateway.CreateResource(path, repo.config.AccessToken, strings.NewReader(body)) -} - -func (repo CloudControllerUserRepository) Delete(userGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/users/%s", repo.config.Target, userGuid) - - apiResponse = repo.ccGateway.DeleteResource(path, repo.config.AccessToken) - if apiResponse.IsNotSuccessful() && apiResponse.ErrorCode != cf.USER_NOT_FOUND { - return - } - - uaaEndpoint, apiResponse := repo.endpointRepo.GetEndpoint(cf.UaaEndpointKey) - if apiResponse.IsNotSuccessful() { - return - } - - path = fmt.Sprintf("%s/Users/%s", uaaEndpoint, userGuid) - return repo.uaaGateway.DeleteResource(path, repo.config.AccessToken) -} - -func (repo CloudControllerUserRepository) SetOrgRole(userGuid string, orgGuid string, role string) (apiResponse net.ApiResponse) { - apiResponse = repo.setOrUnsetOrgRole("PUT", userGuid, orgGuid, role) - if apiResponse.IsNotSuccessful() { - return - } - return repo.addOrgUserRole(userGuid, orgGuid) -} - -func (repo CloudControllerUserRepository) UnsetOrgRole(userGuid, orgGuid, role string) (apiResponse net.ApiResponse) { - return repo.setOrUnsetOrgRole("DELETE", userGuid, orgGuid, role) -} - -func (repo CloudControllerUserRepository) setOrUnsetOrgRole(verb, userGuid, orgGuid, role string) (apiResponse net.ApiResponse) { - rolePath, found := orgRoleToPathMap[role] - - if !found { - apiResponse = net.NewApiResponseWithMessage("Invalid Role %s", role) - return - } - - path := fmt.Sprintf("%s/v2/organizations/%s/%s/%s", repo.config.Target, orgGuid, rolePath, userGuid) - - request, apiResponse := repo.ccGateway.NewRequest(verb, path, repo.config.AccessToken, nil) - if apiResponse.IsNotSuccessful() { - return - } - - apiResponse = repo.ccGateway.PerformRequest(request) - if apiResponse.IsNotSuccessful() { - return - } - return -} - -func (repo CloudControllerUserRepository) SetSpaceRole(userGuid, spaceGuid, orgGuid, role string) (apiResponse net.ApiResponse) { - rolePath, apiResponse := repo.checkSpaceRole(userGuid, spaceGuid, role) - if apiResponse.IsNotSuccessful() { - return - } - - apiResponse = repo.addOrgUserRole(userGuid, orgGuid) - if apiResponse.IsNotSuccessful() { - return - } - - return repo.ccGateway.UpdateResource(rolePath, repo.config.AccessToken, nil) -} - -func (repo CloudControllerUserRepository) UnsetSpaceRole(userGuid, spaceGuid, role string) (apiResponse net.ApiResponse) { - rolePath, apiResponse := repo.checkSpaceRole(userGuid, spaceGuid, role) - if apiResponse.IsNotSuccessful() { - return - } - return repo.ccGateway.DeleteResource(rolePath, repo.config.AccessToken) -} - -func (repo CloudControllerUserRepository) checkSpaceRole(userGuid, spaceGuid, role string) (fullPath string, apiResponse net.ApiResponse) { - rolePath, found := spaceRoleToPathMap[role] - - if !found { - apiResponse = net.NewApiResponseWithMessage("Invalid Role %s", role) - } - - fullPath = fmt.Sprintf("%s/v2/spaces/%s/%s/%s", repo.config.Target, spaceGuid, rolePath, userGuid) - return -} - -func (repo CloudControllerUserRepository) addOrgUserRole(userGuid, orgGuid string) (apiResponse net.ApiResponse) { - path := fmt.Sprintf("%s/v2/organizations/%s/users/%s", repo.config.Target, orgGuid, userGuid) - return repo.ccGateway.UpdateResource(path, repo.config.AccessToken, nil) -} diff --git a/src/cf/api/users_test.go b/src/cf/api/users_test.go deleted file mode 100644 index f05468f76ac..00000000000 --- a/src/cf/api/users_test.go +++ /dev/null @@ -1,416 +0,0 @@ -package api - -import ( - "cf" - "cf/configuration" - "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "net/url" - testapi "testhelpers/api" - testnet "testhelpers/net" - "testing" -) - -func createUsersByRoleEndpoints(rolePath string) (ccReqs []testnet.TestRequest, uaaReqs []testnet.TestRequest) { - nextUrl := rolePath + "?page=2" - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: rolePath, - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: fmt.Sprintf(`{ - "next_url": "%s", - "resources": [ {"metadata": {"guid": "user-1-guid"}, "entity": {}} ] - }`, nextUrl)}, - }) - - secondReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: nextUrl, - Response: testnet.TestResponse{ - Status: http.StatusOK, - Body: `{ - "resources": [ {"metadata": {"guid": "user-2-guid"}, "entity": {}}, {"metadata": {"guid": "user-3-guid"}, "entity": {}} ] - }`}, - }) - - ccReqs = append(ccReqs, req, secondReq) - - uaaRoleResponses := []string{ - `{ "resources": [ { "id": "user-1-guid", "userName": "Super user 1" }]}`, - `{ "resources": [ - { "id": "user-2-guid", "userName": "Super user 2" }, - { "id": "user-3-guid", "userName": "Super user 3" } - ]}`, - } - - filters := []string{ - `Id eq "user-1-guid"`, - `Id eq "user-2-guid" or Id eq "user-3-guid"`, - } - - for index, resp := range uaaRoleResponses { - path := fmt.Sprintf( - "/Users?attributes=id,userName&filter=%s", - url.QueryEscape(filters[index]), - ) - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: path, - Response: testnet.TestResponse{Status: http.StatusOK, Body: resp}, - }) - uaaReqs = append(uaaReqs, req) - } - - return -} - -func TestListUsersInOrgForRole(t *testing.T) { - ccReqs, uaaReqs := createUsersByRoleEndpoints("/v2/organizations/my-org-guid/managers") - - cc, ccHandler, uaa, uaaHandler, repo := createUsersRepo(t, ccReqs, uaaReqs) - defer cc.Close() - defer uaa.Close() - - stopChan := make(chan bool) - defer close(stopChan) - usersChan, statusChan := repo.ListUsersInOrgForRole("my-org-guid", cf.ORG_MANAGER, stopChan) - - users := []cf.UserFields{} - for chunk := range usersChan { - users = append(users, chunk...) - } - apiResponse := <-statusChan - - assert.True(t, ccHandler.AllRequestsCalled()) - assert.True(t, uaaHandler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, len(users), 3) - assert.Equal(t, users[0].Guid, "user-1-guid") - assert.Equal(t, users[0].Username, "Super user 1") - assert.Equal(t, users[1].Guid, "user-2-guid") - assert.Equal(t, users[1].Username, "Super user 2") -} - -func TestListUsersInSpaceForRole(t *testing.T) { - ccReqs, uaaReqs := createUsersByRoleEndpoints("/v2/spaces/my-space-guid/managers") - - cc, ccHandler, uaa, uaaHandler, repo := createUsersRepo(t, ccReqs, uaaReqs) - defer cc.Close() - defer uaa.Close() - - stopChan := make(chan bool) - defer close(stopChan) - usersChan, statusChan := repo.ListUsersInSpaceForRole("my-space-guid", cf.SPACE_MANAGER, stopChan) - - users := []cf.UserFields{} - for chunk := range usersChan { - users = append(users, chunk...) - } - apiResponse := <-statusChan - - assert.True(t, ccHandler.AllRequestsCalled()) - assert.True(t, uaaHandler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - assert.Equal(t, len(users), 3) - assert.Equal(t, users[0].Guid, "user-1-guid") - assert.Equal(t, users[0].Username, "Super user 1") - assert.Equal(t, users[1].Guid, "user-2-guid") - assert.Equal(t, users[1].Username, "Super user 2") -} - -func TestFindByUsername(t *testing.T) { - usersResponse := `{ "resources": [ - { "id": "my-guid", "userName": "my-full-username" } - ]}` - - uaaReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/Users?attributes=id,userName&filter=userName+Eq+%22damien%2Buser1%40pivotallabs.com%22", - Response: testnet.TestResponse{Status: http.StatusOK, Body: usersResponse}, - }) - - uaa, handler, repo := createUsersRepoWithoutCCEndpoints(t, []testnet.TestRequest{uaaReq}) - defer uaa.Close() - - user, apiResponse := repo.FindByUsername("damien+user1@pivotallabs.com") - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) - - expectedUserFields := cf.UserFields{} - expectedUserFields.Username = "my-full-username" - expectedUserFields.Guid = "my-guid" - assert.Equal(t, user, expectedUserFields) -} - -func TestFindByUsernameWhenNotFound(t *testing.T) { - uaaReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "GET", - Path: "/Users?attributes=id,userName&filter=userName+Eq+%22my-user%22", - Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, - }) - - uaa, handler, repo := createUsersRepoWithoutCCEndpoints(t, []testnet.TestRequest{uaaReq}) - defer uaa.Close() - - _, apiResponse := repo.FindByUsername("my-user") - assert.True(t, handler.AllRequestsCalled()) - assert.False(t, apiResponse.IsError()) - assert.True(t, apiResponse.IsNotFound()) -} - -func TestCreateUser(t *testing.T) { - ccReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/v2/users", - Matcher: testnet.RequestBodyMatcher(`{"guid":"my-user-guid"}`), - Response: testnet.TestResponse{Status: http.StatusCreated}, - }) - - uaaReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "POST", - Path: "/Users", - Matcher: testnet.RequestBodyMatcher(`{ - "userName":"my-user", - "emails":[{"value":"my-user"}], - "password":"my-password", - "name":{ - "givenName":"my-user", - "familyName":"my-user"} - }`), - Response: testnet.TestResponse{ - Status: http.StatusCreated, - Body: `{"id":"my-user-guid"}`, - }, - }) - - cc, ccHandler, uaa, uaaHandler, repo := createUsersRepo(t, []testnet.TestRequest{ccReq}, []testnet.TestRequest{uaaReq}) - defer cc.Close() - defer uaa.Close() - - apiResponse := repo.Create("my-user", "my-password") - assert.True(t, ccHandler.AllRequestsCalled()) - assert.True(t, uaaHandler.AllRequestsCalled()) - assert.False(t, apiResponse.IsNotSuccessful()) -} - -func TestDeleteUser(t *testing.T) { - ccReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - uaaReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/Users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - cc, ccHandler, uaa, uaaHandler, repo := createUsersRepo(t, []testnet.TestRequest{ccReq}, []testnet.TestRequest{uaaReq}) - defer cc.Close() - defer uaa.Close() - - apiResponse := repo.Delete("my-user-guid") - assert.True(t, ccHandler.AllRequestsCalled()) - assert.True(t, uaaHandler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestDeleteUserWhenNotFoundOnTheCloudController(t *testing.T) { - ccReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/v2/users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusNotFound, Body: `{ - "code": 20003, "description": "The user could not be found" - }`}, - }) - - uaaReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: "/Users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - cc, ccHandler, uaa, uaaHandler, repo := createUsersRepo(t, []testnet.TestRequest{ccReq}, []testnet.TestRequest{uaaReq}) - defer cc.Close() - defer uaa.Close() - - apiResponse := repo.Delete("my-user-guid") - assert.True(t, ccHandler.AllRequestsCalled()) - assert.True(t, uaaHandler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestSetOrgRoleToOrgManager(t *testing.T) { - testSetOrgRoleWithValidRole(t, "OrgManager", "/v2/organizations/my-org-guid/managers/my-user-guid") -} - -func TestSetOrgRoleToBillingManager(t *testing.T) { - testSetOrgRoleWithValidRole(t, "BillingManager", "/v2/organizations/my-org-guid/billing_managers/my-user-guid") -} - -func TestSetOrgRoleToOrgAuditor(t *testing.T) { - testSetOrgRoleWithValidRole(t, "OrgAuditor", "/v2/organizations/my-org-guid/auditors/my-user-guid") -} - -func TestSetOrgRoleWithInvalidRole(t *testing.T) { - repo := createUsersRepoWithoutEndpoints() - apiResponse := repo.SetOrgRole("user-guid", "org-guid", "foo") - - assert.False(t, apiResponse.IsSuccessful()) - assert.Contains(t, apiResponse.Message, "Invalid Role") -} - -func testSetOrgRoleWithValidRole(t *testing.T, role string, path string) { - - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: path, - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - userReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/organizations/my-org-guid/users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - cc, handler, repo := createUsersRepoWithoutUAAEndpoints(t, []testnet.TestRequest{req, userReq}) - defer cc.Close() - - apiResponse := repo.SetOrgRole("my-user-guid", "my-org-guid", role) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestUnsetOrgRoleFromOrgManager(t *testing.T) { - testUnsetOrgRoleWithValidRole(t, "OrgManager", "/v2/organizations/my-org-guid/managers/my-user-guid") -} - -func TestUnsetOrgRoleFromBillingManager(t *testing.T) { - testUnsetOrgRoleWithValidRole(t, "BillingManager", "/v2/organizations/my-org-guid/billing_managers/my-user-guid") -} - -func TestUnsetOrgRoleFromOrgAuditor(t *testing.T) { - testUnsetOrgRoleWithValidRole(t, "OrgAuditor", "/v2/organizations/my-org-guid/auditors/my-user-guid") -} - -func TestUnsetOrgRoleWithInvalidRole(t *testing.T) { - repo := createUsersRepoWithoutEndpoints() - apiResponse := repo.UnsetOrgRole("user-guid", "org-guid", "foo") - - assert.False(t, apiResponse.IsSuccessful()) - assert.Contains(t, apiResponse.Message, "Invalid Role") -} - -func testUnsetOrgRoleWithValidRole(t *testing.T, role string, path string) { - req := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "DELETE", - Path: path, - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - cc, handler, repo := createUsersRepoWithoutUAAEndpoints(t, []testnet.TestRequest{req}) - defer cc.Close() - - apiResponse := repo.UnsetOrgRole("my-user-guid", "my-org-guid", role) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func TestSetSpaceRoleToSpaceManager(t *testing.T) { - testSetSpaceRoleWithValidRole(t, "SpaceManager", "/v2/spaces/my-space-guid/managers/my-user-guid") -} - -func TestSetSpaceRoleToSpaceDeveloper(t *testing.T) { - testSetSpaceRoleWithValidRole(t, "SpaceDeveloper", "/v2/spaces/my-space-guid/developers/my-user-guid") -} - -func TestSetSpaceRoleToSpaceAuditor(t *testing.T) { - testSetSpaceRoleWithValidRole(t, "SpaceAuditor", "/v2/spaces/my-space-guid/auditors/my-user-guid") -} - -func TestSetSpaceRoleWithInvalidRole(t *testing.T) { - repo := createUsersRepoWithoutEndpoints() - apiResponse := repo.SetSpaceRole("user-guid", "space-guid", "org-guid", "foo") - - assert.False(t, apiResponse.IsSuccessful()) - assert.Contains(t, apiResponse.Message, "Invalid Role") -} - -func testSetSpaceRoleWithValidRole(t *testing.T, role string, path string) { - - addToOrgReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: "/v2/organizations/my-org-guid/users/my-user-guid", - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - setRoleReq := testapi.NewCloudControllerTestRequest(testnet.TestRequest{ - Method: "PUT", - Path: path, - Response: testnet.TestResponse{Status: http.StatusOK}, - }) - - cc, handler, repo := createUsersRepoWithoutUAAEndpoints(t, []testnet.TestRequest{addToOrgReq, setRoleReq}) - defer cc.Close() - - apiResponse := repo.SetSpaceRole("my-user-guid", "my-space-guid", "my-org-guid", role) - - assert.True(t, handler.AllRequestsCalled()) - assert.True(t, apiResponse.IsSuccessful()) -} - -func createUsersRepoWithoutEndpoints() (repo UserRepository) { - _, _, _, _, repo = createUsersRepo(nil, []testnet.TestRequest{}, []testnet.TestRequest{}) - return -} - -func createUsersRepoWithoutUAAEndpoints(t *testing.T, ccReqs []testnet.TestRequest) (cc *httptest.Server, ccHandler *testnet.TestHandler, repo UserRepository) { - cc, ccHandler, _, _, repo = createUsersRepo(t, ccReqs, []testnet.TestRequest{}) - return -} - -func createUsersRepoWithoutCCEndpoints(t *testing.T, uaaReqs []testnet.TestRequest) (uaa *httptest.Server, uaaHandler *testnet.TestHandler, repo UserRepository) { - _, _, uaa, uaaHandler, repo = createUsersRepo(t, []testnet.TestRequest{}, uaaReqs) - return -} - -func createUsersRepo(t *testing.T, ccReqs []testnet.TestRequest, uaaReqs []testnet.TestRequest) (cc *httptest.Server, - ccHandler *testnet.TestHandler, uaa *httptest.Server, uaaHandler *testnet.TestHandler, repo UserRepository) { - - ccTarget := "" - uaaTarget := "" - - if len(ccReqs) > 0 { - cc, ccHandler = testnet.NewTLSServer(t, ccReqs) - ccTarget = cc.URL - } - if len(uaaReqs) > 0 { - uaa, uaaHandler = testnet.NewTLSServer(t, uaaReqs) - uaaTarget = uaa.URL - } - org := cf.OrganizationFields{} - org.Guid = "some-org-guid" - config := &configuration.Configuration{ - AccessToken: "BEARER my_access_token", - Target: ccTarget, - OrganizationFields: org, - } - ccGateway := net.NewCloudControllerGateway() - uaaGateway := net.NewUAAGateway() - endpointRepo := &testapi.FakeEndpointRepo{GetEndpointEndpoints: map[cf.EndpointType]string{ - cf.UaaEndpointKey: uaaTarget, - }} - repo = NewCloudControllerUserRepository(config, uaaGateway, ccGateway, endpointRepo) - return -} diff --git a/src/cf/app/app.go b/src/cf/app/app.go deleted file mode 100644 index d61da34d784..00000000000 --- a/src/cf/app/app.go +++ /dev/null @@ -1,789 +0,0 @@ -package app - -import ( - "cf" - "cf/commands" - "cf/terminal" - "fmt" - "github.com/codegangsta/cli" -) - -func NewApp(cmdRunner commands.Runner) (app *cli.App, err error) { - helpCommand := cli.Command{ - Name: "help", - ShortName: "h", - Description: "Show help", - Usage: fmt.Sprintf("%s help [COMMAND]", cf.Name()), - Action: func(c *cli.Context) { - args := c.Args() - if len(args) > 0 { - cli.ShowCommandHelp(c, args[0]) - } else { - showAppHelp(c.App) - } - }, - } - - app = cli.NewApp() - app.Usage = cf.Usage - app.Version = cf.Version - app.Action = helpCommand.Action - app.Commands = []cli.Command{ - helpCommand, - { - Name: "api", - Description: "Set or view target api url", - Usage: fmt.Sprintf("%s api [URL]", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("api", c) - }, - }, - { - Name: "app", - Description: "Display health and status for app", - Usage: fmt.Sprintf("%s app APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("app", c) - }, - }, - { - Name: "apps", - ShortName: "a", - Description: "List all apps in the target space", - Usage: fmt.Sprintf("%s apps", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("apps", c) - }, - }, - { - Name: "auth", - Description: "Authenticate user non-interactively", - Usage: fmt.Sprintf("%s auth USERNAME PASSWORD\n\n", cf.Name()) + - terminal.WarningColor("WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n") + - "EXAMPLE:\n" + - fmt.Sprintf(" %s auth name@example.com \"my password\" (use quotes for passwords with a space)\n", cf.Name()) + - fmt.Sprintf(" %s auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("auth", c) - }, - }, - { - Name: "bind-service", - ShortName: "bs", - Description: "Bind a service instance to an app", - Usage: fmt.Sprintf("%s bind-service APP SERVICE_INSTANCE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("bind-service", c) - }, - }, - { - Name: "buildpacks", - Description: "List all buildpacks", - Usage: fmt.Sprintf("%s buildpacks", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("buildpacks", c) - }, - }, - { - Name: "create-buildpack", - Description: "Create a buildpack", - Usage: fmt.Sprintf("%s create-buildpack BUILDPACK PATH POSITION", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-buildpack", c) - }, - }, - { - Name: "create-domain", - Description: "Create a domain in an org for later use", - Usage: fmt.Sprintf("%s create-domain ORG DOMAIN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-domain", c) - }, - }, - { - Name: "create-org", - ShortName: "co", - Description: "Create an org", - Usage: fmt.Sprintf("%s create-org ORG", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-org", c) - }, - }, - { - Name: "create-route", - Description: "Create a url route in a space for later use", - Usage: fmt.Sprintf("%s create-route SPACE DOMAIN [-n HOSTNAME]", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "n", Value: "", Usage: "Hostname"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-route", c) - }, - }, - { - Name: "create-service", - ShortName: "cs", - Description: "Create a service instance", - Usage: fmt.Sprintf("%s create-service SERVICE PLAN SERVICE_INSTANCE\n\n", cf.Name()) + - "EXAMPLE:\n" + - fmt.Sprintf(" %s create-service cleardb spark clear-db-mine\n\n", cf.Name()) + - "TIP:\n" + - " Use 'cf create-user-provided-service' to make user-provided services available to cf apps", - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-service", c) - }, - }, - { - Name: "create-service-auth-token", - Description: "Create a service auth token", - Usage: fmt.Sprintf("%s create-service-auth-token LABEL PROVIDER TOKEN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-service-auth-token", c) - }, - }, - { - Name: "create-service-broker", - Description: "Create a service broker", - Usage: fmt.Sprintf("%s create-service-broker SERVICE_BROKER USERNAME PASSWORD URL", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-service-broker", c) - }, - }, - { - Name: "create-space", - Description: "Create a space", - Usage: fmt.Sprintf("%s create-space SPACE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-space", c) - }, - }, - { - Name: "create-user", - Description: "Create a new user", - Usage: fmt.Sprintf("%s create-user USERNAME PASSWORD", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-user", c) - }, - }, - { - Name: "create-user-provided-service", - ShortName: "cups", - Description: "Make a user-provided service available to cf apps", - Usage: fmt.Sprintf("%s create-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]\n", cf.Name()) + - "\n Pass comma separated parameter names to enable interactive mode:\n" + - fmt.Sprintf(" %s create-user-provided-service SERVICE_INSTANCE -p \"comma, separated, parameter, names\"\n", cf.Name()) + - "\n Pass parameters as JSON to create a service non-interactively:\n" + - fmt.Sprintf(" %s create-user-provided-service SERVICE_INSTANCE -p '{\"name\":\"value\",\"name\":\"value\"}'\n", cf.Name()) + - "\nEXAMPLE:\n" + - fmt.Sprintf(" %s create-user-provided-service oracle-db-mine -p \"host, port, dbname, username, password\"\n", cf.Name()) + - fmt.Sprintf(" %s create-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n", cf.Name()) + - fmt.Sprintf(" %s create-user-provided-service my-drain-service -l syslog://example.com\n", cf.Name()) + - fmt.Sprintf(" %s create-user-provided-service my-drain-service -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}' -l syslog://example.com", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "p", Value: "", Usage: "Parameters"}, - cli.StringFlag{Name: "l", Value: "", Usage: "Syslog Drain Url"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("create-user-provided-service", c) - }, - }, - { - Name: "delete", - ShortName: "d", - Description: "Delete an app", - Usage: fmt.Sprintf("%s delete -f APP", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete", c) - }, - }, - { - Name: "delete-buildpack", - Description: "Delete a buildpack", - Usage: fmt.Sprintf("%s delete-buildpack BUILDPACK", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-buildpack", c) - }, - }, - { - Name: "delete-domain", - Description: "Delete a domain", - Usage: fmt.Sprintf("%s delete-domain DOMAIN", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-domain", c) - }, - }, - { - Name: "delete-org", - Description: "Delete an org", - Usage: fmt.Sprintf("%s delete-org ORG", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-org", c) - }, - }, - { - Name: "delete-route", - Description: "Delete a route", - Usage: fmt.Sprintf("%s delete-route DOMAIN -n HOSTNAME", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - cli.StringFlag{Name: "n", Usage: "Hostname"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-route", c) - }, - }, - { - Name: "delete-service", - ShortName: "ds", - Description: "Delete a service instance", - Usage: fmt.Sprintf("%s delete-service SERVICE", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-service", c) - }, - }, - { - Name: "delete-service-auth-token", - Description: "Delete a service auth token", - Usage: fmt.Sprintf("%s delete-service-auth-token LABEL PROVIDER", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-service-auth-token", c) - }, - }, - { - Name: "delete-service-broker", - Description: "Delete a service broker", - Usage: fmt.Sprintf("%s delete-service-broker SERVICE_BROKER", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-service-broker", c) - }, - }, - { - Name: "delete-space", - Description: "Delete a space", - Usage: fmt.Sprintf("%s delete-space SPACE", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-space", c) - }, - }, - { - Name: "delete-user", - Description: "Delete a user", - Usage: fmt.Sprintf("%s delete-user USERNAME", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "f", Usage: "Force deletion without confirmation"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("delete-user", c) - }, - }, - { - Name: "domains", - Description: "List domains in the target org", - Usage: fmt.Sprintf("%s domains", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("domains", c) - }, - }, - { - Name: "env", - ShortName: "e", - Description: "Show all env variables for an app", - Usage: fmt.Sprintf("%s env APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("env", c) - }, - }, - { - Name: "events", - Description: "Show recent app events", - Usage: fmt.Sprintf("%s events APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("events", c) - }, - }, - { - Name: "files", - ShortName: "f", - Description: "Print out a list of files in a directory or the contents of a specific file", - Usage: fmt.Sprintf("%s files APP [PATH]", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("files", c) - }, - }, - { - Name: "login", - ShortName: "l", - Description: "Log user in", - Usage: fmt.Sprintf("%s login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE]\n\n", cf.Name()) + - terminal.WarningColor("WARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n\n") + - "EXAMPLE:\n" + - fmt.Sprintf(" %s login (omit username and password to login interactively -- %s will prompt for both)\n", cf.Name(), cf.Name()) + - fmt.Sprintf(" %s login -u name@example.com -p pa55woRD (specify username and password as arguments)\n", cf.Name()) + - fmt.Sprintf(" %s login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n", cf.Name()) + - fmt.Sprintf(" %s login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "a", Value: "", Usage: "API endpoint (for example: https://api.example.com)"}, - cli.StringFlag{Name: "u", Value: "", Usage: "Username"}, - cli.StringFlag{Name: "p", Value: "", Usage: "Password"}, - cli.StringFlag{Name: "o", Value: "", Usage: "Org"}, - cli.StringFlag{Name: "s", Value: "", Usage: "Space"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("login", c) - }, - }, - { - Name: "logout", - ShortName: "lo", - Description: "Log user out", - Usage: fmt.Sprintf("%s logout", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("logout", c) - }, - }, - { - Name: "logs", - Description: "Tail or show recent logs for an app", - Usage: fmt.Sprintf("%s logs APP", cf.Name()), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "recent", Usage: "dump recent logs instead of tailing"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("logs", c) - }, - }, - { - Name: "marketplace", - ShortName: "m", - Description: "List available offerings in the marketplace", - Usage: fmt.Sprintf("%s marketplace", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("marketplace", c) - }, - }, - { - Name: "map-domain", - Description: "Map a domain to a space", - Usage: fmt.Sprintf("%s map-domain SPACE DOMAIN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("map-domain", c) - }, - }, - { - Name: "map-route", - Description: "Add a url route to an app", - Usage: fmt.Sprintf("%s map-route APP DOMAIN [-n HOSTNAME]", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "n", Value: "", Usage: "Hostname"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("map-route", c) - }, - }, - { - Name: "org", - Description: "Show org info", - Usage: fmt.Sprintf("%s org ORG", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("org", c) - }, - }, - { - Name: "org-users", - Description: "Show org users by role", - Usage: fmt.Sprintf("%s org-users ORG", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("org-users", c) - }, - }, - { - Name: "orgs", - ShortName: "o", - Description: "List all orgs", - Usage: fmt.Sprintf("%s orgs", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("orgs", c) - }, - }, - { - Name: "passwd", - ShortName: "pw", - Description: "Change user password", - Usage: fmt.Sprintf("%s passwd", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("passwd", c) - }, - }, - { - Name: "push", - ShortName: "p", - Description: "Push a new app or sync changes to an existing app", - Usage: fmt.Sprintf("%s push APP [-b URL] [-c COMMAND] [-d DOMAIN] [-i NUM_INSTANCES]\n", cf.Name()) + - " [-m MEMORY] [-n HOST] [-p PATH] [-s STACK]\n" + - " [--no-hostname] [--no-route] [--no-start]", - Flags: []cli.Flag{ - cli.StringFlag{Name: "b", Value: "", Usage: "Custom buildpack URL (for example: https://github.com/heroku/heroku-buildpack-play.git)"}, - cli.StringFlag{Name: "c", Value: "", Usage: "Startup command"}, - cli.StringFlag{Name: "d", Value: "", Usage: "Domain (for example: example.com)"}, - cli.IntFlag{Name: "i", Value: 1, Usage: "Number of instances"}, - cli.StringFlag{Name: "m", Value: "128", Usage: "Memory limit (for example: 256, 1G, 1024M)"}, - cli.StringFlag{Name: "n", Value: "", Usage: "Hostname (for example: my-subdomain)"}, - cli.StringFlag{Name: "p", Value: "", Usage: "Path of app directory or zip file"}, - cli.StringFlag{Name: "s", Value: "", Usage: "Stack to use"}, - cli.BoolFlag{Name: "no-hostname", Usage: "Map the root domain to this app"}, - cli.BoolFlag{Name: "no-route", Usage: "Do not map a route to this app"}, - cli.BoolFlag{Name: "no-start", Usage: "Do not start an app after pushing"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("push", c) - }, - }, - { - Name: "quotas", - Description: "List available usage quotas ", - Usage: fmt.Sprintf("%s quotas", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("quotas", c) - }, - }, - { - Name: "rename", - Description: "Rename an app", - Usage: fmt.Sprintf("%s rename APP NEW_APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("rename", c) - }, - }, - { - Name: "rename-org", - Description: "Rename an org", - Usage: fmt.Sprintf("%s rename-org ORG NEW_ORG", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("rename-org", c) - }, - }, - { - Name: "rename-service", - Description: "Rename a service instance", - Usage: fmt.Sprintf("%s rename-service SERVICE_INSTANCE NEW_SERVICE_INSTANCE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("rename-service", c) - }, - }, - { - Name: "rename-service-broker", - Description: "Rename a service broker", - Usage: fmt.Sprintf("%s rename-service-broker SERVICE_BROKER NEW_SERVICE_BROKER", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("rename-service-broker", c) - }, - }, - { - Name: "rename-space", - Description: "Rename a space", - Usage: fmt.Sprintf("%s rename-space SPACE NEW_SPACE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("rename-space", c) - }, - }, - { - Name: "restart", - ShortName: "rs", - Description: "Restart an app", - Usage: fmt.Sprintf("%s restart APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("restart", c) - }, - }, - { - Name: "routes", - ShortName: "r", - Description: "List all routes", - Usage: fmt.Sprintf("%s routes", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("routes", c) - }, - }, - { - Name: "scale", - Description: "Change the instance count and memory limit for an app", - Usage: fmt.Sprintf("%s scale APP -i INSTANCES -m MEMORY", cf.Name()), - Flags: []cli.Flag{ - cli.IntFlag{Name: "i", Value: 0, Usage: "number of instances"}, - cli.StringFlag{Name: "m", Value: "", Usage: "memory limit (e.g. 256M, 1024M, 1G)"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("scale", c) - }, - }, - { - Name: "service", - Description: "Show service instance info", - Usage: fmt.Sprintf("%s service SERVICE_INSTANCE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("service", c) - }, - }, - { - Name: "service-auth-tokens", - Description: "List service auth tokens", - Usage: fmt.Sprintf("%s service-auth-tokens", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("service-auth-tokens", c) - }, - }, - { - Name: "service-brokers", - Description: "List service brokers", - Usage: fmt.Sprintf("%s service-brokers", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("service-brokers", c) - }, - }, - { - Name: "services", - ShortName: "s", - Description: "List all services in the target space", - Usage: fmt.Sprintf("%s services", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("services", c) - }, - }, - { - Name: "set-env", - ShortName: "se", - Description: "Set an env variable for an app", - Usage: fmt.Sprintf("%s set-env APP NAME VALUE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("set-env", c) - }, - }, - { - Name: "set-org-role", - Description: "Assign an org role to a user", - Usage: fmt.Sprintf("%s set-org-role USERNAME ORG ROLE\n\n", cf.Name()) + - "ROLES:\n" + - " OrgManager - Invite and manage users, select and change plans, and set spending limits\n" + - " BillingManager - Create and manage the billing account and payment info\n" + - " OrgAuditor - View logs, reports, and settings on this org and all spaces\n", - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("set-org-role", c) - }, - }, - { - Name: "set-quota", - Description: "Define the quota for an org", - Usage: fmt.Sprintf("%s set-quota ORG QUOTA\n\n", cf.Name()) + - "TIP:\n" + - " Allowable quotas are 'free,' 'paid,' 'runaway,' and 'trial'", - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("set-quota", c) - }, - }, - { - Name: "set-space-role", - Description: "Assign a space role to a user", - Usage: fmt.Sprintf("%s set-space-role USERNAME ORG SPACE ROLE\n\n", cf.Name()) + - "ROLES:\n" + - " SpaceManager - Invite and manage users, and enable features for a given space\n" + - " SpaceDeveloper - Create and manage apps and services, and see logs and reports\n" + - " SpaceAuditor - View logs, reports, and settings on this space\n", - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("set-space-role", c) - }, - }, - { - Name: "share-domain", - Description: "Share a domain with all orgs", - Usage: fmt.Sprintf("%s share-domain DOMAIN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("share-domain", c) - }, - }, - { - Name: "space", - Description: "Show space info", - Usage: fmt.Sprintf("%s space SPACE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("space", c) - }, - }, - { - Name: "space-users", - Description: "Show space users by role", - Usage: fmt.Sprintf("%s space-users ORG SPACE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("space-users", c) - }, - }, - { - Name: "spaces", - Description: "List all spaces in an org", - Usage: fmt.Sprintf("%s spaces", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("spaces", c) - }, - }, - { - Name: "stacks", - Description: "List all stacks", - Usage: fmt.Sprintf("%s stacks", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("stacks", c) - }, - }, - { - Name: "start", - ShortName: "st", - Description: "Start an app", - Usage: fmt.Sprintf("%s start APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("start", c) - }, - }, - { - Name: "stop", - ShortName: "sp", - Description: "Stop an app", - Usage: fmt.Sprintf("%s stop APP", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("stop", c) - }, - }, - { - Name: "target", - ShortName: "t", - Description: "Set or view the targeted org or space", - Usage: fmt.Sprintf("%s target [-o ORG] [-s SPACE]", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "o", Value: "", Usage: "organization"}, - cli.StringFlag{Name: "s", Value: "", Usage: "space"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("target", c) - }, - }, - { - Name: "unbind-service", - ShortName: "us", - Description: "Unbind a service instance from an app", - Usage: fmt.Sprintf("%s unbind-service APP SERVICE_INSTANCE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unbind-service", c) - }, - }, - { - Name: "unmap-domain", - Description: "Unmap a domain from a space", - Usage: fmt.Sprintf("%s unmap-domain SPACE DOMAIN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unmap-domain", c) - }, - }, - { - Name: "unmap-route", - Description: "Remove a url route from an app", - Usage: fmt.Sprintf("%s unmap-route APP DOMAIN [-n HOSTNAME]", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "n", Value: "", Usage: "Hostname"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unmap-route", c) - }, - }, - { - Name: "unset-env", - Description: "Remove an env variable", - Usage: fmt.Sprintf("%s unset-env APP NAME", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unset-env", c) - }, - }, - { - Name: "unset-org-role", - Description: "Remove an org role from a user", - Usage: fmt.Sprintf("%s unset-org-role USERNAME ORG ROLE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unset-org-role", c) - }, - }, - { - Name: "unset-space-role", - Description: "Remove a space role from a user", - Usage: fmt.Sprintf("%s unset-space-role USERNAME ORG SPACE ROLE", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("unset-space-role", c) - }, - }, - { - Name: "update-buildpack", - Description: "Update a buildpack", - Usage: fmt.Sprintf("%s update-buildpack BUILDPACK [-p PATH] [-i POSITION]", cf.Name()), - Flags: []cli.Flag{ - cli.IntFlag{Name: "i", Value: 0, Usage: "Buildpack position among other buildpacks"}, - cli.StringFlag{Name: "p", Value: "", Usage: "Path to directory or zip file"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("update-buildpack", c) - }, - }, - { - Name: "update-service-broker", - Description: "Update a service broker", - Usage: fmt.Sprintf("%s update-service-broker SERVICE_BROKER USERNAME PASSWORD URL", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("update-service-broker", c) - }, - }, - { - Name: "update-service-auth-token", - Description: "Update a service auth token", - Usage: fmt.Sprintf("%s update-service-auth-token LABEL PROVIDER TOKEN", cf.Name()), - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("update-service-auth-token", c) - }, - }, - { - Name: "update-user-provided-service", - ShortName: "uups", - Description: "Update user-provided service name value pairs", - Usage: fmt.Sprintf("%s update-user-provided-service SERVICE_INSTANCE [-p PARAMETERS] [-l SYSLOG-DRAIN-URL]'\n\n", cf.Name()) + - "EXAMPLE:\n" + - fmt.Sprintf(" %s update-user-provided-service oracle-db-mine -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}'\n", cf.Name()) + - fmt.Sprintf(" %s update-user-provided-service my-drain-service -l syslog://example.com\n", cf.Name()) + - fmt.Sprintf(" %s update-user-provided-service my-drain-service -p '{\"username\":\"admin\",\"password\":\"pa55woRD\"}' -l syslog://example.com", cf.Name()), - Flags: []cli.Flag{ - cli.StringFlag{Name: "p", Value: "", Usage: "Parameters"}, - cli.StringFlag{Name: "l", Value: "", Usage: "Syslog Drain Url"}, - }, - Action: func(c *cli.Context) { - cmdRunner.RunCmdByName("update-user-provided-service", c) - }, - }, - } - return -} diff --git a/src/cf/app/app_integration_test.go b/src/cf/app/app_integration_test.go deleted file mode 100644 index ef5c606d6de..00000000000 --- a/src/cf/app/app_integration_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package app - -import ( - "bytes" - "github.com/stretchr/testify/assert" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" -) - -func TestRunningCommands(t *testing.T) { - stdout, _, err := runCommand(t, "api") - assert.NoError(t, err) - assert.Contains(t, stdout, "API endpoint") - - stdout, _, err = runCommand(t, "app") - assert.Error(t, err) - assert.Contains(t, stdout, "FAILED") - - stdout, _, err = runCommand(t, "target", "foo", "bar") - assert.Error(t, err) - assert.Contains(t, stdout, "FAILED") -} - -func TestHelpCommand(t *testing.T) { - helpOutput, _, err := runCommand(t, "help") - assert.NoError(t, err) - - for _, cmdName := range availableCmdNames() { - included := strings.Contains(helpOutput, "\n "+cmdName) - assert.True(t, included, "Could not find command %s in help text", cmdName) - } -} - -func runCommand(t *testing.T, params ...string) (stdout, stderr string, err error) { - currentDir, err := os.Getwd() - assert.NoError(t, err) - sourceFile := filepath.Join(currentDir, "..", "..", "..", "src", "main", "cf.go") - - args := append([]string{"run", sourceFile}, params...) - cmd := exec.Command("go", args...) - - stdoutWriter := bytes.NewBufferString("") - stderrWriter := bytes.NewBufferString("") - cmd.Stdout = stdoutWriter - cmd.Stderr = stderrWriter - - err = cmd.Start() - assert.NoError(t, err) - - err = cmd.Wait() - stdout = string(stdoutWriter.Bytes()) - stderr = string(stderrWriter.Bytes()) - return -} diff --git a/src/cf/app/app_test.go b/src/cf/app/app_test.go deleted file mode 100644 index fe646333979..00000000000 --- a/src/cf/app/app_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package app - -import ( - "cf/api" - "cf/commands" - "cf/configuration" - "cf/net" - "github.com/codegangsta/cli" - "github.com/stretchr/testify/assert" - "strings" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func availableCmdNames() (names []string) { - reqFactory := &testreq.FakeReqFactory{} - cmdRunner := commands.NewRunner(nil, reqFactory) - app, _ := NewApp(cmdRunner) - - for _, cliCmd := range app.Commands { - if cliCmd.Name != "help" { - names = append(names, cliCmd.Name) - } - } - return -} - -type FakeRunner struct { - cmdFactory commands.Factory - t *testing.T - cmdName string -} - -func (runner *FakeRunner) RunCmdByName(cmdName string, c *cli.Context) (err error) { - _, err = runner.cmdFactory.GetByCmdName(cmdName) - if err != nil { - runner.t.Fatal("Error instantiating command with name", cmdName) - return - } - runner.cmdName = cmdName - return -} - -func TestCommands(t *testing.T) { - for _, cmdName := range availableCmdNames() { - ui := &testterm.FakeUI{} - config := &configuration.Configuration{} - configRepo := testconfig.FakeConfigRepository{} - - repoLocator := api.NewRepositoryLocator(config, configRepo, map[string]net.Gateway{ - "auth": net.NewUAAGateway(), - "cloud-controller": net.NewCloudControllerGateway(), - "uaa": net.NewUAAGateway(), - }) - - cmdFactory := commands.NewFactory(ui, config, configRepo, repoLocator) - cmdRunner := &FakeRunner{cmdFactory: cmdFactory, t: t} - app, _ := NewApp(cmdRunner) - app.Run([]string{"", cmdName}) - - assert.Equal(t, cmdRunner.cmdName, cmdName) - } -} - -func TestUsageIncludesCommandName(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - cmdRunner := commands.NewRunner(nil, reqFactory) - app, _ := NewApp(cmdRunner) - for _, cmd := range app.Commands { - assert.Contains(t, strings.Split(cmd.Usage, "\n")[0], cmd.Name) - } -} diff --git a/src/cf/app/help.go b/src/cf/app/help.go deleted file mode 100644 index cd3c759e3e9..00000000000 --- a/src/cf/app/help.go +++ /dev/null @@ -1,263 +0,0 @@ -package app - -import ( - "cf/terminal" - "github.com/codegangsta/cli" - "os" - "strings" - "text/tabwriter" - "text/template" -) - -var appHelpTemplate = `{{.Title "NAME:"}} - {{.Name}} - {{.Usage}} - -{{.Title "USAGE:"}} - [environment variables] {{.Name}} [global options] command [arguments...] [command options] - -{{.Title "VERSION:"}} - {{.Version}} - {{range .Commands}} -{{.SubTitle .Name}}{{range .CommandSubGroups}} -{{range .}} {{.Name}} {{.Description}} -{{end}}{{end}}{{end}} -{{.Title "GLOBAL OPTIONS:"}} - {{range .Flags}}{{.}} - {{end}} -{{.Title "ENVIRONMENT VARIABLES:"}} - CF_TRACE=true - will output HTTP requests and responses during command - HTTP_PROXY=http://proxy.example.com:8080 - set to your proxy -` - -type groupedCommands struct { - Name string - CommandSubGroups [][]cmdPresenter -} - -func (c groupedCommands) SubTitle(name string) string { - return terminal.HeaderColor(name + ":") -} - -type cmdPresenter struct { - Name string - Description string -} - -func newCmdPresenter(app *cli.App, maxNameLen int, cmdName string) (presenter cmdPresenter) { - cmd := app.Command(cmdName) - - presenter.Name = presentCmdName(*cmd) - padding := strings.Repeat(" ", maxNameLen-len(presenter.Name)) - presenter.Name = presenter.Name + padding - - presenter.Description = cmd.Description - - return -} - -func presentCmdName(cmd cli.Command) (name string) { - name = cmd.Name - if cmd.ShortName != "" { - name = name + ", " + cmd.ShortName - } - return -} - -type appPresenter struct { - cli.App - Commands []groupedCommands -} - -func (p appPresenter) Title(name string) string { - return terminal.HeaderColor(name) -} - -func getMaxCmdNameLength(app *cli.App) (length int) { - for _, cmd := range app.Commands { - name := presentCmdName(cmd) - if len(name) > length { - length = len(name) - } - } - return -} - -func newAppPresenter(app *cli.App) (presenter appPresenter) { - maxNameLen := getMaxCmdNameLength(app) - - presenter.Name = app.Name - presenter.Usage = app.Usage - presenter.Version = app.Version - presenter.Name = app.Name - presenter.Flags = app.Flags - - presenter.Commands = []groupedCommands{ - { - Name: "GETTING STARTED", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "login"), - newCmdPresenter(app, maxNameLen, "logout"), - newCmdPresenter(app, maxNameLen, "passwd"), - newCmdPresenter(app, maxNameLen, "target"), - }, { - newCmdPresenter(app, maxNameLen, "api"), - newCmdPresenter(app, maxNameLen, "auth"), - }, - }, - }, { - Name: "APPS", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "apps"), - newCmdPresenter(app, maxNameLen, "app"), - }, { - newCmdPresenter(app, maxNameLen, "push"), - newCmdPresenter(app, maxNameLen, "scale"), - newCmdPresenter(app, maxNameLen, "delete"), - newCmdPresenter(app, maxNameLen, "rename"), - }, { - newCmdPresenter(app, maxNameLen, "start"), - newCmdPresenter(app, maxNameLen, "stop"), - newCmdPresenter(app, maxNameLen, "restart"), - }, { - newCmdPresenter(app, maxNameLen, "events"), - newCmdPresenter(app, maxNameLen, "files"), - newCmdPresenter(app, maxNameLen, "logs"), - }, { - newCmdPresenter(app, maxNameLen, "env"), - newCmdPresenter(app, maxNameLen, "set-env"), - newCmdPresenter(app, maxNameLen, "unset-env"), - }, { - newCmdPresenter(app, maxNameLen, "stacks"), - }, - }, - }, { - Name: "SERVICES", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "marketplace"), - newCmdPresenter(app, maxNameLen, "services"), - newCmdPresenter(app, maxNameLen, "service"), - }, { - newCmdPresenter(app, maxNameLen, "create-service"), - newCmdPresenter(app, maxNameLen, "delete-service"), - newCmdPresenter(app, maxNameLen, "rename-service"), - }, { - newCmdPresenter(app, maxNameLen, "bind-service"), - newCmdPresenter(app, maxNameLen, "unbind-service"), - }, { - newCmdPresenter(app, maxNameLen, "create-user-provided-service"), - newCmdPresenter(app, maxNameLen, "update-user-provided-service"), - }, - }, - }, { - Name: "ORGS", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "orgs"), - newCmdPresenter(app, maxNameLen, "org"), - }, { - newCmdPresenter(app, maxNameLen, "create-org"), - newCmdPresenter(app, maxNameLen, "delete-org"), - newCmdPresenter(app, maxNameLen, "rename-org"), - }, - }, - }, { - Name: "SPACES", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "spaces"), - newCmdPresenter(app, maxNameLen, "space"), - }, { - newCmdPresenter(app, maxNameLen, "create-space"), - newCmdPresenter(app, maxNameLen, "delete-space"), - newCmdPresenter(app, maxNameLen, "rename-space"), - }, - }, - }, { - Name: "DOMAINS", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "domains"), - newCmdPresenter(app, maxNameLen, "create-domain"), - newCmdPresenter(app, maxNameLen, "share-domain"), - newCmdPresenter(app, maxNameLen, "map-domain"), - newCmdPresenter(app, maxNameLen, "unmap-domain"), - newCmdPresenter(app, maxNameLen, "delete-domain"), - }, - }, - }, { - Name: "ROUTES", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "routes"), - newCmdPresenter(app, maxNameLen, "create-route"), - newCmdPresenter(app, maxNameLen, "map-route"), - newCmdPresenter(app, maxNameLen, "unmap-route"), - newCmdPresenter(app, maxNameLen, "delete-route"), - }, - }, - }, { - Name: "BUILDPACKS", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "buildpacks"), - newCmdPresenter(app, maxNameLen, "create-buildpack"), - newCmdPresenter(app, maxNameLen, "update-buildpack"), - newCmdPresenter(app, maxNameLen, "delete-buildpack"), - }, - }, - }, { - Name: "USER ADMIN", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "create-user"), - newCmdPresenter(app, maxNameLen, "delete-user"), - }, { - newCmdPresenter(app, maxNameLen, "org-users"), - newCmdPresenter(app, maxNameLen, "set-org-role"), - newCmdPresenter(app, maxNameLen, "unset-org-role"), - }, { - newCmdPresenter(app, maxNameLen, "space-users"), - newCmdPresenter(app, maxNameLen, "set-space-role"), - newCmdPresenter(app, maxNameLen, "unset-space-role"), - }, - }, - }, { - Name: "ORG ADMIN", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "quotas"), - newCmdPresenter(app, maxNameLen, "set-quota"), - }, - }, - }, { - Name: "SERVICE ADMIN", - CommandSubGroups: [][]cmdPresenter{ - { - newCmdPresenter(app, maxNameLen, "service-auth-tokens"), - newCmdPresenter(app, maxNameLen, "create-service-auth-token"), - newCmdPresenter(app, maxNameLen, "update-service-auth-token"), - newCmdPresenter(app, maxNameLen, "delete-service-auth-token"), - }, { - newCmdPresenter(app, maxNameLen, "service-brokers"), - newCmdPresenter(app, maxNameLen, "create-service-broker"), - newCmdPresenter(app, maxNameLen, "update-service-broker"), - newCmdPresenter(app, maxNameLen, "delete-service-broker"), - newCmdPresenter(app, maxNameLen, "rename-service-broker"), - }, - }, - }, - } - return -} - -func showAppHelp(app *cli.App) { - presenter := newAppPresenter(app) - - w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) - t := template.Must(template.New("help").Parse(appHelpTemplate)) - t.Execute(w, presenter) - w.Flush() -} diff --git a/src/cf/app_constants.go b/src/cf/app_constants.go deleted file mode 100644 index 3609e60c07e..00000000000 --- a/src/cf/app_constants.go +++ /dev/null @@ -1,15 +0,0 @@ -package cf - -import ( - "os" - "path/filepath" -) - -const ( - Version = "6.0.0.rc1-SHA" - Usage = "A command line tool to interact with Cloud Foundry" -) - -func Name() string { - return filepath.Base(os.Args[0]) -} diff --git a/src/cf/app_files.go b/src/cf/app_files.go deleted file mode 100644 index 957b8982ea4..00000000000 --- a/src/cf/app_files.go +++ /dev/null @@ -1,147 +0,0 @@ -package cf - -import ( - "crypto/sha1" - "fileutils" - "fmt" - "os" - "path/filepath" - "strings" -) - -func AppFilesInDir(dir string) (appFiles []AppFileFields, err error) { - err = walkAppFiles(dir, func(fileName string, fullPath string) (err error) { - fileInfo, err := os.Lstat(fullPath) - if err != nil { - return - } - size := fileInfo.Size() - - h := sha1.New() - - err = fileutils.CopyPathToWriter(fullPath, h) - if err != nil { - return - } - - sha1Bytes := h.Sum(nil) - sha1 := fmt.Sprintf("%x", sha1Bytes) - - appFiles = append(appFiles, AppFileFields{ - Path: fileName, - Sha1: sha1, - Size: size, - }) - - return - }) - return -} - -func CopyFiles(appFiles []AppFileFields, fromDir, toDir string) (err error) { - if err != nil { - return - } - - for _, file := range appFiles { - fromPath := filepath.Join(fromDir, file.Path) - toPath := filepath.Join(toDir, file.Path) - err = fileutils.CopyFilePaths(fromPath, toPath) - if err != nil { - return - } - } - return -} - -type walkAppFileFunc func(fileName, fullPath string) (err error) - -func walkAppFiles(dir string, onEachFile walkAppFileFunc) (err error) { - exclusions := readCfIgnore(dir) - - walkFunc := func(fullPath string, f os.FileInfo, inErr error) (err error) { - err = inErr - if err != nil { - return - } - - if f.IsDir() { - return - } - - fileName, _ := filepath.Rel(dir, fullPath) - if fileShouldBeIgnored(exclusions, fileName) { - return - } - - err = onEachFile(fileName, fullPath) - - return - } - - err = filepath.Walk(dir, walkFunc) - return -} - -func fileShouldBeIgnored(exclusions []string, relativePath string) bool { - for _, exclusion := range exclusions { - if exclusion == relativePath { - return true - } - } - return false -} - -func readCfIgnore(dir string) (exclusions []string) { - cfIgnore, err := os.Open(filepath.Join(dir, ".cfignore")) - if err != nil { - return - } - - ignores := strings.Split(fileutils.ReadFile(cfIgnore), "\n") - ignores = append([]string{".cfignore"}, ignores...) - - for _, pattern := range ignores { - pattern = strings.TrimSpace(pattern) - if pattern == "" { - continue - } - pattern = filepath.Clean(pattern) - patternExclusions := exclusionsForPattern(dir, pattern) - exclusions = append(exclusions, patternExclusions...) - } - - return -} - -func exclusionsForPattern(dir string, pattern string) (exclusions []string) { - starting_dir := dir - - findPatternMatches := func(dir string, f os.FileInfo, inErr error) (err error) { - err = inErr - if err != nil { - return - } - - absolutePaths := []string{} - if f.IsDir() && f.Name() == pattern { - absolutePaths, _ = filepath.Glob(filepath.Join(dir, "*")) - } else { - absolutePaths, _ = filepath.Glob(filepath.Join(dir, pattern)) - } - - for _, p := range absolutePaths { - relpath, _ := filepath.Rel(starting_dir, p) - - exclusions = append(exclusions, relpath) - } - return - } - - err := filepath.Walk(dir, findPatternMatches) - if err != nil { - return - } - - return -} diff --git a/src/cf/chan_utils.go b/src/cf/chan_utils.go deleted file mode 100644 index 55f0b9b0031..00000000000 --- a/src/cf/chan_utils.go +++ /dev/null @@ -1,10 +0,0 @@ -package cf - -func WaitForClose(stop chan bool) { - for { - _, open := <-stop - if !open { - break - } - } -} diff --git a/src/cf/commands/api.go b/src/cf/commands/api.go deleted file mode 100644 index 0f292f4ecc6..00000000000 --- a/src/cf/commands/api.go +++ /dev/null @@ -1,67 +0,0 @@ -package commands - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strings" -) - -type Api struct { - ui terminal.UI - endpointRepo api.EndpointRepository - config *configuration.Configuration -} - -type ApiEndpointSetter interface { - SetApiEndpoint(endpoint string) -} - -func NewApi(ui terminal.UI, config *configuration.Configuration, endpointRepo api.EndpointRepository) (cmd Api) { - cmd.ui = ui - cmd.config = config - cmd.endpointRepo = endpointRepo - return -} - -func (cmd Api) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd Api) Run(c *cli.Context) { - if len(c.Args()) == 0 { - cmd.ui.Say( - "API endpoint: %s (API version: %s)", - terminal.EntityNameColor(cmd.config.Target), - terminal.EntityNameColor(cmd.config.ApiVersion), - ) - return - } - - cmd.SetApiEndpoint(c.Args()[0]) -} - -func (cmd Api) SetApiEndpoint(endpoint string) { - if strings.HasSuffix(endpoint, "/") { - endpoint = strings.TrimSuffix(endpoint, "/") - } - - cmd.ui.Say("Setting api endpoint to %s...", terminal.EntityNameColor(endpoint)) - - endpoint, apiResponse := cmd.endpointRepo.UpdateEndpoint(endpoint) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - if !strings.HasPrefix(endpoint, "https://") { - cmd.ui.Say(terminal.WarningColor("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n")) - } - - cmd.ui.ShowConfiguration(cmd.config) -} diff --git a/src/cf/commands/api_test.go b/src/cf/commands/api_test.go deleted file mode 100644 index 42ffe7c2b12..00000000000 --- a/src/cf/commands/api_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package commands_test - -import ( - . "cf/commands" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestApiWithoutArgument(t *testing.T) { - config := &configuration.Configuration{ - Target: "https://api.run.pivotal.io", - ApiVersion: "2.0", - } - endpointRepo := &testapi.FakeEndpointRepo{} - - ui := callApi([]string{}, config, endpointRepo) - - assert.Equal(t, len(ui.Outputs), 1) - assert.Contains(t, ui.Outputs[0], "https://api.run.pivotal.io") - assert.Contains(t, ui.Outputs[0], "2.0") -} - -func TestApiWhenChangingTheEndpoint(t *testing.T) { - endpointRepo := &testapi.FakeEndpointRepo{} - config := &configuration.Configuration{} - - ui := callApi([]string{"http://example.com"}, config, endpointRepo) - - assert.Contains(t, ui.Outputs[0], "Setting api endpoint to") - assert.Equal(t, endpointRepo.UpdateEndpointEndpoint, "http://example.com") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestApiWithTrailingSlash(t *testing.T) { - endpointRepo := &testapi.FakeEndpointRepo{} - config := &configuration.Configuration{} - - ui := callApi([]string{"https://example.com/"}, config, endpointRepo) - - assert.Contains(t, ui.Outputs[0], "Setting api endpoint to") - assert.Equal(t, endpointRepo.UpdateEndpointEndpoint, "https://example.com") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callApi(args []string, config *configuration.Configuration, endpointRepo *testapi.FakeEndpointRepo) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - - cmd := NewApi(ui, config, endpointRepo) - ctxt := testcmd.NewContext("api", args) - reqFactory := &testreq.FakeReqFactory{} - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/delete_app.go b/src/cf/commands/application/delete_app.go deleted file mode 100644 index 7781c6ac602..00000000000 --- a/src/cf/commands/application/delete_app.go +++ /dev/null @@ -1,80 +0,0 @@ -package application - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteApp struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appReq requirements.ApplicationRequirement -} - -func NewDeleteApp(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository) (cmd *DeleteApp) { - cmd = new(DeleteApp) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - return -} - -func (cmd *DeleteApp) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) == 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete") - return - } - - return -} - -func (cmd *DeleteApp) Run(c *cli.Context) { - appName := c.Args()[0] - force := c.Bool("f") - - if !force { - response := cmd.ui.Confirm( - "Really delete %s?%s", - terminal.EntityNameColor(appName), - terminal.PromptColor(">"), - ) - if !response { - return - } - } - - cmd.ui.Say("Deleting app %s in org %s / space %s as %s...", - terminal.EntityNameColor(appName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - app, apiResponse := cmd.appRepo.FindByName(appName) - - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("App %s does not exist.", appName) - return - } - - apiResponse = cmd.appRepo.Delete(app.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - return -} diff --git a/src/cf/commands/application/delete_app_test.go b/src/cf/commands/application/delete_app_test.go deleted file mode 100644 index df809a7461a..00000000000 --- a/src/cf/commands/application/delete_app_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteConfirmingWithY(t *testing.T) { - ui, _, appRepo := deleteApp(t, "y", []string{"app-to-delete"}) - - assert.Equal(t, appRepo.FindByNameName, "app-to-delete") - assert.Equal(t, appRepo.DeletedAppGuid, "app-to-delete-guid") - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "app-to-delete") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteConfirmingWithYes(t *testing.T) { - ui, _, appRepo := deleteApp(t, "Yes", []string{"app-to-delete"}) - - assert.Equal(t, appRepo.FindByNameName, "app-to-delete") - assert.Equal(t, appRepo.DeletedAppGuid, "app-to-delete-guid") - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "app-to-delete") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteWithForceOption(t *testing.T) { - app := cf.Application{} - app.Name = "app-to-delete" - app.Guid = "app-to-delete-guid" - - reqFactory := &testreq.FakeReqFactory{} - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - - ui := &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete", []string{"-f", "app-to-delete"}) - - cmd := NewDeleteApp(ui, &configuration.Configuration{}, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, appRepo.FindByNameName, "app-to-delete") - assert.Equal(t, appRepo.DeletedAppGuid, "app-to-delete-guid") - assert.Equal(t, len(ui.Prompts), 0) - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "app-to-delete") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteAppThatDoesNotExist(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - appRepo := &testapi.FakeApplicationRepository{FindByNameNotFound: true} - - ui := &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete", []string{"-f", "app-to-delete"}) - - cmd := NewDeleteApp(ui, &configuration.Configuration{}, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, appRepo.FindByNameName, "app-to-delete") - assert.Equal(t, appRepo.DeletedAppGuid, "") - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "app-to-delete") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "app-to-delete") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func TestDeleteCommandFailsWithUsage(t *testing.T) { - ui, _, _ := deleteApp(t, "Yes", []string{}) - assert.True(t, ui.FailedWithUsage) - - ui, _, _ = deleteApp(t, "Yes", []string{"app-to-delete"}) - assert.False(t, ui.FailedWithUsage) -} - -func deleteApp(t *testing.T, confirmation string, args []string) (ui *testterm.FakeUI, reqFactory *testreq.FakeReqFactory, appRepo *testapi.FakeApplicationRepository) { - - app := cf.Application{} - app.Name = "app-to-delete" - app.Guid = "app-to-delete-guid" - - reqFactory = &testreq.FakeReqFactory{} - appRepo = &testapi.FakeApplicationRepository{FindByNameApp: app} - ui = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - ctxt := testcmd.NewContext("delete", args) - cmd := NewDeleteApp(ui, config, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/env.go b/src/cf/commands/application/env.go deleted file mode 100644 index fcc88d825d0..00000000000 --- a/src/cf/commands/application/env.go +++ /dev/null @@ -1,61 +0,0 @@ -package application - -import ( - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Env struct { - ui terminal.UI - config *configuration.Configuration - appReq requirements.ApplicationRequirement -} - -func NewEnv(ui terminal.UI, config *configuration.Configuration) (cmd *Env) { - cmd = new(Env) - cmd.ui = ui - cmd.config = config - return -} - -func (cmd *Env) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "env") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.appReq, - } - return -} - -func (cmd *Env) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - - cmd.ui.Say("Getting env variables for app %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - envVars := app.EnvironmentVars - - cmd.ui.Ok() - cmd.ui.Say("") - - if len(envVars) == 0 { - cmd.ui.Say("No env variables exist") - return - } - for key, value := range envVars { - cmd.ui.Say("%s: %s", key, terminal.EntityNameColor(value)) - } -} diff --git a/src/cf/commands/application/env_test.go b/src/cf/commands/application/env_test.go deleted file mode 100644 index f940590ab83..00000000000 --- a/src/cf/commands/application/env_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestEnvRequirements(t *testing.T) { - reqFactory := getEnvDependencies() - - reqFactory.LoginSuccess = true - callEnv(t, []string{"my-app"}, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") - - reqFactory.LoginSuccess = false - callEnv(t, []string{"my-app"}, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestEnvFailsWithUsage(t *testing.T) { - reqFactory := getEnvDependencies() - ui := callEnv(t, []string{}, reqFactory) - - assert.True(t, ui.FailedWithUsage) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestEnvListsEnvironmentVariables(t *testing.T) { - reqFactory := getEnvDependencies() - reqFactory.Application.EnvironmentVars = map[string]string{ - "my-key": "my-value", - "my-key2": "my-value2", - } - - ui := callEnv(t, []string{"my-app"}, reqFactory) - - assert.Contains(t, ui.Outputs[0], "Getting env variables for app") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[3], "my-key") - assert.Contains(t, ui.Outputs[3], "my-value") - assert.Contains(t, ui.Outputs[4], "my-key2") - assert.Contains(t, ui.Outputs[4], "my-value2") -} - -func TestEnvShowsEmptyMessage(t *testing.T) { - reqFactory := getEnvDependencies() - reqFactory.Application.EnvironmentVars = map[string]string{} - - ui := callEnv(t, []string{"my-app"}, reqFactory) - - assert.Contains(t, ui.Outputs[0], "Getting env variables for") - assert.Contains(t, ui.Outputs[0], "my-app") - - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[3], "No env variables exist") -} - -func callEnv(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("env", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewEnv(ui, config) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} - -func getEnvDependencies() (reqFactory *testreq.FakeReqFactory) { - app := cf.Application{} - app.Name = "my-app" - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, Application: app} - return -} diff --git a/src/cf/commands/application/events.go b/src/cf/commands/application/events.go deleted file mode 100644 index 016627af389..00000000000 --- a/src/cf/commands/application/events.go +++ /dev/null @@ -1,83 +0,0 @@ -package application - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" - "strconv" -) - -type Events struct { - ui terminal.UI - config *configuration.Configuration - appReq requirements.ApplicationRequirement - eventsRepo api.AppEventsRepository -} - -func NewEvents(ui terminal.UI, config *configuration.Configuration, eventsRepo api.AppEventsRepository) (cmd *Events) { - cmd = new(Events) - cmd.ui = ui - cmd.config = config - cmd.eventsRepo = eventsRepo - return -} - -func (cmd *Events) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "events") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *Events) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - - cmd.ui.Say("Getting events for app %s in org %s / space %s as %s...\n", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - eventChan, statusChan := cmd.eventsRepo.ListEvents(app.Guid) - table := cmd.ui.Table([]string{"time", "instance", "description", "exit status"}) - noEvents := true - - for events := range eventChan { - rows := [][]string{} - for i := len(events) - 1; i >= 0; i-- { - event := events[i] - rows = append(rows, []string{ - event.Timestamp.Local().Format(TIMESTAMP_FORMAT), - strconv.Itoa(event.InstanceIndex), - event.ExitDescription, - strconv.Itoa(event.ExitStatus), - }) - } - table.Print(rows) - noEvents = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching events.\n%s", apiStatus.Message) - return - } - if noEvents { - cmd.ui.Say("No events for app %s", terminal.EntityNameColor(app.Name)) - return - } -} diff --git a/src/cf/commands/application/events_test.go b/src/cf/commands/application/events_test.go deleted file mode 100644 index e319ab7719b..00000000000 --- a/src/cf/commands/application/events_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" - "time" -) - -func TestEventsRequirements(t *testing.T) { - reqFactory, eventsRepo := getEventsDependencies() - - callEvents(t, []string{"my-app"}, reqFactory, eventsRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestEventsFailsWithUsage(t *testing.T) { - reqFactory, eventsRepo := getEventsDependencies() - ui := callEvents(t, []string{}, reqFactory, eventsRepo) - - assert.True(t, ui.FailedWithUsage) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestEventsSuccess(t *testing.T) { - timestamp, err := time.Parse(TIMESTAMP_FORMAT, "2000-01-01T00:01:11.00-0000") - assert.NoError(t, err) - - reqFactory, eventsRepo := getEventsDependencies() - app := cf.Application{} - app.Name = "my-app" - reqFactory.Application = app - - eventsRepo.Events = []cf.EventFields{ - { - InstanceIndex: 98, - Timestamp: timestamp, - ExitDescription: "app instance exited", - ExitStatus: 78, - }, - { - InstanceIndex: 99, - Timestamp: timestamp, - ExitDescription: "app instance was stopped", - ExitStatus: 77, - }, - } - - ui := callEvents(t, []string{"my-app"}, reqFactory, eventsRepo) - - assert.Contains(t, ui.Outputs[0], "Getting events for app") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "time") - assert.Contains(t, ui.Outputs[1], "instance") - assert.Contains(t, ui.Outputs[1], "description") - assert.Contains(t, ui.Outputs[1], "exit status") - assert.Contains(t, ui.Outputs[2], timestamp.Local().Format(TIMESTAMP_FORMAT)) - assert.Contains(t, ui.Outputs[2], "98") - assert.Contains(t, ui.Outputs[2], "app instance exited") - assert.Contains(t, ui.Outputs[2], "78") - assert.Contains(t, ui.Outputs[3], timestamp.Local().Format(TIMESTAMP_FORMAT)) - assert.Contains(t, ui.Outputs[3], "99") - assert.Contains(t, ui.Outputs[3], "app instance was stopped") - assert.Contains(t, ui.Outputs[3], "77") -} - -func TestEventsWhenNoEventsAvailable(t *testing.T) { - reqFactory, eventsRepo := getEventsDependencies() - app := cf.Application{} - app.Name = "my-app" - reqFactory.Application = app - - ui := callEvents(t, []string{"my-app"}, reqFactory, eventsRepo) - - assert.Contains(t, ui.Outputs[0], "events") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "No events") - assert.Contains(t, ui.Outputs[1], "my-app") -} - -func getEventsDependencies() (reqFactory *testreq.FakeReqFactory, eventsRepo *testapi.FakeAppEventsRepo) { - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - eventsRepo = &testapi.FakeAppEventsRepo{} - return -} - -func callEvents(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, eventsRepo *testapi.FakeAppEventsRepo) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("events", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewEvents(ui, config, eventsRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/files.go b/src/cf/commands/application/files.go deleted file mode 100644 index 61223b1a767..00000000000 --- a/src/cf/commands/application/files.go +++ /dev/null @@ -1,68 +0,0 @@ -package application - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Files struct { - ui terminal.UI - config *configuration.Configuration - appFilesRepo api.AppFilesRepository - appReq requirements.ApplicationRequirement -} - -func NewFiles(ui terminal.UI, config *configuration.Configuration, appFilesRepo api.AppFilesRepository) (cmd *Files) { - cmd = new(Files) - cmd.ui = ui - cmd.config = config - cmd.appFilesRepo = appFilesRepo - return -} - -func (cmd *Files) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "files") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *Files) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - - cmd.ui.Say("Getting files for app %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - path := "/" - if len(c.Args()) > 1 { - path = c.Args()[1] - } - - list, apiResponse := cmd.appFilesRepo.ListFiles(app.Guid, path) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - cmd.ui.Say(list) -} diff --git a/src/cf/commands/application/files_test.go b/src/cf/commands/application/files_test.go deleted file mode 100644 index 214640c5148..00000000000 --- a/src/cf/commands/application/files_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestFilesRequirements(t *testing.T) { - args := []string{"my-app", "/foo"} - appFilesRepo := &testapi.FakeAppFilesRepo{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true, Application: cf.Application{}} - callFiles(t, args, reqFactory, appFilesRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false, Application: cf.Application{}} - callFiles(t, args, reqFactory, appFilesRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: cf.Application{}} - callFiles(t, args, reqFactory, appFilesRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") -} - -func TestFilesFailsWithUsage(t *testing.T) { - appFilesRepo := &testapi.FakeAppFilesRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: cf.Application{}} - ui := callFiles(t, []string{}, reqFactory, appFilesRepo) - - assert.True(t, ui.FailedWithUsage) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListingDirectoryEntries(t *testing.T) { - app := cf.Application{} - app.Name = "my-found-app" - app.Guid = "my-app-guid" - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: app} - appFilesRepo := &testapi.FakeAppFilesRepo{FileList: "file 1\nfile 2"} - - ui := callFiles(t, []string{"my-app", "/foo"}, reqFactory, appFilesRepo) - - assert.Contains(t, ui.Outputs[0], "Getting files for app") - assert.Contains(t, ui.Outputs[0], "my-found-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, appFilesRepo.AppGuid, "my-app-guid") - assert.Equal(t, appFilesRepo.Path, "/foo") - - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[3], "file 1\nfile 2") -} - -func callFiles(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appFilesRepo *testapi.FakeAppFilesRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("files", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewFiles(ui, config, appFilesRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/application/helpers.go b/src/cf/commands/application/helpers.go deleted file mode 100644 index 5245cc5834c..00000000000 --- a/src/cf/commands/application/helpers.go +++ /dev/null @@ -1,151 +0,0 @@ -package application - -import ( - "cf" - "cf/terminal" - "fmt" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "regexp" - "strings" - "time" -) - -const ( - TIMESTAMP_FORMAT = "2006-01-02T15:04:05.00-0700" -) - -func simpleLogMessageOutput(msg *logmessage.Message) (msgText string) { - logMsg := msg.GetLogMessage() - msgText = string(logMsg.GetMessage()) - reg, err := regexp.Compile("[\n\r]+$") - if err != nil { - return - } - msgText = reg.ReplaceAllString(msgText, "") - return -} - -func logMessageOutput(msg *logmessage.Message) string { - logHeader, coloredLogHeader := extractLogHeader(msg) - logMsg := msg.GetLogMessage() - logContent := extractLogContent(logMsg, logHeader) - - return fmt.Sprintf("%s%s", coloredLogHeader, logContent) -} - -func extractLogHeader(msg *logmessage.Message) (logHeader, coloredLogHeader string) { - logMsg := msg.GetLogMessage() - sourceType := msg.GetShortSourceTypeName() - sourceId := logMsg.GetSourceId() - t := time.Unix(0, logMsg.GetTimestamp()) - timeFormat := TIMESTAMP_FORMAT - timeString := t.Format(timeFormat) - - logHeader = fmt.Sprintf("%s [%s]", timeString, sourceType) - coloredLogHeader = terminal.LogSysHeaderColor(logHeader) - - if logMsg.GetSourceType() == logmessage.LogMessage_WARDEN_CONTAINER { - logHeader = fmt.Sprintf("%s [%s/%s]", timeString, sourceType, sourceId) - coloredLogHeader = terminal.LogAppHeaderColor(logHeader) - } - - // Calculate padding - longestHeader := fmt.Sprintf("%s [App/0] ", timeFormat) - expectedHeaderLength := len(longestHeader) - padding := strings.Repeat(" ", expectedHeaderLength-len(logHeader)) - - logHeader = logHeader + padding - coloredLogHeader = coloredLogHeader + padding - - return -} - -func extractLogContent(logMsg *logmessage.LogMessage, logHeader string) (logContent string) { - msgText := string(logMsg.GetMessage()) - reg, err := regexp.Compile("[\n\r]+$") - if err == nil { - msgText = reg.ReplaceAllString(msgText, "") - } - - msgLines := strings.Split(msgText, "\n") - padding := strings.Repeat(" ", len(logHeader)) - coloringFunc := terminal.LogStdoutColor - logType := "OUT" - - if logMsg.GetMessageType() == logmessage.LogMessage_ERR { - coloringFunc = terminal.LogStderrColor - logType = "ERR" - } - - logContent = fmt.Sprintf("%s %s", logType, msgLines[0]) - for _, msgLine := range msgLines[1:] { - logContent = fmt.Sprintf("%s\n%s%s", logContent, padding, msgLine) - } - logContent = coloringFunc(logContent) - - return -} - -func envVarFound(varName string, existingEnvVars map[string]string) (found bool) { - for name, _ := range existingEnvVars { - if name == varName { - found = true - return - } - } - return -} - -func coloredAppState(app cf.ApplicationFields) string { - appState := strings.ToLower(app.State) - - if app.RunningInstances == 0 { - if appState == "stopped" { - return appState - } else { - return terminal.CrashedColor(appState) - } - } - - if app.RunningInstances < app.InstanceCount { - return terminal.WarningColor(appState) - } - - return appState -} - -func coloredAppInstaces(app cf.ApplicationFields) string { - healthString := fmt.Sprintf("%d/%d", app.RunningInstances, app.InstanceCount) - - if app.RunningInstances == 0 { - if strings.ToLower(app.State) == "stopped" { - return healthString - } else { - return terminal.CrashedColor(healthString) - } - } - - if app.RunningInstances < app.InstanceCount { - return terminal.WarningColor(healthString) - } - - return healthString -} - -func coloredInstanceState(instance cf.AppInstanceFields) (colored string) { - state := string(instance.State) - switch state { - case "started", "running": - colored = terminal.StartedColor("running") - case "stopped": - colored = terminal.StoppedColor("stopped") - case "flapping": - colored = terminal.WarningColor("crashing") - case "starting": - colored = terminal.AdvisoryColor("starting") - default: - colored = terminal.FailureColor(state) - } - - return -} diff --git a/src/cf/commands/application/helpers_test.go b/src/cf/commands/application/helpers_test.go deleted file mode 100644 index 8b18655caee..00000000000 --- a/src/cf/commands/application/helpers_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package application - -import ( - "cf/terminal" - "code.google.com/p/gogoprotobuf/proto" - "fmt" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func TestTimestampFormat(t *testing.T) { - assert.Equal(t, TIMESTAMP_FORMAT, "2006-01-02T15:04:05.00-0700") -} - -func TestLogMessageOutput(t *testing.T) { - cloud_controller := logmessage.LogMessage_CLOUD_CONTROLLER - router := logmessage.LogMessage_ROUTER - uaa := logmessage.LogMessage_UAA - dea := logmessage.LogMessage_DEA - wardenContainer := logmessage.LogMessage_WARDEN_CONTAINER - - stdout := logmessage.LogMessage_OUT - stderr := logmessage.LogMessage_ERR - - date := time.Now() - timestamp := date.UnixNano() - - sourceId := "0" - - protoMessage := &logmessage.LogMessage{ - Message: []byte("Hello World!\n\r\n\r"), - AppId: proto.String("my-app-guid"), - MessageType: &stdout, - SourceId: &sourceId, - Timestamp: ×tamp, - } - - msg := createMessage(t, protoMessage, &cloud_controller, &stdout) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [API]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStdoutColor("OUT Hello World!")) - - msg = createMessage(t, protoMessage, &cloud_controller, &stderr) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [API]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStderrColor("ERR Hello World!")) - - sourceId = "1" - msg = createMessage(t, protoMessage, &router, &stdout) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [RTR]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStdoutColor("OUT Hello World!")) - msg = createMessage(t, protoMessage, &router, &stderr) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [RTR]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStderrColor("ERR Hello World!")) - - sourceId = "2" - msg = createMessage(t, protoMessage, &uaa, &stdout) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [UAA]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStdoutColor("OUT Hello World!")) - msg = createMessage(t, protoMessage, &uaa, &stderr) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [UAA]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStderrColor("ERR Hello World!")) - - sourceId = "3" - msg = createMessage(t, protoMessage, &dea, &stdout) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [DEA]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStdoutColor("OUT Hello World!")) - msg = createMessage(t, protoMessage, &dea, &stderr) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [DEA]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStderrColor("ERR Hello World!")) - - sourceId = "4" - msg = createMessage(t, protoMessage, &wardenContainer, &stdout) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [App/4]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStdoutColor("OUT Hello World!")) - msg = createMessage(t, protoMessage, &wardenContainer, &stderr) - assert.Contains(t, logMessageOutput(msg), fmt.Sprintf("%s [App/4]", date.Format(TIMESTAMP_FORMAT))) - assert.Contains(t, logMessageOutput(msg), terminal.LogStderrColor("ERR Hello World!")) -} - -func createMessage(t *testing.T, protoMsg *logmessage.LogMessage, sourceType *logmessage.LogMessage_SourceType, msgType *logmessage.LogMessage_MessageType) (msg *logmessage.Message) { - protoMsg.SourceType = sourceType - protoMsg.MessageType = msgType - - data, err := proto.Marshal(protoMsg) - assert.NoError(t, err) - - msg, err = logmessage.ParseMessage(data) - assert.NoError(t, err) - - return -} diff --git a/src/cf/commands/application/list_apps.go b/src/cf/commands/application/list_apps.go deleted file mode 100644 index b4265a02e1c..00000000000 --- a/src/cf/commands/application/list_apps.go +++ /dev/null @@ -1,72 +0,0 @@ -package application - -import ( - "cf/api" - "cf/configuration" - "cf/formatters" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strings" -) - -type ListApps struct { - ui terminal.UI - config *configuration.Configuration - appSummaryRepo api.AppSummaryRepository -} - -func NewListApps(ui terminal.UI, config *configuration.Configuration, appSummaryRepo api.AppSummaryRepository) (cmd ListApps) { - cmd.ui = ui - cmd.config = config - cmd.appSummaryRepo = appSummaryRepo - return -} - -func (cmd ListApps) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - } - return -} - -func (cmd ListApps) Run(c *cli.Context) { - cmd.ui.Say("Getting apps in org %s / space %s as %s...", - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apps, apiResponse := cmd.appSummaryRepo.GetSummariesInCurrentSpace() - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - []string{"name", "state", "instances", "memory", "disk", "urls"}, - } - - for _, appSummary := range apps { - var urls []string - for _, route := range appSummary.RouteSummaries { - urls = append(urls, route.URL()) - } - - table = append(table, []string{ - appSummary.Name, - coloredAppState(appSummary.ApplicationFields), - coloredAppInstaces(appSummary.ApplicationFields), - formatters.ByteSize(appSummary.Memory * formatters.MEGABYTE), - formatters.ByteSize(appSummary.DiskQuota * formatters.MEGABYTE), - strings.Join(urls, ", "), - }) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/application/list_apps_test.go b/src/cf/commands/application/list_apps_test.go deleted file mode 100644 index d47c8a52bd5..00000000000 --- a/src/cf/commands/application/list_apps_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestApps(t *testing.T) { - domain := cf.DomainFields{} - domain.Name = "cfapps.io" - domain2 := cf.DomainFields{} - domain2.Name = "example.com" - - route1 := cf.RouteSummary{} - route1.Host = "app1" - route1.Domain = domain - - route2 := cf.RouteSummary{} - route2.Host = "app1" - route2.Domain = domain2 - - app1Routes := []cf.RouteSummary{route1, route2} - - domain3 := cf.DomainFields{} - domain3.Name = "cfapps.io" - - route3 := cf.RouteSummary{} - route3.Host = "app2" - route3.Domain = domain3 - - app2Routes := []cf.RouteSummary{route3} - - app := cf.AppSummary{} - app.Name = "Application-1" - app.State = "started" - app.RunningInstances = 1 - app.InstanceCount = 1 - app.Memory = 512 - app.DiskQuota = 1024 - app.RouteSummaries = app1Routes - - app2 := cf.AppSummary{} - app2.Name = "Application-2" - app2.State = "started" - app2.RunningInstances = 1 - app2.InstanceCount = 2 - app2.Memory = 256 - app2.DiskQuota = 1024 - app2.RouteSummaries = app2Routes - - apps := []cf.AppSummary{app, app2} - - appSummaryRepo := &testapi.FakeAppSummaryRepo{ - GetSummariesInCurrentSpaceApps: apps, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - - ui := callApps(t, appSummaryRepo, reqFactory) - - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Contains(t, ui.Outputs[0], "Getting apps in") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "development") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[4], "Application-1") - assert.Contains(t, ui.Outputs[4], "started") - assert.Contains(t, ui.Outputs[4], "1/1") - assert.Contains(t, ui.Outputs[4], "512M") - assert.Contains(t, ui.Outputs[4], "1G") - assert.Contains(t, ui.Outputs[4], "app1.cfapps.io, app1.example.com") - - assert.Contains(t, ui.Outputs[5], "Application-2") - assert.Contains(t, ui.Outputs[5], "started") - assert.Contains(t, ui.Outputs[5], "1/2") - assert.Contains(t, ui.Outputs[5], "256M") - assert.Contains(t, ui.Outputs[5], "1G") - assert.Contains(t, ui.Outputs[5], "app2.cfapps.io") -} - -func TestAppsRequiresLogin(t *testing.T) { - appSummaryRepo := &testapi.FakeAppSummaryRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true} - - callApps(t, appSummaryRepo, reqFactory) - - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestAppsRequiresASelectedSpaceAndOrg(t *testing.T) { - appSummaryRepo := &testapi.FakeAppSummaryRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false} - - callApps(t, appSummaryRepo, reqFactory) - - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func callApps(t *testing.T, appSummaryRepo *testapi.FakeAppSummaryRepo, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - space := cf.SpaceFields{} - space.Name = "development" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - ctxt := testcmd.NewContext("apps", []string{}) - cmd := NewListApps(ui, config, appSummaryRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/application/logs.go b/src/cf/commands/application/logs.go deleted file mode 100644 index ea6c269019b..00000000000 --- a/src/cf/commands/application/logs.go +++ /dev/null @@ -1,105 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/codegangsta/cli" - "time" -) - -type Logs struct { - ui terminal.UI - config *configuration.Configuration - logsRepo api.LogsRepository - appReq requirements.ApplicationRequirement -} - -func NewLogs(ui terminal.UI, config *configuration.Configuration, logsRepo api.LogsRepository) (cmd *Logs) { - cmd = new(Logs) - cmd.ui = ui - cmd.config = config - cmd.logsRepo = logsRepo - return -} - -func (cmd *Logs) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - cmd.ui.FailWithUsage(c, "logs") - err = errors.New("Incorrect Usage") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.appReq, - } - - return -} - -func (cmd *Logs) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - logChan := make(chan *logmessage.Message, 1000) - - go func() { - defer close(logChan) - if c.Bool("recent") { - cmd.recentLogsFor(app, logChan) - } else { - cmd.tailLogsFor(app, logChan) - } - }() - - cmd.displayLogMessages(logChan) -} - -func (cmd *Logs) recentLogsFor(app cf.Application, logChan chan *logmessage.Message) { - onConnect := func() { - cmd.ui.Say("Connected, dumping recent logs for app %s in org %s / space %s as %s...\n", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - } - - err := cmd.logsRepo.RecentLogsFor(app.Guid, onConnect, logChan) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } -} - -func (cmd *Logs) tailLogsFor(app cf.Application, logChan chan *logmessage.Message) { - onConnect := func() { - cmd.ui.Say("Connected, tailing logs for app %s in org %s / space %s as %s...\n", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - } - - // in this case we tail the logs forever, so we never send true on this channel - stopLoggingChan := make(chan bool) - defer close(stopLoggingChan) - - err := cmd.logsRepo.TailLogsFor(app.Guid, onConnect, logChan, stopLoggingChan, 5*time.Second) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } -} - -func (cmd *Logs) displayLogMessages(logChan chan *logmessage.Message) { - for msg := range logChan { - cmd.ui.Say(logMessageOutput(msg)) - } -} diff --git a/src/cf/commands/application/logs_test.go b/src/cf/commands/application/logs_test.go deleted file mode 100644 index 28426d1235d..00000000000 --- a/src/cf/commands/application/logs_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "code.google.com/p/gogoprotobuf/proto" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" - "time" -) - -func TestLogsFailWithUsage(t *testing.T) { - reqFactory, logsRepo := getLogsDependencies() - - fakeUI := callLogs(t, []string{}, reqFactory, logsRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callLogs(t, []string{"foo"}, reqFactory, logsRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestLogsRequirements(t *testing.T) { - reqFactory, logsRepo := getLogsDependencies() - - reqFactory.LoginSuccess = true - callLogs(t, []string{"my-app"}, reqFactory, logsRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") - - reqFactory.LoginSuccess = false - callLogs(t, []string{"my-app"}, reqFactory, logsRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestLogsOutputsRecentLogs(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - - currentTime := time.Now() - messageType := logmessage.LogMessage_ERR - sourceType := logmessage.LogMessage_DEA - logMessage1 := logmessage.LogMessage{ - Message: []byte("Log Line 1"), - AppId: proto.String("my-app"), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(currentTime.UnixNano()), - } - - logMessage2 := logmessage.LogMessage{ - Message: []byte("Log Line 2"), - AppId: proto.String("my-app"), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(currentTime.UnixNano()), - } - - recentLogs := []logmessage.LogMessage{ - logMessage1, - logMessage2, - } - - reqFactory, logsRepo := getLogsDependencies() - reqFactory.Application = app - logsRepo.RecentLogs = recentLogs - - ui := callLogs(t, []string{"--recent", "my-app"}, reqFactory, logsRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, app.Guid, logsRepo.AppLoggedGuid) - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "Connected, dumping recent logs for app") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "Log Line 1") - assert.Contains(t, ui.Outputs[2], "Log Line 2") -} - -func TestLogsTailsTheAppLogs(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - - currentTime := time.Now() - messageType := logmessage.LogMessage_ERR - deaSourceType := logmessage.LogMessage_DEA - deaSourceId := "42" - deaLogMessage := logmessage.LogMessage{ - Message: []byte("Log Line 1"), - AppId: proto.String("my-app"), - MessageType: &messageType, - SourceType: &deaSourceType, - SourceId: &deaSourceId, - Timestamp: proto.Int64(currentTime.UnixNano()), - } - - logs := []logmessage.LogMessage{deaLogMessage} - - reqFactory, logsRepo := getLogsDependencies() - reqFactory.Application = app - logsRepo.TailLogMessages = logs - - ui := callLogs(t, []string{"my-app"}, reqFactory, logsRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, app.Guid, logsRepo.AppLoggedGuid) - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Outputs[0], "Connected, tailing logs for app") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "Log Line 1") -} - -func getLogsDependencies() (reqFactory *testreq.FakeReqFactory, logsRepo *testapi.FakeLogsRepository) { - logsRepo = &testapi.FakeLogsRepository{} - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true} - return -} - -func callLogs(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, logsRepo *testapi.FakeLogsRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("logs", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewLogs(ui, config, logsRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/push.go b/src/cf/commands/application/push.go deleted file mode 100644 index 557cf2c9465..00000000000 --- a/src/cf/commands/application/push.go +++ /dev/null @@ -1,267 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "os" - "strconv" - "strings" -) - -type Push struct { - ui terminal.UI - config *configuration.Configuration - starter ApplicationStarter - stopper ApplicationStopper - appRepo api.ApplicationRepository - domainRepo api.DomainRepository - routeRepo api.RouteRepository - stackRepo api.StackRepository - appBitsRepo api.ApplicationBitsRepository -} - -func NewPush(ui terminal.UI, config *configuration.Configuration, starter ApplicationStarter, stopper ApplicationStopper, - aR api.ApplicationRepository, dR api.DomainRepository, rR api.RouteRepository, sR api.StackRepository, - appBitsRepo api.ApplicationBitsRepository) (cmd Push) { - - cmd.ui = ui - cmd.config = config - cmd.starter = starter - cmd.stopper = stopper - cmd.appRepo = aR - cmd.domainRepo = dR - cmd.routeRepo = rR - cmd.stackRepo = sR - cmd.appBitsRepo = appBitsRepo - return -} - -func (cmd Push) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - } - return -} - -func (cmd Push) Run(c *cli.Context) { - var ( - apiResponse net.ApiResponse - ) - - if len(c.Args()) != 1 { - cmd.ui.FailWithUsage(c, "push") - return - } - - app, didCreate := cmd.getApp(c) - - domain := cmd.domain(c) - hostName := cmd.hostName(app, c) - cmd.bindAppToRoute(app, domain, hostName, didCreate, c) - - cmd.ui.Say("Uploading %s...", terminal.EntityNameColor(app.Name)) - - dir := cmd.path(c) - apiResponse = cmd.appBitsRepo.UploadApp(app.Guid, dir) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - cmd.restart(app, c) -} - -func (cmd Push) getApp(c *cli.Context) (app cf.Application, didCreate bool) { - appName := c.Args()[0] - - app, apiResponse := cmd.appRepo.FindByName(appName) - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if apiResponse.IsNotFound() { - app, apiResponse = cmd.createApp(appName, c) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - didCreate = true - } - - return -} - -func (cmd Push) createApp(appName string, c *cli.Context) (app cf.Application, apiResponse net.ApiResponse) { - buildpackUrl := c.String("b") - instances := c.Int("i") - memory := memoryLimit(c.String("m")) - command := c.String("c") - stackName := c.String("s") - - var stack cf.Stack - if stackName != "" { - stack, apiResponse = cmd.stackRepo.FindByName(stackName) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Say("Using stack %s...", terminal.EntityNameColor(stack.Name)) - } - - cmd.ui.Say("Creating app %s in org %s / space %s as %s...", - terminal.EntityNameColor(appName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - app, apiResponse = cmd.appRepo.Create(appName, buildpackUrl, stack.Guid, command, memory, instances) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - return -} - -func (cmd Push) domain(c *cli.Context) (domain cf.Domain) { - var apiResponse net.ApiResponse - - domainName := c.String("d") - - if domainName != "" { - domain, apiResponse = cmd.domainRepo.FindByNameInCurrentSpace(domainName) - } else { - domain, apiResponse = cmd.domainRepo.FindDefaultAppDomain() - } - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - } - return -} - -func (cmd Push) hostName(app cf.Application, c *cli.Context) (hostName string) { - if !c.Bool("no-hostname") { - hostName = c.String("n") - if hostName == "" { - hostName = app.Name - } - } - return -} - -func (cmd Push) createRoute(hostName string, domain cf.Domain) (route cf.RouteFields) { - cmd.ui.Say("Creating route %s...", terminal.EntityNameColor(domain.UrlForHost(hostName))) - - route, apiResponse := cmd.routeRepo.Create(hostName, domain.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - return -} - -func (cmd Push) bindAppToRoute(app cf.Application, domain cf.Domain, hostName string, didCreate bool, c *cli.Context) { - if c.Bool("no-route") { - return - } - - if len(app.Routes) == 0 && didCreate == false { - cmd.ui.Say("App %s currently exists as a worker, skipping route creation", terminal.EntityNameColor(app.Name)) - return - } - - routeGuid := "" - route, apiResponse := cmd.routeRepo.FindByHostAndDomain(hostName, domain.Name) - if apiResponse.IsNotSuccessful() { - routeGuid = cmd.createRoute(hostName, domain).Guid - } else { - routeGuid = route.Guid - cmd.ui.Say("Using route %s", terminal.EntityNameColor(route.URL())) - } - - for _, boundRoute := range app.Routes { - if boundRoute.Guid == routeGuid { - return - } - } - - cmd.ui.Say("Binding %s to %s...", terminal.EntityNameColor(domain.UrlForHost(hostName)), terminal.EntityNameColor(app.Name)) - - apiResponse = cmd.routeRepo.Bind(routeGuid, app.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") -} - -func (cmd Push) path(c *cli.Context) (dir string) { - dir = c.String("p") - if dir == "" { - var err error - dir, err = os.Getwd() - if err != nil { - cmd.ui.Failed(err.Error()) - return - } - } - return -} - -func (cmd Push) restart(app cf.Application, c *cli.Context) { - updatedApp, _ := cmd.stopper.ApplicationStop(app) - - cmd.ui.Say("") - - if !c.Bool("no-start") { - if buildpackUrl := c.String("b"); buildpackUrl == "" { - cmd.starter.ApplicationStart(updatedApp) - } else { - cmd.starter.ApplicationStartWithBuildpack(updatedApp, buildpackUrl) - } - } -} - -func memoryLimit(arg string) (memory uint64) { - var err error - - switch { - case strings.HasSuffix(arg, "M"): - trimmedArg := arg[:len(arg)-1] - memory, err = strconv.ParseUint(trimmedArg, 10, 0) - case strings.HasSuffix(arg, "G"): - trimmedArg := arg[:len(arg)-1] - memory, err = strconv.ParseUint(trimmedArg, 10, 0) - memory = memory * 1024 - default: - memory, err = strconv.ParseUint(arg, 10, 0) - } - - if err != nil { - memory = 128 - } - - return -} diff --git a/src/cf/commands/application/push_test.go b/src/cf/commands/application/push_test.go deleted file mode 100644 index d0019b6add7..00000000000 --- a/src/cf/commands/application/push_test.go +++ /dev/null @@ -1,545 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - "os" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestPushingRequirements(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - fakeUI := new(testterm.FakeUI) - config := &configuration.Configuration{} - cmd := NewPush(fakeUI, config, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - ctxt := testcmd.NewContext("push", []string{}) - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true} - testcmd.RunCommand(cmd, ctxt, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - testcmd.CommandDidPassRequirements = true - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false} - testcmd.RunCommand(cmd, ctxt, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestPushingAppWhenItDoesNotExist(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "foo.cf-app.com" - domain.Guid = "foo-domain-guid" - domains := []cf.Domain{domain} - - domainRepo.DefaultAppDomain = domains[0] - routeRepo.FindByHostAndDomainErr = true - appRepo.FindByNameNotFound = true - - fakeUI := callPush(t, []string{"my-new-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating app") - assert.Contains(t, fakeUI.Outputs[0], "my-new-app") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Equal(t, appRepo.CreateName, "my-new-app") - assert.Equal(t, appRepo.CreateInstances, 1) - assert.Equal(t, appRepo.CreateMemory, uint64(128)) - assert.Equal(t, appRepo.CreateBuildpackUrl, "") - assert.Contains(t, fakeUI.Outputs[1], "OK") - - assert.Contains(t, fakeUI.Outputs[3], "my-new-app.foo.cf-app.com") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "my-new-app") - assert.Equal(t, routeRepo.CreatedHost, "my-new-app") - assert.Equal(t, routeRepo.CreatedDomainGuid, "foo-domain-guid") - assert.Contains(t, fakeUI.Outputs[4], "OK") - - assert.Contains(t, fakeUI.Outputs[6], "my-new-app.foo.cf-app.com") - assert.Equal(t, routeRepo.BoundAppGuid, "my-new-app-guid") - assert.Equal(t, routeRepo.BoundRouteGuid, "my-new-app-route-guid") - assert.Contains(t, fakeUI.Outputs[7], "OK") - - expectedAppDir, err := os.Getwd() - assert.NoError(t, err) - - assert.Contains(t, fakeUI.Outputs[9], "my-new-app") - assert.Equal(t, appBitsRepo.UploadedAppGuid, "my-new-app-guid") - assert.Equal(t, appBitsRepo.UploadedDir, expectedAppDir) - assert.Contains(t, fakeUI.Outputs[10], "OK") - - assert.Equal(t, stopper.AppToStop.Guid, "my-new-app-guid") - assert.Equal(t, starter.AppToStart.Guid, "my-new-app-guid") -} - -func TestPushingAppWhenItDoesNotExistButRouteExists(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - domain1 := cf.Domain{} - domain1.Name = "foo.cf-app.com" - domain1.Guid = "foo-domain-guid" - - domain2 := cf.DomainFields{} - domain2.Name = "foo.cf-app.com" - domain2.Guid = "foo-domain-guid" - - route := cf.Route{} - route.Guid = "my-route-guid" - route.Host = "my-new-app" - route.Domain = domain2 - - domainRepo.DefaultAppDomain = domain1 - routeRepo.FindByHostAndDomainRoute = route - appRepo.FindByNameNotFound = true - - fakeUI := callPush(t, []string{"my-new-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Empty(t, routeRepo.CreatedHost) - assert.Empty(t, routeRepo.CreatedDomainGuid) - assert.Contains(t, fakeUI.Outputs[3], "my-new-app.foo.cf-app.com") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "my-new-app") - - assert.Contains(t, fakeUI.Outputs[4], "my-new-app.foo.cf-app.com") - assert.Equal(t, routeRepo.BoundAppGuid, "my-new-app-guid") - assert.Equal(t, routeRepo.BoundRouteGuid, "my-route-guid") - assert.Contains(t, fakeUI.Outputs[5], "OK") -} - -func TestPushingAppWithCustomFlags(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - stack := cf.Stack{} - stack.Name = "customLinux" - stack.Guid = "custom-linux-guid" - - domainRepo.FindByNameDomain = domain - routeRepo.FindByHostAndDomainErr = true - stackRepo.FindByNameStack = stack - appRepo.FindByNameNotFound = true - - fakeUI := callPush(t, []string{ - "-c", "unicorn -c config/unicorn.rb -D", - "-d", "bar.cf-app.com", - "-n", "my-hostname", - "-i", "3", - "-m", "2G", - "-b", "https://github.com/heroku/heroku-buildpack-play.git", - "-p", "/Users/pivotal/workspace/my-new-app", - "-s", "customLinux", - "--no-start", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "customLinux") - assert.Equal(t, stackRepo.FindByNameName, "customLinux") - - assert.Contains(t, fakeUI.Outputs[1], "my-new-app") - assert.Equal(t, appRepo.CreateName, "my-new-app") - assert.Equal(t, appRepo.CreateCommand, "unicorn -c config/unicorn.rb -D") - assert.Equal(t, appRepo.CreateInstances, 3) - assert.Equal(t, appRepo.CreateMemory, uint64(2048)) - assert.Equal(t, appRepo.CreateStackGuid, "custom-linux-guid") - assert.Equal(t, appRepo.CreateBuildpackUrl, "https://github.com/heroku/heroku-buildpack-play.git") - assert.Contains(t, fakeUI.Outputs[2], "OK") - - assert.Contains(t, fakeUI.Outputs[4], "my-hostname.bar.cf-app.com") - assert.Equal(t, domainRepo.FindByNameInCurrentSpaceName, "bar.cf-app.com") - assert.Equal(t, routeRepo.CreatedHost, "my-hostname") - assert.Equal(t, routeRepo.CreatedDomainGuid, "bar-domain-guid") - assert.Contains(t, fakeUI.Outputs[5], "OK") - - assert.Contains(t, fakeUI.Outputs[7], "my-hostname.bar.cf-app.com") - assert.Contains(t, fakeUI.Outputs[7], "my-new-app") - assert.Equal(t, routeRepo.BoundAppGuid, "my-new-app-guid") - assert.Equal(t, routeRepo.BoundRouteGuid, "my-hostname-route-guid") - assert.Contains(t, fakeUI.Outputs[8], "OK") - - assert.Contains(t, fakeUI.Outputs[10], "my-new-app") - assert.Equal(t, appBitsRepo.UploadedAppGuid, "my-new-app-guid") - assert.Equal(t, appBitsRepo.UploadedDir, "/Users/pivotal/workspace/my-new-app") - assert.Contains(t, fakeUI.Outputs[11], "OK") - - assert.Equal(t, starter.AppToStart.Name, "") -} - -func TestPushingAppWithNoRoute(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - stack := cf.Stack{} - stack.Name = "customLinux" - stack.Guid = "custom-linux-guid" - - domainRepo.FindByNameDomain = domain - routeRepo.FindByHostErr = true - stackRepo.FindByNameStack = stack - appRepo.FindByNameNotFound = true - - callPush(t, []string{ - "--no-route", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, appRepo.CreateName, "my-new-app") - assert.Equal(t, routeRepo.CreatedHost, "") - assert.Equal(t, routeRepo.CreatedDomainGuid, "") -} - -func TestPushingAppWithNoHostname(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - stack := cf.Stack{} - stack.Name = "customLinux" - stack.Guid = "custom-linux-guid" - - domainRepo.DefaultAppDomain = domain - routeRepo.FindByHostAndDomainErr = true - stackRepo.FindByNameStack = stack - appRepo.FindByNameNotFound = true - - callPush(t, []string{ - "--no-hostname", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, appRepo.CreateName, "my-new-app") - assert.Equal(t, routeRepo.CreatedHost, "") - assert.Equal(t, routeRepo.CreatedDomainGuid, "bar-domain-guid") -} - -func TestPushingAppWithMemoryInMegaBytes(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - domainRepo.FindByNameDomain = domain - appRepo.FindByNameNotFound = true - - callPush(t, []string{ - "-m", "256M", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, appRepo.CreateMemory, uint64(256)) -} - -func TestPushingAppWithMemoryWithoutUnit(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - domainRepo.FindByNameDomain = domain - appRepo.FindByNameNotFound = true - - callPush(t, []string{ - "-m", "512", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, appRepo.CreateMemory, uint64(512)) -} - -func TestPushingAppWithInvalidMemory(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - domain := cf.Domain{} - domain.Name = "bar.cf-app.com" - domain.Guid = "bar-domain-guid" - domainRepo.FindByNameDomain = domain - appRepo.FindByNameNotFound = true - - callPush(t, []string{ - "-m", "abcM", - "my-new-app", - }, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, appRepo.CreateMemory, uint64(128)) -} - -func TestPushingAppWhenItAlreadyExistsAndNothingIsSpecified(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - existingRoute := cf.RouteSummary{} - existingRoute.Host = "existing-app" - - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - existingApp.Routes = []cf.RouteSummary{existingRoute} - - appRepo.FindByNameApp = existingApp - - domain := cf.DomainFields{} - domain.Name = "example.com" - - foundRoute := cf.Route{} - foundRoute.RouteFields = existingRoute.RouteFields - foundRoute.Domain = domain - - routeRepo.FindByHostAndDomainRoute = foundRoute - fakeUI := callPush(t, []string{"existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Equal(t, stopper.AppToStop.Name, "existing-app") - assert.Contains(t, fakeUI.Outputs[0], "Using route") - assert.Contains(t, fakeUI.Outputs[0], "existing-app.example.com") - assert.Equal(t, appBitsRepo.UploadedAppGuid, "existing-app-guid") -} - -func TestPushingAppWhenItAlreadyExistsAndDomainIsSpecifiedIsAlreadyBound(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - existingRoute := cf.RouteSummary{} - existingRoute.Host = "existing-app" - - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - existingApp.Routes = []cf.RouteSummary{existingRoute} - - domain := cf.DomainFields{} - domain.Name = "example.com" - - foundRoute := cf.Route{} - foundRoute.RouteFields = existingRoute.RouteFields - foundRoute.Domain = domain - - appRepo.FindByNameApp = existingApp - routeRepo.FindByHostAndDomainRoute = foundRoute - - fakeUI := callPush(t, []string{"-d", "example.com", "existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Using route") - assert.Contains(t, fakeUI.Outputs[0], "existing-app") - assert.Equal(t, appBitsRepo.UploadedAppGuid, "existing-app-guid") -} - -func TestPushingAppWhenItAlreadyExistsAndDomainSpecifiedIsNotBound(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - domain := cf.DomainFields{} - domain.Name = "example.com" - - existingRoute := cf.RouteSummary{} - existingRoute.Host = "existing-app" - existingRoute.Domain = domain - - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - existingApp.Routes = []cf.RouteSummary{existingRoute} - - foundDomain := cf.Domain{} - foundDomain.Guid = "domain-guid" - foundDomain.Name = "newdomain.com" - - appRepo.FindByNameApp = existingApp - routeRepo.FindByHostAndDomainNotFound = true - domainRepo.FindByNameDomain = foundDomain - - fakeUI := callPush(t, []string{"-d", "newdomain.com", "existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating route") - assert.Contains(t, fakeUI.Outputs[0], "existing-app.newdomain.com") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Binding") - assert.Contains(t, fakeUI.Outputs[3], "existing-app.newdomain.com") - - assert.Equal(t, appBitsRepo.UploadedAppGuid, "existing-app-guid") - assert.Equal(t, domainRepo.FindByNameInCurrentSpaceName, "newdomain.com") - assert.Equal(t, routeRepo.FindByHostAndDomainDomain, "newdomain.com") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "existing-app") - assert.Equal(t, routeRepo.CreatedHost, "existing-app") - assert.Equal(t, routeRepo.CreatedDomainGuid, "domain-guid") -} - -func TestPushingAppWhenItAlreadyExistsAndHostIsSpecified(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - domain := cf.DomainFields{} - domain.Name = "example.com" - domain.Guid = "domain-guid" - - existingRoute := cf.RouteSummary{} - existingRoute.Host = "existing-app" - existingRoute.Domain = domain - - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - existingApp.Routes = []cf.RouteSummary{existingRoute} - - appRepo.FindByNameApp = existingApp - routeRepo.FindByHostAndDomainNotFound = true - domainRepo.DefaultAppDomain = cf.Domain{DomainFields: domain} - - fakeUI := callPush(t, []string{"-n", "new-host", "existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating route") - assert.Contains(t, fakeUI.Outputs[0], "new-host.example.com") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Binding") - assert.Contains(t, fakeUI.Outputs[3], "new-host.example.com") - - assert.Equal(t, routeRepo.FindByHostAndDomainDomain, "example.com") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "new-host") - assert.Equal(t, routeRepo.CreatedHost, "new-host") - assert.Equal(t, routeRepo.CreatedDomainGuid, "domain-guid") -} - -func TestPushingAppWhenItAlreadyExistsAndNoRouteFlagIsPresent(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - - appRepo.FindByNameApp = existingApp - - fakeUI := callPush(t, []string{"--no-route", "existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Uploading") - assert.Contains(t, fakeUI.Outputs[0], "existing-app") - assert.Contains(t, fakeUI.Outputs[1], "OK") - - assert.Equal(t, appBitsRepo.UploadedAppGuid, "existing-app-guid") - assert.Equal(t, domainRepo.FindByNameInCurrentSpaceName, "") - assert.Equal(t, routeRepo.FindByHostAndDomainDomain, "") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "") - assert.Equal(t, routeRepo.CreatedHost, "") - assert.Equal(t, routeRepo.CreatedDomainGuid, "") -} - -func TestPushingAppWhenItAlreadyExistsAndNoHostFlagIsPresent(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - - domain := cf.DomainFields{} - domain.Name = "example.com" - domain.Guid = "domain-guid" - - existingRoute := cf.RouteSummary{} - existingRoute.Host = "existing-app" - existingRoute.Domain = domain - - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - existingApp.Routes = []cf.RouteSummary{existingRoute} - - appRepo.FindByNameApp = existingApp - routeRepo.FindByHostAndDomainNotFound = true - domainRepo.DefaultAppDomain = cf.Domain{DomainFields: domain} - - fakeUI := callPush(t, []string{"--no-hostname", "existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating route") - assert.Contains(t, fakeUI.Outputs[0], "example.com") - assert.NotContains(t, fakeUI.Outputs[0], "existing-app.example.com") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Binding") - assert.Contains(t, fakeUI.Outputs[3], "example.com") - assert.NotContains(t, fakeUI.Outputs[3], "existing-app.example.com") - - assert.Equal(t, routeRepo.FindByHostAndDomainDomain, "example.com") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "") - assert.Equal(t, routeRepo.CreatedHost, "") - assert.Equal(t, routeRepo.CreatedDomainGuid, "domain-guid") -} - -func TestPushingAppWhenItAlreadyExistsWithoutARouteAndARouteIsNotProvided(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - existingApp := cf.Application{} - existingApp.Name = "existing-app" - existingApp.Guid = "existing-app-guid" - - appRepo.FindByNameApp = existingApp - - fakeUI := callPush(t, []string{"existing-app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - testassert.SliceContains(t, fakeUI.Outputs, []string{ - "skipping route creation", - "Uploading", - "OK", - }) - - assert.Equal(t, routeRepo.FindByHostAndDomainDomain, "") - assert.Equal(t, routeRepo.FindByHostAndDomainHost, "") - assert.Equal(t, routeRepo.CreatedHost, "") - assert.Equal(t, routeRepo.CreatedDomainGuid, "") -} - -func TestPushingAppWithInvalidPath(t *testing.T) { - starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo := getPushDependencies() - appBitsRepo.UploadAppErr = true - - fakeUI := callPush(t, []string{"app"}, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - - testassert.SliceContains(t, fakeUI.Outputs, []string{"Uploading", "FAILED"}) -} - -func getPushDependencies() (starter *testcmd.FakeAppStarter, - stopper *testcmd.FakeAppStopper, - appRepo *testapi.FakeApplicationRepository, - domainRepo *testapi.FakeDomainRepository, - routeRepo *testapi.FakeRouteRepository, - stackRepo *testapi.FakeStackRepository, - appBitsRepo *testapi.FakeApplicationBitsRepository) { - - starter = &testcmd.FakeAppStarter{} - stopper = &testcmd.FakeAppStopper{} - appRepo = &testapi.FakeApplicationRepository{} - domainRepo = &testapi.FakeDomainRepository{} - routeRepo = &testapi.FakeRouteRepository{} - stackRepo = &testapi.FakeStackRepository{} - appBitsRepo = &testapi.FakeApplicationBitsRepository{} - - return -} - -func callPush(t *testing.T, - args []string, - starter ApplicationStarter, - stopper ApplicationStopper, - appRepo api.ApplicationRepository, - domainRepo api.DomainRepository, - routeRepo api.RouteRepository, - stackRepo api.StackRepository, - appBitsRepo *testapi.FakeApplicationBitsRepository) (fakeUI *testterm.FakeUI) { - - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("push", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewPush(fakeUI, config, starter, stopper, appRepo, domainRepo, routeRepo, stackRepo, appBitsRepo) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/application/rename_app.go b/src/cf/commands/application/rename_app.go deleted file mode 100644 index 1f4ed608220..00000000000 --- a/src/cf/commands/application/rename_app.go +++ /dev/null @@ -1,58 +0,0 @@ -package application - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RenameApp struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appReq requirements.ApplicationRequirement -} - -func NewRenameApp(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository) (cmd *RenameApp) { - cmd = new(RenameApp) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - return -} - -func (cmd *RenameApp) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "rename") - return - } - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.appReq, - } - return -} - -func (cmd *RenameApp) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - new_name := c.Args()[1] - cmd.ui.Say("Renaming app %s to %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(new_name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.appRepo.Rename(app.Guid, new_name) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Ok() -} diff --git a/src/cf/commands/application/rename_app_test.go b/src/cf/commands/application/rename_app_test.go deleted file mode 100644 index c2c1eb59e90..00000000000 --- a/src/cf/commands/application/rename_app_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRenameAppFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - appRepo := &testapi.FakeApplicationRepository{} - - fakeUI := callRename(t, []string{}, reqFactory, appRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callRename(t, []string{"foo"}, reqFactory, appRepo) - assert.True(t, fakeUI.FailedWithUsage) -} - -func TestRenameRequirements(t *testing.T) { - appRepo := &testapi.FakeApplicationRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callRename(t, []string{"my-app", "my-new-app"}, reqFactory, appRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") -} - -func TestRenameRun(t *testing.T) { - appRepo := &testapi.FakeApplicationRepository{} - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Application: app} - ui := callRename(t, []string{"my-app", "my-new-app"}, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Renaming app ") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-new-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, appRepo.RenameAppGuid, app.Guid) - assert.Equal(t, appRepo.RenameNewName, "my-new-app") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callRename(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appRepo *testapi.FakeApplicationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("rename", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewRenameApp(ui, config, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/restart.go b/src/cf/commands/application/restart.go deleted file mode 100644 index 2640eff6146..00000000000 --- a/src/cf/commands/application/restart.go +++ /dev/null @@ -1,66 +0,0 @@ -package application - -import ( - "cf" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Restart struct { - ui terminal.UI - starter ApplicationStarter - stopper ApplicationStopper - appReq requirements.ApplicationRequirement -} - -type ApplicationRestarter interface { - ApplicationRestart(app cf.Application) -} - -func NewRestart(ui terminal.UI, starter ApplicationStarter, stopper ApplicationStopper) (cmd *Restart) { - cmd = new(Restart) - cmd.ui = ui - cmd.starter = starter - cmd.stopper = stopper - return -} - -func (cmd *Restart) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) == 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "restart") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *Restart) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - cmd.ApplicationRestart(app) -} - -func (cmd *Restart) ApplicationRestart(app cf.Application) { - stoppedApp, err := cmd.stopper.ApplicationStop(app) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } - - cmd.ui.Say("") - - _, err = cmd.starter.ApplicationStart(stoppedApp) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } -} diff --git a/src/cf/commands/application/restart_test.go b/src/cf/commands/application/restart_test.go deleted file mode 100644 index 55549732e47..00000000000 --- a/src/cf/commands/application/restart_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRestartCommandFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - starter := &testcmd.FakeAppStarter{} - stopper := &testcmd.FakeAppStopper{} - ui := callRestart([]string{}, reqFactory, starter, stopper) - assert.True(t, ui.FailedWithUsage) - - ui = callRestart([]string{"my-app"}, reqFactory, starter, stopper) - assert.False(t, ui.FailedWithUsage) -} - -func TestRestartRequirements(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - starter := &testcmd.FakeAppStarter{} - stopper := &testcmd.FakeAppStopper{} - - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - callRestart([]string{"my-app"}, reqFactory, starter, stopper) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: false, TargetedSpaceSuccess: true} - callRestart([]string{"my-app"}, reqFactory, starter, stopper) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: false} - callRestart([]string{"my-app"}, reqFactory, starter, stopper) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestRestartApplication(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - starter := &testcmd.FakeAppStarter{} - stopper := &testcmd.FakeAppStopper{} - callRestart([]string{"my-app"}, reqFactory, starter, stopper) - - assert.Equal(t, stopper.AppToStop, app) - assert.Equal(t, starter.AppToStart, app) -} - -func callRestart(args []string, reqFactory *testreq.FakeReqFactory, starter ApplicationStarter, stopper ApplicationStopper) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("restart", args) - - cmd := NewRestart(ui, starter, stopper) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/scale.go b/src/cf/commands/application/scale.go deleted file mode 100644 index f5735cd5bd9..00000000000 --- a/src/cf/commands/application/scale.go +++ /dev/null @@ -1,90 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/formatters" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Scale struct { - ui terminal.UI - config *configuration.Configuration - restarter ApplicationRestarter - appReq requirements.ApplicationRequirement - appRepo api.ApplicationRepository -} - -func NewScale(ui terminal.UI, config *configuration.Configuration, restarter ApplicationRestarter, appRepo api.ApplicationRepository) (cmd *Scale) { - cmd = new(Scale) - cmd.ui = ui - cmd.config = config - cmd.restarter = restarter - cmd.appRepo = appRepo - return -} - -func (cmd *Scale) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "scale") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *Scale) Run(c *cli.Context) { - currentApp := cmd.appReq.GetApplication() - cmd.ui.Say("Scaling app %s in org %s / space %s as %s...", - terminal.EntityNameColor(currentApp.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - changedAppFields := cf.ApplicationFields{} - changedAppFields.Guid = currentApp.Guid - - memory, err := extractMegaBytes(c.String("m")) - if err != nil { - cmd.ui.Say("Invalid value for memory") - cmd.ui.FailWithUsage(c, "scale") - return - } - changedAppFields.Memory = memory - changedAppFields.InstanceCount = c.Int("i") - - apiResponse := cmd.appRepo.Scale(changedAppFields) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Ok() - cmd.ui.Say("") -} - -func extractMegaBytes(arg string) (megaBytes uint64, err error) { - if arg != "" { - var byteSize uint64 - byteSize, err = formatters.BytesFromString(arg) - if err != nil { - return - } - megaBytes = byteSize / formatters.MEGABYTE - } - - return -} diff --git a/src/cf/commands/application/scale_test.go b/src/cf/commands/application/scale_test.go deleted file mode 100644 index 3204a54cc2f..00000000000 --- a/src/cf/commands/application/scale_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestScaleRequirements(t *testing.T) { - args := []string{"-m", "1G", "my-app"} - reqFactory, restarter, appRepo := getScaleDependencies() - - reqFactory.LoginSuccess = false - reqFactory.TargetedSpaceSuccess = true - callScale(t, args, reqFactory, restarter, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - reqFactory.TargetedSpaceSuccess = false - callScale(t, args, reqFactory, restarter, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - reqFactory.TargetedSpaceSuccess = true - callScale(t, args, reqFactory, restarter, appRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") -} - -func TestScaleFailsWithUsage(t *testing.T) { - reqFactory, restarter, appRepo := getScaleDependencies() - - ui := callScale(t, []string{}, reqFactory, restarter, appRepo) - - assert.True(t, ui.FailedWithUsage) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestScaleAll(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory, restarter, appRepo := getScaleDependencies() - reqFactory.Application = app - - ui := callScale(t, []string{"-i", "5", "-m", "512M", "my-app"}, reqFactory, restarter, appRepo) - - assert.Contains(t, ui.Outputs[0], "Scaling") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, appRepo.ScaledApp.Guid, "my-app-guid") - assert.Equal(t, appRepo.ScaledApp.Memory, uint64(512)) - assert.Equal(t, appRepo.ScaledApp.InstanceCount, 5) -} - -func TestScaleOnlyInstances(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory, restarter, appRepo := getScaleDependencies() - reqFactory.Application = app - - callScale(t, []string{"-i", "5", "my-app"}, reqFactory, restarter, appRepo) - - assert.Equal(t, appRepo.ScaledApp.Guid, "my-app-guid") - assert.Equal(t, appRepo.ScaledApp.DiskQuota, uint64(0)) - assert.Equal(t, appRepo.ScaledApp.Memory, uint64(0)) - assert.Equal(t, appRepo.ScaledApp.InstanceCount, 5) -} - -func TestScaleOnlyMemory(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory, restarter, appRepo := getScaleDependencies() - reqFactory.Application = app - - callScale(t, []string{"-m", "512M", "my-app"}, reqFactory, restarter, appRepo) - - assert.Equal(t, appRepo.ScaledApp.Guid, "my-app-guid") - assert.Equal(t, appRepo.ScaledApp.DiskQuota, uint64(0)) - assert.Equal(t, appRepo.ScaledApp.Memory, uint64(512)) - assert.Equal(t, appRepo.ScaledApp.InstanceCount, 0) -} - -func getScaleDependencies() (reqFactory *testreq.FakeReqFactory, restarter *testcmd.FakeAppRestarter, appRepo *testapi.FakeApplicationRepository) { - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - restarter = &testcmd.FakeAppRestarter{} - appRepo = &testapi.FakeApplicationRepository{} - return -} - -func callScale(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, restarter *testcmd.FakeAppRestarter, appRepo api.ApplicationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("scale", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewScale(ui, config, restarter, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/set_env.go b/src/cf/commands/application/set_env.go deleted file mode 100644 index 2f246828004..00000000000 --- a/src/cf/commands/application/set_env.go +++ /dev/null @@ -1,82 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type SetEnv struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appReq requirements.ApplicationRequirement -} - -func NewSetEnv(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository) (cmd *SetEnv) { - cmd = new(SetEnv) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - return -} - -func (cmd *SetEnv) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 3 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "set-env") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *SetEnv) Run(c *cli.Context) { - varName := c.Args()[1] - varValue := c.Args()[2] - app := cmd.appReq.GetApplication() - - cmd.ui.Say("Setting env variable %s for app %s in org %s / space %s as %s...", - terminal.EntityNameColor(varName), - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - var envVars map[string]string - - if app.EnvironmentVars != nil { - envVars = app.EnvironmentVars - } else { - envVars = map[string]string{} - } - - if envVarFound(varName, envVars) { - cmd.ui.Ok() - cmd.ui.Warn("Env var %s was already set.", varName) - return - } - - envVars[varName] = varValue - - apiResponse := cmd.appRepo.SetEnv(app.Guid, envVars) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("TIP: Use '%s push' to ensure your env variable changes take effect", cf.Name()) -} diff --git a/src/cf/commands/application/set_env_test.go b/src/cf/commands/application/set_env_test.go deleted file mode 100644 index 949d99e5a14..00000000000 --- a/src/cf/commands/application/set_env_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSetEnvRequirements(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{} - args := []string{"my-app", "DATABASE_URL", "mysql://example.com/my-db"} - - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - callSetEnv(t, args, reqFactory, appRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: false, TargetedSpaceSuccess: true} - callSetEnv(t, args, reqFactory, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - testcmd.CommandDidPassRequirements = true - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: false} - callSetEnv(t, args, reqFactory, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestRunWhenApplicationExists(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.EnvironmentVars = map[string]string{"foo": "bar"} - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{} - - args := []string{"my-app", "DATABASE_URL", "mysql://example.com/my-db"} - ui := callSetEnv(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Setting env variable") - assert.Contains(t, ui.Outputs[0], "DATABASE_URL") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, appRepo.SetEnvAppGuid, app.Guid) - assert.Equal(t, appRepo.SetEnvVars, map[string]string{ - "DATABASE_URL": "mysql://example.com/my-db", - "foo": "bar", - }) -} - -func TestSetEnvWhenItAlreadyExists(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.EnvironmentVars = map[string]string{"DATABASE_URL": "mysql://example.com/my-db"} - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{} - - args := []string{"my-app", "DATABASE_URL", "mysql://example.com/my-db"} - ui := callSetEnv(t, args, reqFactory, appRepo) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "DATABASE_URL") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "DATABASE_URL") - assert.Contains(t, ui.Outputs[2], "was already set.") - -} - -func TestRunWhenSettingTheEnvFails(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{ - FindByNameApp: app, - SetEnvErr: true, - } - - args := []string{"does-not-exist", "DATABASE_URL", "mysql://example.com/my-db"} - ui := callSetEnv(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Setting env variable") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "Failed setting env") -} - -func TestSetEnvFailsWithUsage(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - - args := []string{"my-app", "DATABASE_URL", "..."} - ui := callSetEnv(t, args, reqFactory, appRepo) - assert.False(t, ui.FailedWithUsage) - - args = []string{"my-app", "DATABASE_URL"} - ui = callSetEnv(t, args, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) - - args = []string{"my-app"} - ui = callSetEnv(t, args, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) - - args = []string{} - ui = callSetEnv(t, args, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) -} - -func callSetEnv(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appRepo api.ApplicationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("set-env", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewSetEnv(ui, config, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/show_app.go b/src/cf/commands/application/show_app.go deleted file mode 100644 index c0587e9c8cd..00000000000 --- a/src/cf/commands/application/show_app.go +++ /dev/null @@ -1,103 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/formatters" - "cf/requirements" - "cf/terminal" - "errors" - "fmt" - "github.com/codegangsta/cli" - "strings" -) - -type ShowApp struct { - ui terminal.UI - config *configuration.Configuration - appSummaryRepo api.AppSummaryRepository - appInstancesRepo api.AppInstancesRepository - appReq requirements.ApplicationRequirement -} - -func NewShowApp(ui terminal.UI, config *configuration.Configuration, appSummaryRepo api.AppSummaryRepository, appInstancesRepo api.AppInstancesRepository) (cmd *ShowApp) { - cmd = new(ShowApp) - cmd.ui = ui - cmd.config = config - cmd.appSummaryRepo = appSummaryRepo - cmd.appInstancesRepo = appInstancesRepo - return -} - -func (cmd *ShowApp) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "app") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *ShowApp) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - cmd.ui.Say("Showing health and status for app %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - appSummary, apiResponse := cmd.appSummaryRepo.GetSummary(app.Guid) - appIsStopped := apiResponse.ErrorCode == cf.APP_STOPPED || apiResponse.ErrorCode == cf.APP_NOT_STAGED - if apiResponse.IsNotSuccessful() && !appIsStopped { - cmd.ui.Failed(apiResponse.Message) - return - } - - instances, apiResponse := cmd.appInstancesRepo.GetInstances(app.Guid) - if apiResponse.IsNotSuccessful() && !appIsStopped { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("\n%s %s", terminal.HeaderColor("state:"), coloredAppState(appSummary.ApplicationFields)) - cmd.ui.Say("%s %s", terminal.HeaderColor("instances:"), coloredAppInstaces(appSummary.ApplicationFields)) - cmd.ui.Say("%s %s x %d instances", terminal.HeaderColor("usage:"), formatters.ByteSize(appSummary.Memory*formatters.MEGABYTE), appSummary.InstanceCount) - - var urls []string - for _, route := range appSummary.RouteSummaries { - urls = append(urls, route.URL()) - } - cmd.ui.Say("%s %s\n", terminal.HeaderColor("urls:"), strings.Join(urls, ", ")) - - if appIsStopped { - return - } - - table := [][]string{ - []string{"", "status", "since", "cpu", "memory", "disk"}, - } - - for index, instance := range instances { - table = append(table, []string{ - fmt.Sprintf("#%d", index), - coloredInstanceState(instance), - instance.Since.Format("2006-01-02 03:04:05 PM"), - fmt.Sprintf("%.1f%%", instance.CpuUsage), - fmt.Sprintf("%s of %s", formatters.ByteSize(instance.MemUsage), formatters.ByteSize(instance.MemQuota)), - fmt.Sprintf("%s of %s", formatters.ByteSize(instance.DiskUsage), formatters.ByteSize(instance.DiskQuota)), - }) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/application/show_app_test.go b/src/cf/commands/application/show_app_test.go deleted file mode 100644 index b105f694b1d..00000000000 --- a/src/cf/commands/application/show_app_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package application_test - -import ( - "cf" - . "cf/commands/application" - "cf/configuration" - "cf/formatters" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" - "time" -) - -func TestAppRequirements(t *testing.T) { - args := []string{"my-app", "/foo"} - appSummaryRepo := &testapi.FakeAppSummaryRepo{} - appInstancesRepo := &testapi.FakeAppInstancesRepo{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true, Application: cf.Application{}} - callApp(t, args, reqFactory, appSummaryRepo, appInstancesRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false, Application: cf.Application{}} - callApp(t, args, reqFactory, appSummaryRepo, appInstancesRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: cf.Application{}} - callApp(t, args, reqFactory, appSummaryRepo, appInstancesRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") -} - -func TestAppFailsWithUsage(t *testing.T) { - appSummaryRepo := &testapi.FakeAppSummaryRepo{} - appInstancesRepo := &testapi.FakeAppInstancesRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: cf.Application{}} - ui := callApp(t, []string{}, reqFactory, appSummaryRepo, appInstancesRepo) - - assert.True(t, ui.FailedWithUsage) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestDisplayingAppSummary(t *testing.T) { - reqApp := cf.Application{} - reqApp.Name = "my-app" - reqApp.Guid = "my-app-guid" - - route1 := cf.RouteSummary{} - route1.Host = "my-app" - - domain := cf.DomainFields{} - domain.Name = "example.com" - route1.Domain = domain - - route2 := cf.RouteSummary{} - route2.Host = "foo" - domain2 := cf.DomainFields{} - domain2.Name = "example.com" - route2.Domain = domain2 - - appSummary := cf.AppSummary{} - appSummary.State = "started" - appSummary.InstanceCount = 2 - appSummary.RunningInstances = 2 - appSummary.Memory = 256 - appSummary.RouteSummaries = []cf.RouteSummary{route1, route2} - - time1, err := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2012") - assert.NoError(t, err) - - time2, err := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Apr 1 15:04:05 -0700 MST 2012") - assert.NoError(t, err) - - appInstance := cf.AppInstanceFields{} - appInstance.State = cf.InstanceRunning - appInstance.Since = time1 - appInstance.CpuUsage = 1.0 - appInstance.DiskQuota = 1 * formatters.GIGABYTE - appInstance.DiskUsage = 32 * formatters.MEGABYTE - appInstance.MemQuota = 64 * formatters.MEGABYTE - appInstance.MemUsage = 13 * formatters.BYTE - - appInstance2 := cf.AppInstanceFields{} - appInstance2.State = cf.InstanceDown - appInstance2.Since = time2 - - instances := []cf.AppInstanceFields{appInstance, appInstance2} - - appSummaryRepo := &testapi.FakeAppSummaryRepo{GetSummarySummary: appSummary} - appInstancesRepo := &testapi.FakeAppInstancesRepo{GetInstancesResponses: [][]cf.AppInstanceFields{instances}} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: reqApp} - ui := callApp(t, []string{"my-app"}, reqFactory, appSummaryRepo, appInstancesRepo) - - assert.Equal(t, appSummaryRepo.GetSummaryAppGuid, "my-app-guid") - - assert.Contains(t, ui.Outputs[0], "Showing health and status") - assert.Contains(t, ui.Outputs[0], "my-app") - - assert.Contains(t, ui.Outputs[2], "state") - assert.Contains(t, ui.Outputs[2], "started") - - assert.Contains(t, ui.Outputs[3], "instances") - assert.Contains(t, ui.Outputs[3], "2/2") - - assert.Contains(t, ui.Outputs[4], "usage") - assert.Contains(t, ui.Outputs[4], "256M x 2 instances") - - assert.Contains(t, ui.Outputs[5], "urls") - assert.Contains(t, ui.Outputs[5], "my-app.example.com, foo.example.com") - - assert.Contains(t, ui.Outputs[7], "#0") - assert.Contains(t, ui.Outputs[7], "running") - assert.Contains(t, ui.Outputs[7], "2012-01-02 03:04:05 PM") - assert.Contains(t, ui.Outputs[7], "1.0%") - assert.Contains(t, ui.Outputs[7], "13 of 64M") - assert.Contains(t, ui.Outputs[7], "32M of 1G") - - assert.Contains(t, ui.Outputs[8], "#1") - assert.Contains(t, ui.Outputs[8], "down") - assert.Contains(t, ui.Outputs[8], "2012-04-01 03:04:05 PM") - assert.Contains(t, ui.Outputs[8], "0%") - assert.Contains(t, ui.Outputs[8], "0 of 0") - assert.Contains(t, ui.Outputs[8], "0 of 0") -} - -func TestDisplayingStoppedAppSummary(t *testing.T) { - testDisplayingAppSummaryWithErrorCode(t, cf.APP_STOPPED) -} - -func TestDisplayingNotStagedAppSummary(t *testing.T) { - testDisplayingAppSummaryWithErrorCode(t, cf.APP_NOT_STAGED) -} - -func testDisplayingAppSummaryWithErrorCode(t *testing.T, errorCode string) { - reqApp := cf.Application{} - reqApp.Name = "my-app" - reqApp.Guid = "my-app-guid" - - domain3 := cf.DomainFields{} - domain3.Name = "example.com" - domain4 := cf.DomainFields{} - domain4.Name = "example.com" - - route1 := cf.RouteSummary{} - route1.Host = "my-app" - route1.Domain = domain3 - - route2 := cf.RouteSummary{} - route2.Host = "foo" - route2.Domain = domain4 - - routes := []cf.RouteSummary{ - route1, - route2, - } - - app := cf.ApplicationFields{} - app.State = "stopped" - app.InstanceCount = 2 - app.RunningInstances = 0 - app.Memory = 256 - - appSummary := cf.AppSummary{} - appSummary.ApplicationFields = app - appSummary.RouteSummaries = routes - - appSummaryRepo := &testapi.FakeAppSummaryRepo{GetSummarySummary: appSummary, GetSummaryErrorCode: errorCode} - appInstancesRepo := &testapi.FakeAppInstancesRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, Application: reqApp} - ui := callApp(t, []string{"my-app"}, reqFactory, appSummaryRepo, appInstancesRepo) - - assert.Equal(t, appSummaryRepo.GetSummaryAppGuid, "my-app-guid") - assert.Equal(t, appInstancesRepo.GetInstancesAppGuid, "my-app-guid") - assert.Equal(t, len(ui.Outputs), 6) - - assert.Contains(t, ui.Outputs[0], "Showing health and status") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Contains(t, ui.Outputs[2], "state") - assert.Contains(t, ui.Outputs[2], "stopped") - - assert.Contains(t, ui.Outputs[3], "instances") - assert.Contains(t, ui.Outputs[3], "0/2") - - assert.Contains(t, ui.Outputs[4], "usage") - assert.Contains(t, ui.Outputs[4], "256M x 2 instances") - - assert.Contains(t, ui.Outputs[5], "urls") - assert.Contains(t, ui.Outputs[5], "my-app.example.com, foo.example.com") -} - -func callApp(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appSummaryRepo *testapi.FakeAppSummaryRepo, appInstancesRepo *testapi.FakeAppInstancesRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("app", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewShowApp(ui, config, appSummaryRepo, appInstancesRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/application/start.go b/src/cf/commands/application/start.go deleted file mode 100644 index 1561f753b1f..00000000000 --- a/src/cf/commands/application/start.go +++ /dev/null @@ -1,214 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "errors" - "fmt" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/codegangsta/cli" - "strings" - "time" -) - -const MaxInstanceStartupPings = 60 - -type Start struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appInstancesRepo api.AppInstancesRepository - logRepo api.LogsRepository - startTime time.Time - appReq requirements.ApplicationRequirement -} - -type ApplicationStarter interface { - ApplicationStart(app cf.Application) (updatedApp cf.Application, err error) - ApplicationStartWithBuildpack(app cf.Application, buildpackUrl string) (startedApp cf.Application, err error) -} - -func NewStart(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository, appInstancesRepo api.AppInstancesRepository, logRepo api.LogsRepository) (cmd *Start) { - cmd = new(Start) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - cmd.appInstancesRepo = appInstancesRepo - cmd.logRepo = logRepo - - return -} - -func (cmd *Start) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) == 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "start") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{cmd.appReq} - return -} - -func (cmd *Start) Run(c *cli.Context) { - cmd.ApplicationStart(cmd.appReq.GetApplication()) -} - -func (cmd *Start) ApplicationStart(app cf.Application) (updatedApp cf.Application, err error) { - return cmd.applicationStartWithOptions(app, "") -} - -func (cmd *Start) ApplicationStartWithBuildpack(app cf.Application, buildpackUrl string) (updatedApp cf.Application, err error) { - return cmd.applicationStartWithOptions(app, buildpackUrl) -} - -func (cmd *Start) applicationStartWithOptions(app cf.Application, buildpackUrl string) (updatedApp cf.Application, err error) { - if app.State == "started" { - cmd.ui.Say(terminal.WarningColor("App " + app.Name + " is already started")) - return - } - - cmd.ui.Say("Starting app %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - var apiResponse net.ApiResponse - if buildpackUrl == "" { - updatedApp, apiResponse = cmd.appRepo.Start(app.Guid) - } else { - updatedApp, apiResponse = cmd.appRepo.StartWithDifferentBuildpack(app.Guid, buildpackUrl) - } - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - - stopLoggingChan := make(chan bool, 1) - defer close(stopLoggingChan) - go cmd.tailStagingLogs(app, stopLoggingChan) - - instances := cmd.waitForInstanceStartup(updatedApp) - stopLoggingChan <- true - - cmd.ui.Say("") - - cmd.startTime = time.Now() - - for cmd.displayInstancesStatus(app, instances) { - cmd.ui.Wait(1 * time.Second) - instances, _ = cmd.appInstancesRepo.GetInstances(updatedApp.Guid) - } - - return -} - -func (cmd Start) tailStagingLogs(app cf.Application, stopChan chan bool) { - logChan := make(chan *logmessage.Message, 1000) - - go func() { - defer close(logChan) - - onConnect := func() { - cmd.ui.Say("\n%s", terminal.HeaderColor("Staging...")) - } - - err := cmd.logRepo.TailLogsFor(app.Guid, onConnect, logChan, stopChan, 1) - if err != nil { - cmd.ui.Warn("Warning: error tailing logs") - cmd.ui.Say("%s", err) - } - }() - - cmd.displayLogMessages(logChan) -} - -func (cmd Start) displayLogMessages(logChan chan *logmessage.Message) { - for msg := range logChan { - cmd.ui.Say(simpleLogMessageOutput(msg)) - } -} - -func (cmd Start) waitForInstanceStartup(app cf.Application) []cf.AppInstanceFields { - instances, apiResponse := cmd.appInstancesRepo.GetInstances(app.Guid) - for count := 0; apiResponse.IsNotSuccessful() && count < MaxInstanceStartupPings; count++ { - if apiResponse.ErrorCode != cf.APP_NOT_STAGED { - cmd.ui.Say("") - cmd.ui.Failed(apiResponse.Message) - return []cf.AppInstanceFields{} - } - - cmd.ui.Wait(1 * time.Second) - instances, apiResponse = cmd.appInstancesRepo.GetInstances(app.Guid) - } - return instances -} - -func (cmd Start) displayInstancesStatus(app cf.Application, instances []cf.AppInstanceFields) (notFinished bool) { - totalCount := len(instances) - runningCount, startingCount, flappingCount, downCount := 0, 0, 0, 0 - - for _, inst := range instances { - switch inst.State { - case cf.InstanceRunning: - runningCount++ - case cf.InstanceStarting: - startingCount++ - case cf.InstanceFlapping: - flappingCount++ - case cf.InstanceDown: - downCount++ - } - } - - if flappingCount > 0 { - cmd.ui.Failed("Start unsuccessful") - return false - } - - anyInstanceRunning := runningCount > 0 - - if anyInstanceRunning { - if len(app.Routes) == 0 { - cmd.ui.Say(terminal.HeaderColor("Started")) - } else { - cmd.ui.Say("Started: app %s available at %s", terminal.EntityNameColor(app.Name), terminal.EntityNameColor(app.Routes[0].URL())) - } - return false - } else { - details := instancesDetails(runningCount, startingCount, downCount) - cmd.ui.Say("%d of %d instances running (%s)", runningCount, totalCount, details) - } - - if time.Since(cmd.startTime) > cmd.config.ApplicationStartTimeout*time.Second { - cmd.ui.Failed("Start app timeout") - return false - } - - return totalCount > runningCount -} - -func instancesDetails(runningCount int, startingCount int, downCount int) string { - details := []string{} - - if startingCount > 0 { - details = append(details, fmt.Sprintf("%d starting", startingCount)) - } - - if downCount > 0 { - details = append(details, fmt.Sprintf("%d down", downCount)) - } - - return strings.Join(details, ", ") -} diff --git a/src/cf/commands/application/start_test.go b/src/cf/commands/application/start_test.go deleted file mode 100644 index bdafaeeaf74..00000000000 --- a/src/cf/commands/application/start_test.go +++ /dev/null @@ -1,380 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "code.google.com/p/gogoprotobuf/proto" - "errors" - "github.com/cloudfoundry/loggregatorlib/logmessage" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" - "time" -) - -var ( - defaultAppForStart = cf.Application{} - defaultInstanceReponses = [][]cf.AppInstanceFields{} - defaultInstanceErrorCodes = []string{"", ""} -) - -func init() { - defaultAppForStart.Name = "my-app" - defaultAppForStart.Guid = "my-app-guid" - defaultAppForStart.InstanceCount = 2 - - domain := cf.DomainFields{} - domain.Name = "example.com" - - route := cf.RouteSummary{} - route.Host = "my-app" - route.Domain = domain - - defaultAppForStart.Routes = []cf.RouteSummary{route} - - instance1 := cf.AppInstanceFields{} - instance1.State = cf.InstanceStarting - - instance2 := cf.AppInstanceFields{} - instance2.State = cf.InstanceStarting - - instance3 := cf.AppInstanceFields{} - instance3.State = cf.InstanceRunning - - instance4 := cf.AppInstanceFields{} - instance4.State = cf.InstanceStarting - - defaultInstanceReponses = [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{instance1, instance2}, - []cf.AppInstanceFields{instance3, instance4}, - } -} - -func callStart(args []string, config *configuration.Configuration, reqFactory *testreq.FakeReqFactory, appRepo api.ApplicationRepository, appInstancesRepo api.AppInstancesRepository, logRepo api.LogsRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("start", args) - - cmd := NewStart(ui, config, appRepo, appInstancesRepo, logRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} - -func startAppWithInstancesAndErrors(t *testing.T, app cf.Application, instances [][]cf.AppInstanceFields, errorCodes []string) (ui *testterm.FakeUI, appRepo *testapi.FakeApplicationRepository, appInstancesRepo *testapi.FakeAppInstancesRepo, reqFactory *testreq.FakeReqFactory) { - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - ApplicationStartTimeout: 2, - } - - appRepo = &testapi.FakeApplicationRepository{ - FindByNameApp: app, - StartUpdatedApp: app, - } - appInstancesRepo = &testapi.FakeAppInstancesRepo{ - GetInstancesResponses: instances, - GetInstancesErrorCodes: errorCodes, - } - - currentTime := time.Now() - messageType := logmessage.LogMessage_ERR - sourceType := logmessage.LogMessage_DEA - logMessage1 := logmessage.LogMessage{ - Message: []byte("Log Line 1"), - AppId: proto.String(app.Guid), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(currentTime.UnixNano()), - } - - logMessage2 := logmessage.LogMessage{ - Message: []byte("Log Line 2"), - AppId: proto.String(app.Guid), - MessageType: &messageType, - SourceType: &sourceType, - Timestamp: proto.Int64(currentTime.UnixNano()), - } - - logRepo := &testapi.FakeLogsRepository{ - TailLogMessages: []logmessage.LogMessage{ - logMessage1, - logMessage2, - }, - } - - args := []string{"my-app"} - reqFactory = &testreq.FakeReqFactory{Application: app} - ui = callStart(args, config, reqFactory, appRepo, appInstancesRepo, logRepo) - return -} - -func TestStartCommandFailsWithUsage(t *testing.T) { - t.Parallel() - - config := &configuration.Configuration{} - appRepo := &testapi.FakeApplicationRepository{} - appInstancesRepo := &testapi.FakeAppInstancesRepo{ - GetInstancesResponses: [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{}, - }, - GetInstancesErrorCodes: []string{""}, - } - logRepo := &testapi.FakeLogsRepository{} - - reqFactory := &testreq.FakeReqFactory{} - - ui := callStart([]string{}, config, reqFactory, appRepo, appInstancesRepo, logRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callStart([]string{"my-app"}, config, reqFactory, appRepo, appInstancesRepo, logRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestStartApplication(t *testing.T) { - t.Parallel() - - ui, appRepo, _, reqFactory := startAppWithInstancesAndErrors(t, defaultAppForStart, defaultInstanceReponses, defaultInstanceErrorCodes) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[6], "0 of 2 instances running (2 starting)") - assert.Contains(t, ui.Outputs[7], "Started") - assert.Contains(t, ui.Outputs[7], "my-app") - assert.Contains(t, ui.Outputs[7], "my-app.example.com") - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, appRepo.StartAppGuid, "my-app-guid") -} - -func TestStartApplicationWhenAppHasNoURL(t *testing.T) { - t.Parallel() - - app := defaultAppForStart - app.Routes = []cf.RouteSummary{} - appInstance5 := cf.AppInstanceFields{} - appInstance5.State = cf.InstanceRunning - instances := [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{appInstance5}, - } - - errorCodes := []string{""} - ui, appRepo, _, reqFactory := startAppWithInstancesAndErrors(t, app, instances, errorCodes) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[6], "Started") - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, appRepo.StartAppGuid, "my-app-guid") -} - -func TestStartApplicationWhenAppIsStillStaging(t *testing.T) { - t.Parallel() - appInstance6 := cf.AppInstanceFields{} - appInstance6.State = cf.InstanceDown - appInstance7 := cf.AppInstanceFields{} - appInstance7.State = cf.InstanceStarting - appInstance8 := cf.AppInstanceFields{} - appInstance8.State = cf.InstanceStarting - appInstance9 := cf.AppInstanceFields{} - appInstance9.State = cf.InstanceStarting - appInstance10 := cf.AppInstanceFields{} - appInstance10.State = cf.InstanceRunning - appInstance11 := cf.AppInstanceFields{} - appInstance11.State = cf.InstanceRunning - instances := [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{}, - []cf.AppInstanceFields{}, - []cf.AppInstanceFields{appInstance6, appInstance7}, - []cf.AppInstanceFields{appInstance8, appInstance9}, - []cf.AppInstanceFields{appInstance10, appInstance11}, - } - - errorCodes := []string{cf.APP_NOT_STAGED, cf.APP_NOT_STAGED, "", "", ""} - - ui, _, appInstancesRepo, _ := startAppWithInstancesAndErrors(t, defaultAppForStart, instances, errorCodes) - - assert.Equal(t, appInstancesRepo.GetInstancesAppGuid, "my-app-guid") - - assert.Contains(t, ui.Outputs[2], "Staging") - assert.Contains(t, ui.Outputs[3], "Log Line 1") - assert.Contains(t, ui.Outputs[4], "Log Line 2") - - assert.Contains(t, ui.Outputs[6], "0 of 2 instances running (1 starting, 1 down)") - assert.Contains(t, ui.Outputs[7], "0 of 2 instances running (2 starting)") -} - -func TestStartApplicationWhenStagingFails(t *testing.T) { - t.Parallel() - - instances := [][]cf.AppInstanceFields{[]cf.AppInstanceFields{}} - errorCodes := []string{"170001"} - - ui, _, _, _ := startAppWithInstancesAndErrors(t, defaultAppForStart, instances, errorCodes) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[6], "FAILED") - assert.Contains(t, ui.Outputs[7], "Error staging app") -} - -func TestStartApplicationWhenOneInstanceFlaps(t *testing.T) { - t.Parallel() - appInstance12 := cf.AppInstanceFields{} - appInstance12.State = cf.InstanceStarting - appInstance13 := cf.AppInstanceFields{} - appInstance13.State = cf.InstanceStarting - appInstance14 := cf.AppInstanceFields{} - appInstance14.State = cf.InstanceStarting - appInstance15 := cf.AppInstanceFields{} - appInstance15.State = cf.InstanceFlapping - instances := [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{appInstance12, appInstance13}, - []cf.AppInstanceFields{appInstance14, appInstance15}, - } - - errorCodes := []string{"", ""} - - ui, _, _, _ := startAppWithInstancesAndErrors(t, defaultAppForStart, instances, errorCodes) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[6], "0 of 2 instances running (2 starting)") - assert.Contains(t, ui.Outputs[7], "FAILED") - assert.Contains(t, ui.Outputs[8], "Start unsuccessful") -} - -func TestStartApplicationWhenStartTimesOut(t *testing.T) { - t.Parallel() - appInstance16 := cf.AppInstanceFields{} - appInstance16.State = cf.InstanceStarting - appInstance17 := cf.AppInstanceFields{} - appInstance17.State = cf.InstanceStarting - appInstance18 := cf.AppInstanceFields{} - appInstance18.State = cf.InstanceStarting - appInstance19 := cf.AppInstanceFields{} - appInstance19.State = cf.InstanceDown - appInstance20 := cf.AppInstanceFields{} - appInstance20.State = cf.InstanceDown - appInstance21 := cf.AppInstanceFields{} - appInstance21.State = cf.InstanceDown - instances := [][]cf.AppInstanceFields{ - []cf.AppInstanceFields{appInstance16, appInstance17}, - []cf.AppInstanceFields{appInstance18, appInstance19}, - []cf.AppInstanceFields{appInstance20, appInstance21}, - } - - errorCodes := []string{"", "", ""} - - ui, _, _, _ := startAppWithInstancesAndErrors(t, defaultAppForStart, instances, errorCodes) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[6], "0 of 2 instances running (2 starting)") - assert.Contains(t, ui.Outputs[7], "0 of 2 instances running (1 starting, 1 down)") - assert.Contains(t, ui.Outputs[8], "0 of 2 instances running (2 down)") - assert.Contains(t, ui.Outputs[9], "FAILED") - assert.Contains(t, ui.Outputs[10], "Start app timeout") -} - -func TestStartApplicationWhenStartFails(t *testing.T) { - t.Parallel() - - config := &configuration.Configuration{} - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app, StartAppErr: true} - appInstancesRepo := &testapi.FakeAppInstancesRepo{} - logRepo := &testapi.FakeLogsRepository{} - args := []string{"my-app"} - reqFactory := &testreq.FakeReqFactory{Application: app} - ui := callStart(args, config, reqFactory, appRepo, appInstancesRepo, logRepo) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "Error starting application") - assert.Equal(t, appRepo.StartAppGuid, "my-app-guid") -} - -func TestStartApplicationIsAlreadyStarted(t *testing.T) { - t.Parallel() - - config := &configuration.Configuration{} - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.State = "started" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - appInstancesRepo := &testapi.FakeAppInstancesRepo{} - logRepo := &testapi.FakeLogsRepository{} - - reqFactory := &testreq.FakeReqFactory{Application: app} - - args := []string{"my-app"} - ui := callStart(args, config, reqFactory, appRepo, appInstancesRepo, logRepo) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "is already started") - assert.Equal(t, appRepo.StartAppGuid, "") -} - -func TestStartApplicationWithLoggingFailure(t *testing.T) { - t.Parallel() - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{Username: "my-user"}) - assert.NoError(t, err) - space2 := cf.SpaceFields{} - space2.Name = "my-space" - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - ApplicationStartTimeout: 2, - } - - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: defaultAppForStart} - appInstancesRepo := &testapi.FakeAppInstancesRepo{ - GetInstancesResponses: defaultInstanceReponses, - GetInstancesErrorCodes: defaultInstanceErrorCodes, - } - - logRepo := &testapi.FakeLogsRepository{ - TailLogErr: errors.New("Ooops"), - } - - reqFactory := &testreq.FakeReqFactory{Application: defaultAppForStart} - - ui := new(testterm.FakeUI) - - ctxt := testcmd.NewContext("start", []string{"my-app"}) - - cmd := NewStart(ui, config, appRepo, appInstancesRepo, logRepo) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - testassert.SliceContains(t, ui.Outputs, []string{ - "error tailing logs", - "Ooops", - }) -} diff --git a/src/cf/commands/application/stop.go b/src/cf/commands/application/stop.go deleted file mode 100644 index 26be1a1a38b..00000000000 --- a/src/cf/commands/application/stop.go +++ /dev/null @@ -1,74 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type ApplicationStopper interface { - ApplicationStop(app cf.Application) (updatedApp cf.Application, err error) -} - -type Stop struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appReq requirements.ApplicationRequirement -} - -func NewStop(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository) (cmd *Stop) { - cmd = new(Stop) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - - return -} - -func (cmd *Stop) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) == 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "stop") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{cmd.appReq} - return -} - -func (cmd *Stop) ApplicationStop(app cf.Application) (updatedApp cf.Application, err error) { - if app.State == "stopped" { - updatedApp = app - cmd.ui.Say(terminal.WarningColor("App " + app.Name + " is already stopped")) - return - } - - cmd.ui.Say("Stopping app %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - updatedApp, apiResponse := cmd.appRepo.Stop(app.Guid) - if apiResponse.IsNotSuccessful() { - err = errors.New(apiResponse.Message) - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - return -} - -func (cmd *Stop) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - cmd.ApplicationStop(app) -} diff --git a/src/cf/commands/application/stop_test.go b/src/cf/commands/application/stop_test.go deleted file mode 100644 index f4f76eee820..00000000000 --- a/src/cf/commands/application/stop_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestStopCommandFailsWithUsage(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - reqFactory := &testreq.FakeReqFactory{Application: app} - - ui := callStop(t, []string{}, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callStop(t, []string{"my-app"}, reqFactory, appRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestStopApplication(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - args := []string{"my-app"} - reqFactory := &testreq.FakeReqFactory{Application: app} - ui := callStop(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Stopping app") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, appRepo.StopAppGuid, "my-app-guid") -} - -func TestStopApplicationWhenStopFails(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app, StopAppErr: true} - args := []string{"my-app"} - reqFactory := &testreq.FakeReqFactory{Application: app} - ui := callStop(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "Error stopping application") - assert.Equal(t, appRepo.StopAppGuid, "my-app-guid") -} - -func TestStopApplicationIsAlreadyStopped(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.State = "stopped" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - args := []string{"my-app"} - reqFactory := &testreq.FakeReqFactory{Application: app} - ui := callStop(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "is already stopped") - assert.Equal(t, appRepo.StopAppGuid, "") -} - -func TestApplicationStopReturnsUpdatedApp(t *testing.T) { - appToStop := cf.Application{} - appToStop.Name = "my-app" - appToStop.Guid = "my-app-guid" - appToStop.State = "started" - expectedStoppedApp := cf.Application{} - expectedStoppedApp.Name = "my-stopped-app" - expectedStoppedApp.Guid = "my-stopped-app-guid" - expectedStoppedApp.State = "stopped" - - appRepo := &testapi.FakeApplicationRepository{StopUpdatedApp: expectedStoppedApp} - config := &configuration.Configuration{} - stopper := NewStop(new(testterm.FakeUI), config, appRepo) - actualStoppedApp, err := stopper.ApplicationStop(appToStop) - - assert.NoError(t, err) - assert.Equal(t, expectedStoppedApp, actualStoppedApp) -} - -func TestApplicationStopReturnsUpdatedAppWhenAppIsAlreadyStopped(t *testing.T) { - appToStop := cf.Application{} - appToStop.Name = "my-app" - appToStop.Guid = "my-app-guid" - appToStop.State = "stopped" - appRepo := &testapi.FakeApplicationRepository{} - config := &configuration.Configuration{} - stopper := NewStop(new(testterm.FakeUI), config, appRepo) - updatedApp, err := stopper.ApplicationStop(appToStop) - - assert.NoError(t, err) - assert.Equal(t, appToStop, updatedApp) -} - -func callStop(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appRepo api.ApplicationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("stop", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewStop(ui, config, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/application/unset_env.go b/src/cf/commands/application/unset_env.go deleted file mode 100644 index b6e77e143ab..00000000000 --- a/src/cf/commands/application/unset_env.go +++ /dev/null @@ -1,75 +0,0 @@ -package application - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UnsetEnv struct { - ui terminal.UI - config *configuration.Configuration - appRepo api.ApplicationRepository - appReq requirements.ApplicationRequirement -} - -func NewUnsetEnv(ui terminal.UI, config *configuration.Configuration, appRepo api.ApplicationRepository) (cmd *UnsetEnv) { - cmd = new(UnsetEnv) - cmd.ui = ui - cmd.config = config - cmd.appRepo = appRepo - return -} - -func (cmd *UnsetEnv) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "unset-env") - return - } - - cmd.appReq = reqFactory.NewApplicationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.appReq, - } - return -} - -func (cmd *UnsetEnv) Run(c *cli.Context) { - varName := c.Args()[1] - app := cmd.appReq.GetApplication() - - cmd.ui.Say("Removing env variable %s from app %s in org %s / space %s as %s...", - terminal.EntityNameColor(varName), - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - envVars := app.EnvironmentVars - - if !envVarFound(varName, envVars) { - cmd.ui.Ok() - cmd.ui.Warn("Env variable %s was not set.", varName) - return - } - - delete(envVars, varName) - - apiResponse := cmd.appRepo.SetEnv(app.Guid, envVars) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("TIP: Use '%s push' to ensure your env variable changes take effect", cf.Name()) -} diff --git a/src/cf/commands/application/unset_env_test.go b/src/cf/commands/application/unset_env_test.go deleted file mode 100644 index 91b5c0ef7c5..00000000000 --- a/src/cf/commands/application/unset_env_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package application_test - -import ( - "cf" - "cf/api" - . "cf/commands/application" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUnsetEnvRequirements(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{} - args := []string{"my-app", "DATABASE_URL"} - - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - callUnsetEnv(t, args, reqFactory, appRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: false, TargetedSpaceSuccess: true} - callUnsetEnv(t, args, reqFactory, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: false} - callUnsetEnv(t, args, reqFactory, appRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestUnsetEnvWhenApplicationExists(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.EnvironmentVars = map[string]string{"foo": "bar", "DATABASE_URL": "mysql://example.com/my-db"} - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{} - - args := []string{"my-app", "DATABASE_URL"} - ui := callUnsetEnv(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Removing env variable") - assert.Contains(t, ui.Outputs[0], "DATABASE_URL") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, appRepo.SetEnvAppGuid, "my-app-guid") - assert.Equal(t, appRepo.SetEnvVars, map[string]string{"foo": "bar"}) -} - -func TestUnsetEnvWhenUnsettingTheEnvFails(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - app.EnvironmentVars = map[string]string{"DATABASE_URL": "mysql://example.com/my-db"} - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{ - FindByNameApp: app, - SetEnvErr: true, - } - - args := []string{"does-not-exist", "DATABASE_URL"} - ui := callUnsetEnv(t, args, reqFactory, appRepo) - - assert.Contains(t, ui.Outputs[0], "Removing env variable") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "Failed setting env") -} - -func TestUnsetEnvWhenEnvVarDoesNotExist(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{} - - args := []string{"my-app", "DATABASE_URL"} - ui := callUnsetEnv(t, args, reqFactory, appRepo) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "Removing env variable") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "DATABASE_URL") - assert.Contains(t, ui.Outputs[2], "was not set.") -} - -func TestUnsetEnvFailsWithUsage(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - reqFactory := &testreq.FakeReqFactory{Application: app, LoginSuccess: true, TargetedSpaceSuccess: true} - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - - args := []string{"my-app", "DATABASE_URL"} - ui := callUnsetEnv(t, args, reqFactory, appRepo) - assert.False(t, ui.FailedWithUsage) - - args = []string{"my-app"} - ui = callUnsetEnv(t, args, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) - - args = []string{} - ui = callUnsetEnv(t, args, reqFactory, appRepo) - assert.True(t, ui.FailedWithUsage) -} - -func callUnsetEnv(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, appRepo api.ApplicationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("unset-env", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewUnsetEnv(ui, config, appRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/authenticate.go b/src/cf/commands/authenticate.go deleted file mode 100644 index 51f52ffdc13..00000000000 --- a/src/cf/commands/authenticate.go +++ /dev/null @@ -1,62 +0,0 @@ -package commands - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Authenticate struct { - ui terminal.UI - config *configuration.Configuration - configRepo configuration.ConfigurationRepository - authenticator api.AuthenticationRepository -} - -func NewAuthenticate(ui terminal.UI, configRepo configuration.ConfigurationRepository, authenticator api.AuthenticationRepository) (cmd Authenticate) { - cmd.ui = ui - cmd.configRepo = configRepo - cmd.config, _ = configRepo.Get() - cmd.authenticator = authenticator - return -} - -func (cmd Authenticate) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) < 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "auth") - return - } - return -} - -func (cmd Authenticate) Run(c *cli.Context) { - cmd.ui.Say("API endpoint: %s", terminal.EntityNameColor(cmd.config.Target)) - - username := c.Args()[0] - password := c.Args()[1] - - cmd.ui.Say("Authenticating...") - - apiResponse := cmd.doLogin(username, password) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - return -} - -func (cmd Authenticate) doLogin(username, password string) (apiResponse net.ApiResponse) { - apiResponse = cmd.authenticator.Authenticate(username, password) - if apiResponse.IsSuccessful() { - cmd.ui.Ok() - cmd.ui.Say("Use '%s' to view or set your target org and space", terminal.CommandColor(cf.Name()+" target")) - } - return -} diff --git a/src/cf/commands/authenticate_test.go b/src/cf/commands/authenticate_test.go deleted file mode 100644 index 1974e7d5b80..00000000000 --- a/src/cf/commands/authenticate_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package commands_test - -import ( - "cf/api" - . "cf/commands" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func testSuccessfulAuthenticate(t *testing.T, args []string) (ui *testterm.FakeUI) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - config, _ := configRepo.Get() - - auth := &testapi.FakeAuthenticationRepository{ - AccessToken: "my_access_token", - RefreshToken: "my_refresh_token", - ConfigRepo: configRepo, - } - ui = callAuthenticate( - args, - configRepo, - auth, - ) - - savedConfig := testconfig.SavedConfiguration - - assert.Contains(t, ui.Outputs[0], config.Target) - assert.Contains(t, ui.Outputs[2], "OK") - - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - assert.Equal(t, auth.Email, "user@example.com") - assert.Equal(t, auth.Password, "password") - - return -} - -func TestAuthenticateFailsWithUsage(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - - auth := &testapi.FakeAuthenticationRepository{ - AccessToken: "my_access_token", - RefreshToken: "my_refresh_token", - ConfigRepo: configRepo, - } - - ui := callAuthenticate([]string{}, configRepo, auth) - assert.True(t, ui.FailedWithUsage) - - ui = callAuthenticate([]string{"my-username"}, configRepo, auth) - assert.True(t, ui.FailedWithUsage) - - ui = callAuthenticate([]string{"my-username", "my-password"}, configRepo, auth) - assert.False(t, ui.FailedWithUsage) - -} - -func TestSuccessfullyAuthenticatingWithUsernameAndPasswordAsArguments(t *testing.T) { - testSuccessfulAuthenticate(t, []string{"user@example.com", "password"}) -} - -func TestUnsuccessfullyAuthenticatingWithoutInteractivity(t *testing.T) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - config, _ := configRepo.Get() - - ui := callAuthenticate( - []string{ - "foo@example.com", - "bar", - }, - configRepo, - &testapi.FakeAuthenticationRepository{AuthError: true, ConfigRepo: configRepo}, - ) - - assert.Contains(t, ui.Outputs[0], config.Target) - assert.Equal(t, ui.Outputs[1], "Authenticating...") - assert.Equal(t, ui.Outputs[2], "FAILED") - assert.Contains(t, ui.Outputs[3], "Error authenticating") - assert.Equal(t, len(ui.Outputs), 4) -} - -func callAuthenticate(args []string, configRepo configuration.ConfigurationRepository, auth api.AuthenticationRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("auth", args) - cmd := NewAuthenticate(ui, configRepo, auth) - testcmd.RunCommand(cmd, ctxt, &testreq.FakeReqFactory{}) - return -} diff --git a/src/cf/commands/buildpack/create_buildpack.go b/src/cf/commands/buildpack/create_buildpack.go deleted file mode 100644 index fb0cfdccb71..00000000000 --- a/src/cf/commands/buildpack/create_buildpack.go +++ /dev/null @@ -1,78 +0,0 @@ -package buildpack - -import ( - "cf" - "cf/api" - "cf/net" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strconv" -) - -type CreateBuildpack struct { - ui terminal.UI - buildpackRepo api.BuildpackRepository - buildpackBitsRepo api.BuildpackBitsRepository -} - -func NewCreateBuildpack(ui terminal.UI, buildpackRepo api.BuildpackRepository, buildpackBitsRepo api.BuildpackBitsRepository) (cmd CreateBuildpack) { - cmd.ui = ui - cmd.buildpackRepo = buildpackRepo - cmd.buildpackBitsRepo = buildpackBitsRepo - return -} - -func (cmd CreateBuildpack) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd CreateBuildpack) Run(c *cli.Context) { - if len(c.Args()) != 3 { - cmd.ui.FailWithUsage(c, "create-buildpack") - return - } - - buildpackName := c.Args()[0] - - cmd.ui.Say("Creating buildpack %s...", terminal.EntityNameColor(buildpackName)) - - buildpack, apiResponse := cmd.createBuildpack(buildpackName, c) - if apiResponse.IsNotSuccessful() { - if apiResponse.ErrorCode == cf.BUILDPACK_EXISTS { - cmd.ui.Ok() - cmd.ui.Warn("Buildpack %s already exists", buildpackName) - } else { - cmd.ui.Failed(apiResponse.Message) - } - return - } - cmd.ui.Ok() - cmd.ui.Say("") - - cmd.ui.Say("Uploading buildpack %s...", terminal.EntityNameColor(buildpackName)) - - dir := c.Args()[1] - - apiResponse = cmd.buildpackBitsRepo.UploadBuildpack(buildpack, dir) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} - -func (cmd CreateBuildpack) createBuildpack(buildpackName string, c *cli.Context) (buildpack cf.Buildpack, apiResponse net.ApiResponse) { - position, err := strconv.Atoi(c.Args()[2]) - if err != nil { - apiResponse = net.NewApiResponseWithMessage("Invalid position. %s", err.Error()) - } - - buildpack, apiResponse = cmd.buildpackRepo.Create(buildpackName, &position) - - return -} diff --git a/src/cf/commands/buildpack/create_buildpack_test.go b/src/cf/commands/buildpack/create_buildpack_test.go deleted file mode 100644 index 6791b4e2741..00000000000 --- a/src/cf/commands/buildpack/create_buildpack_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package buildpack_test - -import ( - "cf" - . "cf/commands/buildpack" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateBuildpackRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - - repo.FindByNameBuildpack = cf.Buildpack{} - callCreateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callCreateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateBuildpack(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - fakeUI := callCreateBuildpack([]string{"my-buildpack", "my.war", "5"}, reqFactory, repo, bitsRepo) - - assert.Equal(t, len(fakeUI.Outputs), 5) - assert.Contains(t, fakeUI.Outputs[0], "Creating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Uploading buildpack") - assert.Contains(t, fakeUI.Outputs[3], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[4], "OK") -} - -func TestCreateBuildpackWhenItAlreadyExists(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - - repo.CreateBuildpackExists = true - fakeUI := callCreateBuildpack([]string{"my-buildpack", "my.war", "5"}, reqFactory, repo, bitsRepo) - - assert.Equal(t, len(fakeUI.Outputs), 3) - assert.Contains(t, fakeUI.Outputs[0], "Creating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[2], "already exists") -} - -func TestCreateBuildpackWithPosition(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - fakeUI := callCreateBuildpack([]string{"my-buildpack", "my.war", "5"}, reqFactory, repo, bitsRepo) - - assert.Equal(t, len(fakeUI.Outputs), 5) - assert.Contains(t, fakeUI.Outputs[0], "Creating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Uploading buildpack") - assert.Contains(t, fakeUI.Outputs[3], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[4], "OK") -} - -func TestCreateBuildpackWithInvalidPath(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - - bitsRepo.UploadBuildpackErr = true - fakeUI := callCreateBuildpack([]string{"my-buildpack", "bogus/path", "5"}, reqFactory, repo, bitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[3], "Uploading buildpack") - assert.Contains(t, fakeUI.Outputs[4], "FAILED") -} - -func TestCreateBuildpackFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo, bitsRepo := getRepositories() - - fakeUI := callCreateBuildpack([]string{}, reqFactory, repo, bitsRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callCreateBuildpack([]string{"my-buildpack", "my.war", "5"}, reqFactory, repo, bitsRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func getRepositories() (*testapi.FakeBuildpackRepository, *testapi.FakeBuildpackBitsRepository) { - return &testapi.FakeBuildpackRepository{}, &testapi.FakeBuildpackBitsRepository{} -} - -func callCreateBuildpack(args []string, reqFactory *testreq.FakeReqFactory, fakeRepo *testapi.FakeBuildpackRepository, - fakeBitsRepo *testapi.FakeBuildpackBitsRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-buildpack", args) - - cmd := NewCreateBuildpack(ui, fakeRepo, fakeBitsRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/buildpack/delete_buildpack.go b/src/cf/commands/buildpack/delete_buildpack.go deleted file mode 100644 index 51b23d4b82b..00000000000 --- a/src/cf/commands/buildpack/delete_buildpack.go +++ /dev/null @@ -1,73 +0,0 @@ -package buildpack - -import ( - "cf/api" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteBuildpack struct { - ui terminal.UI - buildpackRepo api.BuildpackRepository -} - -func NewDeleteBuildpack(ui terminal.UI, repo api.BuildpackRepository) (cmd *DeleteBuildpack) { - cmd = new(DeleteBuildpack) - cmd.ui = ui - cmd.buildpackRepo = repo - return -} - -func (cmd *DeleteBuildpack) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-buildpack") - return - } - - loginReq := reqFactory.NewLoginRequirement() - - reqs = []requirements.Requirement{ - loginReq, - } - - return -} - -func (cmd *DeleteBuildpack) Run(c *cli.Context) { - buildpackName := c.Args()[0] - - force := c.Bool("f") - - if !force { - answer := cmd.ui.Confirm("Are you sure you want to delete the buildpack %s ?", terminal.EntityNameColor(buildpackName)) - if !answer { - return - } - } - - cmd.ui.Say("Deleting buildpack %s...", terminal.EntityNameColor(buildpackName)) - - buildpack, apiResponse := cmd.buildpackRepo.FindByName(buildpackName) - - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Buildpack %s does not exist.", buildpackName) - return - } - - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - apiResponse = cmd.buildpackRepo.Delete(buildpack.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error deleting buildpack %s\n%s", terminal.EntityNameColor(buildpack.Name), apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/buildpack/delete_buildpack_test.go b/src/cf/commands/buildpack/delete_buildpack_test.go deleted file mode 100644 index 195596c4f1f..00000000000 --- a/src/cf/commands/buildpack/delete_buildpack_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package buildpack_test - -import ( - "cf" - . "cf/commands/buildpack" - "cf/net" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteBuildpackGetRequirements(t *testing.T) { - ui := &testterm.FakeUI{Inputs: []string{"y"}} - buildpackRepo := &testapi.FakeBuildpackRepository{} - cmd := NewDeleteBuildpack(ui, buildpackRepo) - - ctxt := testcmd.NewContext("delete-buildpack", []string{"my-buildpack"}) - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteBuildpackSuccess(t *testing.T) { - ui := &testterm.FakeUI{Inputs: []string{"y"}} - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{ - FindByNameBuildpack: buildpack, - } - cmd := NewDeleteBuildpack(ui, buildpackRepo) - - ctxt := testcmd.NewContext("delete-buildpack", []string{"my-buildpack"}) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, buildpackRepo.DeleteBuildpackGuid, "my-buildpack-guid") - - assert.Contains(t, ui.Prompts[0], "delete") - assert.Contains(t, ui.Prompts[0], "my-buildpack") - - assert.Contains(t, ui.Outputs[0], "Deleting buildpack") - assert.Contains(t, ui.Outputs[0], "my-buildpack") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteBuildpackNoConfirmation(t *testing.T) { - ui := &testterm.FakeUI{Inputs: []string{"no"}} - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{ - FindByNameBuildpack: buildpack, - } - cmd := NewDeleteBuildpack(ui, buildpackRepo) - - ctxt := testcmd.NewContext("delete-buildpack", []string{"my-buildpack"}) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, buildpackRepo.DeleteBuildpackGuid, "") - - assert.Contains(t, ui.Prompts[0], "delete") - assert.Contains(t, ui.Prompts[0], "my-buildpack") -} - -func TestDeleteBuildpackThatDoesNotExist(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{ - FindByNameNotFound: true, - FindByNameBuildpack: buildpack, - } - - ui := &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete-buildpack", []string{"-f", "my-buildpack"}) - - cmd := NewDeleteBuildpack(ui, buildpackRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, buildpackRepo.FindByNameName, "my-buildpack") - assert.True(t, buildpackRepo.FindByNameNotFound) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "my-buildpack") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "my-buildpack") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func TestDeleteBuildpackDeleteError(t *testing.T) { - ui := &testterm.FakeUI{Inputs: []string{"y"}} - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{ - FindByNameBuildpack: buildpack, - DeleteApiResponse: net.NewApiResponseWithMessage("failed badly"), - } - - cmd := NewDeleteBuildpack(ui, buildpackRepo) - - ctxt := testcmd.NewContext("delete-buildpack", []string{"my-buildpack"}) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, buildpackRepo.DeleteBuildpackGuid, "my-buildpack-guid") - - assert.Contains(t, ui.Outputs[0], "Deleting buildpack") - assert.Contains(t, ui.Outputs[0], "my-buildpack") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "my-buildpack") - assert.Contains(t, ui.Outputs[2], "failed badly") -} - -func TestDeleteBuildpackForceFlagSkipsConfirmation(t *testing.T) { - ui := &testterm.FakeUI{} - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{ - FindByNameBuildpack: buildpack, - } - - cmd := NewDeleteBuildpack(ui, buildpackRepo) - - ctxt := testcmd.NewContext("delete-buildpack", []string{"-f", "my-buildpack"}) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - assert.Equal(t, buildpackRepo.DeleteBuildpackGuid, "my-buildpack-guid") - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting buildpack") - assert.Contains(t, ui.Outputs[0], "my-buildpack") - assert.Contains(t, ui.Outputs[1], "OK") -} diff --git a/src/cf/commands/buildpack/list_buildpacks.go b/src/cf/commands/buildpack/list_buildpacks.go deleted file mode 100644 index f68cc45ab2a..00000000000 --- a/src/cf/commands/buildpack/list_buildpacks.go +++ /dev/null @@ -1,65 +0,0 @@ -package buildpack - -import ( - "cf/api" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strconv" -) - -type ListBuildpacks struct { - ui terminal.UI - buildpackRepo api.BuildpackRepository -} - -func NewListBuildpacks(ui terminal.UI, buildpackRepo api.BuildpackRepository) (cmd ListBuildpacks) { - cmd.ui = ui - cmd.buildpackRepo = buildpackRepo - return -} - -func (cmd ListBuildpacks) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd ListBuildpacks) Run(c *cli.Context) { - cmd.ui.Say("Getting buildpacks...\n") - - stopChan := make(chan bool) - defer close(stopChan) - - buildpackChan, statusChan := cmd.buildpackRepo.ListBuildpacks(stopChan) - - table := cmd.ui.Table([]string{"buildpack", "position"}) - noBuildpacks := true - - for buildpacks := range buildpackChan { - rows := [][]string{} - for _, buildpack := range buildpacks { - position := "" - if buildpack.Position != nil { - position = strconv.Itoa(*buildpack.Position) - } - rows = append(rows, []string{ - buildpack.Name, - position, - }) - } - table.Print(rows) - noBuildpacks = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching buildpacks.\n%s", apiStatus.Message) - return - } - - if noBuildpacks { - cmd.ui.Say("No buildpacks found") - } -} diff --git a/src/cf/commands/buildpack/list_buildpacks_test.go b/src/cf/commands/buildpack/list_buildpacks_test.go deleted file mode 100644 index d544858c045..00000000000 --- a/src/cf/commands/buildpack/list_buildpacks_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package buildpack_test - -import ( - "cf" - "cf/commands/buildpack" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListBuildpacksRequirements(t *testing.T) { - buildpackRepo := &testapi.FakeBuildpackRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callListBuildpacks(reqFactory, buildpackRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callListBuildpacks(reqFactory, buildpackRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListBuildpacks(t *testing.T) { - buildpackBuilder := func(name string, position int) (buildpack cf.Buildpack) { - buildpack.Name = name - buildpack.Position = &position - return - } - - buildpacks := []cf.Buildpack{ - buildpackBuilder("Buildpack-1", 5), - buildpackBuilder("Buildpack-2", 10), - buildpackBuilder("Buildpack-3", 15), - } - - buildpackRepo := &testapi.FakeBuildpackRepository{ - Buildpacks: buildpacks, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - ui := callListBuildpacks(reqFactory, buildpackRepo) - - assert.Contains(t, ui.Outputs[0], "Getting buildpacks") - - assert.Contains(t, ui.Outputs[1], "buildpack") - assert.Contains(t, ui.Outputs[1], "position") - - assert.Contains(t, ui.Outputs[2], "Buildpack-1") - assert.Contains(t, ui.Outputs[2], "5") - - assert.Contains(t, ui.Outputs[3], "Buildpack-2") - assert.Contains(t, ui.Outputs[3], "10") - - assert.Contains(t, ui.Outputs[4], "Buildpack-3") - assert.Contains(t, ui.Outputs[4], "15") -} - -func TestListingBuildpacksWhenNoneExist(t *testing.T) { - buildpacks := []cf.Buildpack{} - buildpackRepo := &testapi.FakeBuildpackRepository{ - Buildpacks: buildpacks, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - ui := callListBuildpacks(reqFactory, buildpackRepo) - - assert.Contains(t, ui.Outputs[0], "Getting buildpacks") - assert.Contains(t, ui.Outputs[1], "No buildpacks found") -} - -func callListBuildpacks(reqFactory *testreq.FakeReqFactory, buildpackRepo *testapi.FakeBuildpackRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{} - ctxt := testcmd.NewContext("buildpacks", []string{}) - cmd := buildpack.NewListBuildpacks(fakeUI, buildpackRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/buildpack/update_buildpack.go b/src/cf/commands/buildpack/update_buildpack.go deleted file mode 100644 index c71f7a22361..00000000000 --- a/src/cf/commands/buildpack/update_buildpack.go +++ /dev/null @@ -1,74 +0,0 @@ -package buildpack - -import ( - "cf/api" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UpdateBuildpack struct { - ui terminal.UI - buildpackRepo api.BuildpackRepository - buildpackBitsRepo api.BuildpackBitsRepository - buildpackReq requirements.BuildpackRequirement -} - -func NewUpdateBuildpack(ui terminal.UI, repo api.BuildpackRepository, bitsRepo api.BuildpackBitsRepository) (cmd *UpdateBuildpack) { - cmd = new(UpdateBuildpack) - cmd.ui = ui - cmd.buildpackRepo = repo - cmd.buildpackBitsRepo = bitsRepo - return -} - -func (cmd *UpdateBuildpack) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "update-buildpack") - return - } - - loginReq := reqFactory.NewLoginRequirement() - cmd.buildpackReq = reqFactory.NewBuildpackRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - loginReq, - cmd.buildpackReq, - } - - return -} - -func (cmd *UpdateBuildpack) Run(c *cli.Context) { - buildpack := cmd.buildpackReq.GetBuildpack() - - cmd.ui.Say("Updating buildpack %s...", terminal.EntityNameColor(buildpack.Name)) - - updateBuildpack := false - - if c.String("i") != "" { - val := c.Int("i") - buildpack.Position = &val - updateBuildpack = true - } - - if updateBuildpack { - buildpack, apiResponse := cmd.buildpackRepo.Update(buildpack) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error updating buildpack %s\n%s", terminal.EntityNameColor(buildpack.Name), apiResponse.Message) - return - } - } - - dir := c.String("p") - if dir != "" { - apiResponse := cmd.buildpackBitsRepo.UploadBuildpack(buildpack, dir) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error uploading buildpack %s\n%s", terminal.EntityNameColor(buildpack.Name), apiResponse.Message) - return - } - } - cmd.ui.Ok() -} diff --git a/src/cf/commands/buildpack/update_buildpack_test.go b/src/cf/commands/buildpack/update_buildpack_test.go deleted file mode 100644 index 594095c8b84..00000000000 --- a/src/cf/commands/buildpack/update_buildpack_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package buildpack_test - -import ( - . "cf/commands/buildpack" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUpdateBuildpackRequirements(t *testing.T) { - repo, bitsRepo := getRepositories() - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} - callUpdateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: false} - callUpdateBuildpack([]string{"my-buildpack", "-p", "buildpack.zip", "extraArg"}, reqFactory, repo, bitsRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: false} - callUpdateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, BuildpackSuccess: true} - callUpdateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestUpdateBuildpack(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} - repo, bitsRepo := getRepositories() - - fakeUI := callUpdateBuildpack([]string{"my-buildpack"}, reqFactory, repo, bitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Updating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestUpdateBuildpackPosition(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} - repo, bitsRepo := getRepositories() - - fakeUI := callUpdateBuildpack([]string{"-i", "999", "my-buildpack"}, reqFactory, repo, bitsRepo) - - assert.Equal(t, *repo.UpdateBuildpack.Position, 999) - - assert.Contains(t, fakeUI.Outputs[0], "Updating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestUpdateBuildpackPath(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} - repo, bitsRepo := getRepositories() - - fakeUI := callUpdateBuildpack([]string{"-p", "buildpack.zip", "my-buildpack"}, reqFactory, repo, bitsRepo) - - assert.Equal(t, bitsRepo.UploadBuildpackPath, "buildpack.zip") - - assert.Contains(t, fakeUI.Outputs[0], "Updating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestUpdateBuildpackWithInvalidPath(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, BuildpackSuccess: true} - repo, bitsRepo := getRepositories() - bitsRepo.UploadBuildpackErr = true - - fakeUI := callUpdateBuildpack([]string{"-p", "bogus/path", "my-buildpack"}, reqFactory, repo, bitsRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Updating buildpack") - assert.Contains(t, fakeUI.Outputs[0], "my-buildpack") - assert.Contains(t, fakeUI.Outputs[1], "FAILED") -} - -func callUpdateBuildpack(args []string, reqFactory *testreq.FakeReqFactory, fakeRepo *testapi.FakeBuildpackRepository, - fakeBitsRepo *testapi.FakeBuildpackBitsRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("update-buildpack", args) - - cmd := NewUpdateBuildpack(ui, fakeRepo, fakeBitsRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/domain/create_domain.go b/src/cf/commands/domain/create_domain.go deleted file mode 100644 index 0cacf46c096..00000000000 --- a/src/cf/commands/domain/create_domain.go +++ /dev/null @@ -1,61 +0,0 @@ -package domain - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateDomain struct { - ui terminal.UI - config *configuration.Configuration - domainRepo api.DomainRepository - orgReq requirements.OrganizationRequirement -} - -func NewCreateDomain(ui terminal.UI, config *configuration.Configuration, domainRepo api.DomainRepository) (cmd *CreateDomain) { - cmd = new(CreateDomain) - cmd.ui = ui - cmd.config = config - cmd.domainRepo = domainRepo - return -} - -func (cmd *CreateDomain) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-domain") - return - } - - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.orgReq, - } - return -} - -func (cmd *CreateDomain) Run(c *cli.Context) { - domainName := c.Args()[1] - owningOrg := cmd.orgReq.GetOrganization() - - cmd.ui.Say("Creating domain %s for org %s as %s...", - terminal.EntityNameColor(domainName), - terminal.EntityNameColor(owningOrg.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - _, apiResponse := cmd.domainRepo.Create(domainName, owningOrg.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("TIP: Use '%s map-domain' to assign it to a space", cf.Name()) -} diff --git a/src/cf/commands/domain/create_domain_test.go b/src/cf/commands/domain/create_domain_test.go deleted file mode 100644 index 82c4b5ed598..00000000000 --- a/src/cf/commands/domain/create_domain_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package domain_test - -import ( - "cf" - "cf/commands/domain" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateDomainRequirements(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - callCreateDomain(t, []string{"my-org", "example.com"}, reqFactory, domainRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.OrganizationName, "my-org") - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - - callCreateDomain(t, []string{"my-org", "example.com"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateDomainFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - domainRepo := &testapi.FakeDomainRepository{} - ui := callCreateDomain(t, []string{""}, reqFactory, domainRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateDomain(t, []string{"org1"}, reqFactory, domainRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateDomain(t, []string{"org1", "example.com"}, reqFactory, domainRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestCreateDomain(t *testing.T) { - org := cf.Organization{} - org.Name = "myOrg" - org.Guid = "myOrg-guid" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Organization: org} - domainRepo := &testapi.FakeDomainRepository{} - fakeUI := callCreateDomain(t, []string{"myOrg", "example.com"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.CreateDomainName, "example.com") - assert.Equal(t, domainRepo.CreateDomainOwningOrgGuid, "myOrg-guid") - assert.Contains(t, fakeUI.Outputs[0], "Creating domain") - assert.Contains(t, fakeUI.Outputs[0], "example.com") - assert.Contains(t, fakeUI.Outputs[0], "myOrg") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func callCreateDomain(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-domain", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - config := &configuration.Configuration{ - AccessToken: token, - } - - cmd := domain.NewCreateDomain(fakeUI, config, domainRepo) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/domain/delete_domain.go b/src/cf/commands/domain/delete_domain.go deleted file mode 100644 index 0ef671343ab..00000000000 --- a/src/cf/commands/domain/delete_domain.go +++ /dev/null @@ -1,85 +0,0 @@ -package domain - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteDomain struct { - ui terminal.UI - config *configuration.Configuration - orgReq requirements.TargetedOrgRequirement - domainRepo api.DomainRepository -} - -func NewDeleteDomain(ui terminal.UI, config *configuration.Configuration, repo api.DomainRepository) (cmd *DeleteDomain) { - cmd = new(DeleteDomain) - cmd.ui = ui - cmd.config = config - cmd.domainRepo = repo - return -} - -func (cmd *DeleteDomain) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-domain") - return - } - - loginReq := reqFactory.NewLoginRequirement() - cmd.orgReq = reqFactory.NewTargetedOrgRequirement() - - reqs = []requirements.Requirement{ - loginReq, - cmd.orgReq, - } - - return -} - -func (cmd *DeleteDomain) Run(c *cli.Context) { - domainName := c.Args()[0] - force := c.Bool("f") - - cmd.ui.Say("Deleting domain %s as %s...", - terminal.EntityNameColor(domainName), - terminal.EntityNameColor(cmd.config.Username()), - ) - - domain, apiResponse := cmd.domainRepo.FindByNameInOrg(domainName, cmd.orgReq.GetOrganizationFields().Guid) - if apiResponse.IsError() { - cmd.ui.Failed("Error finding domain %s\n%s", domainName, apiResponse.Message) - return - } - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn(apiResponse.Message) - return - } - - if !force { - var answer bool - if domain.Shared { - answer = cmd.ui.Confirm("This domain is shared across all orgs.\nDeleting it will remove all associated routes, and will make any app with this domain unreachable.\nAre you sure you want to delete the domain %s? ", domainName) - } else { - answer = cmd.ui.Confirm("Are you sure you want to delete the domain %s and all of its associations?", domainName) - } - - if !answer { - return - } - } - - apiResponse = cmd.domainRepo.Delete(domain.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error deleting domain %s\n%s", domainName, apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/domain/delete_domain_test.go b/src/cf/commands/domain/delete_domain_test.go deleted file mode 100644 index 9d1ac1acff7..00000000000 --- a/src/cf/commands/domain/delete_domain_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package domain_test - -import ( - "cf" - "cf/commands/domain" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestGetRequirements(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteDomainSuccess(t *testing.T) { - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "foo-guid") - - assert.Contains(t, ui.Prompts[0], "delete") - assert.Contains(t, ui.Prompts[0], "foo.com") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteDomainNoConfirmation(t *testing.T) { - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"no"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "") - - assert.Contains(t, ui.Prompts[0], "delete") - assert.Contains(t, ui.Prompts[0], "foo.com") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - - assert.Equal(t, len(ui.Outputs), 1) -} - -func TestDeleteDomainNotFound(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgApiResponse: net.NewNotFoundApiResponse("%s %s not found", "Domain", "foo.com"), - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "foo.com") - assert.Contains(t, ui.Outputs[2], "not found") -} - -func TestDeleteDomainFindError(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgApiResponse: net.NewApiResponseWithMessage("failed badly"), - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "foo.com") - assert.Contains(t, ui.Outputs[2], "failed badly") -} - -func TestDeleteDomainDeleteError(t *testing.T) { - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - DeleteApiResponse: net.NewApiResponseWithMessage("failed badly"), - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "foo-guid") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "foo.com") - assert.Contains(t, ui.Outputs[2], "failed badly") -} - -func TestDeleteDomainDeleteSharedHasSharedConfirmation(t *testing.T) { - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domain.Shared = true - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - ui := callDeleteDomain(t, []string{"foo.com"}, []string{"y"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "foo-guid") - - assert.Contains(t, ui.Prompts[0], "shared") - assert.Contains(t, ui.Prompts[0], "foo.com") - - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteDomainForceFlagSkipsConfirmation(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domain.Shared = true - domainRepo := &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - } - ui := callDeleteDomain(t, []string{"-f", "foo.com"}, []string{}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.DeleteDomainGuid, "foo-guid") - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callDeleteDomain(t *testing.T, args []string, inputs []string, reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) (ui *testterm.FakeUI) { - ctxt := testcmd.NewContext("delete-domain", args) - ui = &testterm.FakeUI{ - Inputs: inputs, - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := domain.NewDeleteDomain(ui, config, domainRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/domain/domain_mapper.go b/src/cf/commands/domain/domain_mapper.go deleted file mode 100644 index 7ff09387f5e..00000000000 --- a/src/cf/commands/domain/domain_mapper.go +++ /dev/null @@ -1,103 +0,0 @@ -package domain - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DomainMapper struct { - ui terminal.UI - config *configuration.Configuration - domainRepo api.DomainRepository - spaceReq requirements.SpaceRequirement - orgReq requirements.TargetedOrgRequirement - bind bool -} - -func NewDomainMapper(ui terminal.UI, config *configuration.Configuration, domainRepo api.DomainRepository, bind bool) (cmd *DomainMapper) { - cmd = new(DomainMapper) - cmd.ui = ui - cmd.config = config - cmd.domainRepo = domainRepo - cmd.bind = bind - return -} - -func (cmd *DomainMapper) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - if cmd.bind { - cmd.ui.FailWithUsage(c, "map-domain") - } else { - cmd.ui.FailWithUsage(c, "unmap-domain") - } - return - } - - spaceName := c.Args()[0] - cmd.spaceReq = reqFactory.NewSpaceRequirement(spaceName) - - loginReq := reqFactory.NewLoginRequirement() - cmd.orgReq = reqFactory.NewTargetedOrgRequirement() - - reqs = []requirements.Requirement{ - loginReq, - cmd.orgReq, - cmd.spaceReq, - } - - return -} - -func (cmd *DomainMapper) Run(c *cli.Context) { - var ( - apiResponse net.ApiResponse - domain cf.Domain - ) - - domainName := c.Args()[1] - space := cmd.spaceReq.GetSpace() - org := cmd.orgReq.GetOrganizationFields() - - if cmd.bind { - cmd.ui.Say("Mapping domain %s to org %s / space %s as %s...", - terminal.EntityNameColor(domainName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - } else { - cmd.ui.Say("Unmapping domain %s from org %s / space %s as %s...", - terminal.EntityNameColor(domainName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - } - - domain, apiResponse = cmd.domainRepo.FindByNameInOrg(domainName, org.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error finding domain %s\n%s", terminal.EntityNameColor(domainName), apiResponse.Message) - return - } - - if cmd.bind { - apiResponse = cmd.domainRepo.Map(domain.Guid, space.Guid) - } else { - apiResponse = cmd.domainRepo.Unmap(domain.Guid, space.Guid) - } - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - return -} diff --git a/src/cf/commands/domain/domain_mapper_test.go b/src/cf/commands/domain/domain_mapper_test.go deleted file mode 100644 index ce1d89faae6..00000000000 --- a/src/cf/commands/domain/domain_mapper_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package domain_test - -import ( - "cf" - "cf/commands/domain" - "cf/configuration" - "cf/net" - "errors" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestMapDomainRequirements(t *testing.T) { - reqFactory, domainRepo := getDomainMapperDeps() - callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - reqFactory.TargetedOrgSuccess = false - callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = false - reqFactory.TargetedOrgSuccess = true - callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - reqFactory.TargetedOrgSuccess = true - callDomainMapper(t, true, []string{}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestMapDomainSuccess(t *testing.T) { - reqFactory, domainRepo := getDomainMapperDeps() - ui := callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.MapDomainGuid, "foo-guid") - assert.Equal(t, domainRepo.MapSpaceGuid, "my-space-guid") - assert.Contains(t, ui.Outputs[0], "Mapping domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestMapDomainDomainNotFound(t *testing.T) { - reqFactory, domainRepo := getDomainMapperDeps() - domainRepo.FindByNameInOrgApiResponse = net.NewNotFoundApiResponse("Domain foo.com not found") - ui := callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "Mapping domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "foo.com") -} - -func TestMapDomainMappingFails(t *testing.T) { - reqFactory, domainRepo := getDomainMapperDeps() - domainRepo.MapApiResponse = net.NewApiResponseWithError("Did not work %s", errors.New("bummer")) - - ui := callDomainMapper(t, true, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "Mapping domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "Did not work") - assert.Contains(t, ui.Outputs[2], "bummer") -} - -func TestUnmapDomainSuccess(t *testing.T) { - reqFactory, domainRepo := getDomainMapperDeps() - ui := callDomainMapper(t, false, []string{"my-space", "foo.com"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.UnmapDomainGuid, "foo-guid") - assert.Equal(t, domainRepo.UnmapSpaceGuid, "my-space-guid") - assert.Contains(t, ui.Outputs[0], "Unmapping domain") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func getDomainMapperDeps() (reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) { - domain := cf.Domain{} - domain.Name = "foo.com" - domain.Guid = "foo-guid" - domainRepo = &testapi.FakeDomainRepository{ - FindByNameInOrgDomain: domain, - } - - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - - reqFactory = &testreq.FakeReqFactory{ - LoginSuccess: true, - TargetedOrgSuccess: true, - Organization: org, - Space: space, - } - return -} - -func callDomainMapper(t *testing.T, shouldMap bool, args []string, reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) (ui *testterm.FakeUI) { - cmdName := "map-domain" - if !shouldMap { - cmdName = "unmap-domain" - } - - ctxt := testcmd.NewContext(cmdName, args) - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := domain.NewDomainMapper(ui, config, domainRepo, shouldMap) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/domain/list_domains.go b/src/cf/commands/domain/list_domains.go deleted file mode 100644 index 08c2ca1a7cc..00000000000 --- a/src/cf/commands/domain/list_domains.go +++ /dev/null @@ -1,92 +0,0 @@ -package domain - -import ( - "cf/api" - "cf/configuration" - "cf/formatters" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" - "strings" -) - -type ListDomains struct { - ui terminal.UI - config *configuration.Configuration - orgReq requirements.TargetedOrgRequirement - domainRepo api.DomainRepository -} - -func NewListDomains(ui terminal.UI, config *configuration.Configuration, domainRepo api.DomainRepository) (cmd *ListDomains) { - cmd = new(ListDomains) - cmd.ui = ui - cmd.config = config - cmd.domainRepo = domainRepo - return -} - -func (cmd *ListDomains) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) > 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "domains") - return - } - - cmd.orgReq = reqFactory.NewTargetedOrgRequirement() - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.orgReq, - } - return -} - -func (cmd *ListDomains) Run(c *cli.Context) { - org := cmd.orgReq.GetOrganizationFields() - - cmd.ui.Say("Getting domains in org %s as %s...", - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - stopChan := make(chan bool) - defer close(stopChan) - - domainsChan, statusChan := cmd.domainRepo.ListDomainsForOrg(org.Guid, stopChan) - - table := cmd.ui.Table([]string{"name", "status", "spaces"}) - noDomains := true - - for domains := range domainsChan { - rows := [][]string{} - for _, domain := range domains { - - var status string - if domain.Shared { - status = "shared" - } else if len(domain.Spaces) == 0 { - status = "reserved" - } else { - status = "owned" - } - - rows = append(rows, []string{ - domain.Name, - status, - strings.Join(formatters.MapStr(domain.Spaces), ", "), - }) - } - table.Print(rows) - noDomains = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching domains.\n%s", apiStatus.Message) - return - } - - if noDomains { - cmd.ui.Say("No domains found") - } -} diff --git a/src/cf/commands/domain/list_domains_test.go b/src/cf/commands/domain/list_domains_test.go deleted file mode 100644 index 8e27b4b12dd..00000000000 --- a/src/cf/commands/domain/list_domains_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package domain_test - -import ( - "cf" - "cf/commands/domain" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListDomainsRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - domainRepo := &testapi.FakeDomainRepository{} - - callListDomains(t, []string{}, reqFactory, domainRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callListDomains(t, []string{}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callListDomains(t, []string{}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListDomainsFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - domainRepo := &testapi.FakeDomainRepository{} - - ui := callListDomains(t, []string{"foo"}, reqFactory, domainRepo) - assert.True(t, ui.FailedWithUsage) -} - -func TestListDomains(t *testing.T) { - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - orgFields.Guid = "my-org-guid" - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true, OrganizationFields: orgFields} - domain1 := cf.Domain{} - domain1.Shared = true - domain1.Name = "Domain1" - - domain2 := cf.Domain{} - domain2.Shared = false - domain2.Name = "Domain2" - - space1 := cf.SpaceFields{} - space1.Name = "my-space" - - space2 := cf.SpaceFields{} - space2.Name = "my-space-2" - - domain2.Spaces = []cf.SpaceFields{space1, space2} - - domain3 := cf.Domain{} - domain3.Shared = false - domain3.Name = "Domain3" - - domainRepo := &testapi.FakeDomainRepository{ - ListDomainsForOrgDomains: []cf.Domain{domain1, domain2, domain3}, - } - fakeUI := callListDomains(t, []string{}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.ListDomainsForOrgDomainsGuid, "my-org-guid") - - assert.Contains(t, fakeUI.Outputs[0], "Getting domains in org") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - - assert.Contains(t, fakeUI.Outputs[2], "Domain1") - assert.Contains(t, fakeUI.Outputs[2], "shared") - - assert.Contains(t, fakeUI.Outputs[3], "Domain2") - assert.Contains(t, fakeUI.Outputs[3], "owned") - assert.Contains(t, fakeUI.Outputs[3], "my-space, my-space-2") - - assert.Contains(t, fakeUI.Outputs[4], "Domain3") - assert.Contains(t, fakeUI.Outputs[4], "reserved") -} - -func callListDomains(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("domains", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := domain.NewListDomains(fakeUI, config, domainRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/domain/share_domain.go b/src/cf/commands/domain/share_domain.go deleted file mode 100644 index 64f6ce1c593..00000000000 --- a/src/cf/commands/domain/share_domain.go +++ /dev/null @@ -1,55 +0,0 @@ -package domain - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type ShareDomain struct { - ui terminal.UI - config *configuration.Configuration - domainRepo api.DomainRepository - orgReq requirements.OrganizationRequirement -} - -func NewShareDomain(ui terminal.UI, config *configuration.Configuration, domainRepo api.DomainRepository) (cmd *ShareDomain) { - cmd = new(ShareDomain) - cmd.ui = ui - cmd.config = config - cmd.domainRepo = domainRepo - return -} - -func (cmd *ShareDomain) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "share-domain") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd *ShareDomain) Run(c *cli.Context) { - domainName := c.Args()[0] - - cmd.ui.Say("Sharing domain %s as %s...", - terminal.EntityNameColor(domainName), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.domainRepo.CreateSharedDomain(domainName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/domain/share_domain_test.go b/src/cf/commands/domain/share_domain_test.go deleted file mode 100644 index b0cc6ea4831..00000000000 --- a/src/cf/commands/domain/share_domain_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package domain_test - -import ( - . "cf/commands/domain" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestShareDomainRequirements(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callShareDomain(t, []string{"example.com"}, reqFactory, domainRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callShareDomain(t, []string{"example.com"}, reqFactory, domainRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestShareDomainFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - domainRepo := &testapi.FakeDomainRepository{} - ui := callShareDomain(t, []string{}, reqFactory, domainRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callShareDomain(t, []string{"example.com"}, reqFactory, domainRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestShareDomain(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - domainRepo := &testapi.FakeDomainRepository{} - fakeUI := callShareDomain(t, []string{"example.com"}, reqFactory, domainRepo) - - assert.Equal(t, domainRepo.CreateSharedDomainName, "example.com") - assert.Contains(t, fakeUI.Outputs[0], "Sharing domain") - assert.Contains(t, fakeUI.Outputs[0], "example.com") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func callShareDomain(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, domainRepo *testapi.FakeDomainRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("share-domain", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - config := &configuration.Configuration{ - AccessToken: token, - } - - cmd := NewShareDomain(fakeUI, config, domainRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/factory.go b/src/cf/commands/factory.go deleted file mode 100644 index ee2666c51b7..00000000000 --- a/src/cf/commands/factory.go +++ /dev/null @@ -1,124 +0,0 @@ -package commands - -import ( - "cf/api" - "cf/commands/application" - "cf/commands/buildpack" - "cf/commands/domain" - "cf/commands/organization" - "cf/commands/route" - "cf/commands/service" - "cf/commands/serviceauthtoken" - "cf/commands/servicebroker" - "cf/commands/space" - "cf/commands/user" - "cf/configuration" - "cf/terminal" - "errors" -) - -type Factory interface { - GetByCmdName(cmdName string) (cmd Command, err error) -} - -type ConcreteFactory struct { - cmdsByName map[string]Command -} - -func NewFactory(ui terminal.UI, config *configuration.Configuration, configRepo configuration.ConfigurationRepository, repoLocator api.RepositoryLocator) (factory ConcreteFactory) { - factory.cmdsByName = make(map[string]Command) - - factory.cmdsByName["api"] = NewApi(ui, config, repoLocator.GetEndpointRepository()) - factory.cmdsByName["app"] = application.NewShowApp(ui, config, repoLocator.GetAppSummaryRepository(), repoLocator.GetAppInstancesRepository()) - factory.cmdsByName["apps"] = application.NewListApps(ui, config, repoLocator.GetAppSummaryRepository()) - factory.cmdsByName["auth"] = NewAuthenticate(ui, configRepo, repoLocator.GetAuthenticationRepository()) - factory.cmdsByName["bind-service"] = service.NewBindService(ui, config, repoLocator.GetServiceBindingRepository()) - factory.cmdsByName["buildpacks"] = buildpack.NewListBuildpacks(ui, repoLocator.GetBuildpackRepository()) - factory.cmdsByName["create-buildpack"] = buildpack.NewCreateBuildpack(ui, repoLocator.GetBuildpackRepository(), repoLocator.GetBuildpackBitsRepository()) - factory.cmdsByName["create-domain"] = domain.NewCreateDomain(ui, config, repoLocator.GetDomainRepository()) - factory.cmdsByName["create-org"] = organization.NewCreateOrg(ui, config, repoLocator.GetOrganizationRepository()) - factory.cmdsByName["create-service"] = service.NewCreateService(ui, config, repoLocator.GetServiceRepository()) - factory.cmdsByName["create-service-auth-token"] = serviceauthtoken.NewCreateServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) - factory.cmdsByName["create-service-broker"] = servicebroker.NewCreateServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) - factory.cmdsByName["create-space"] = space.NewCreateSpace(ui, config, repoLocator.GetSpaceRepository()) - factory.cmdsByName["create-user"] = user.NewCreateUser(ui, config, repoLocator.GetUserRepository()) - factory.cmdsByName["create-user-provided-service"] = service.NewCreateUserProvidedService(ui, config, repoLocator.GetUserProvidedServiceInstanceRepository()) - factory.cmdsByName["delete"] = application.NewDeleteApp(ui, config, repoLocator.GetApplicationRepository()) - factory.cmdsByName["delete-buildpack"] = buildpack.NewDeleteBuildpack(ui, repoLocator.GetBuildpackRepository()) - factory.cmdsByName["delete-domain"] = domain.NewDeleteDomain(ui, config, repoLocator.GetDomainRepository()) - factory.cmdsByName["delete-org"] = organization.NewDeleteOrg(ui, config, repoLocator.GetOrganizationRepository(), configRepo) - factory.cmdsByName["delete-route"] = route.NewDeleteRoute(ui, config, repoLocator.GetRouteRepository()) - factory.cmdsByName["delete-service"] = service.NewDeleteService(ui, config, repoLocator.GetServiceRepository()) - factory.cmdsByName["delete-service-auth-token"] = serviceauthtoken.NewDeleteServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) - factory.cmdsByName["delete-service-broker"] = servicebroker.NewDeleteServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) - factory.cmdsByName["delete-space"] = space.NewDeleteSpace(ui, config, repoLocator.GetSpaceRepository(), configRepo) - factory.cmdsByName["delete-user"] = user.NewDeleteUser(ui, config, repoLocator.GetUserRepository()) - factory.cmdsByName["domains"] = domain.NewListDomains(ui, config, repoLocator.GetDomainRepository()) - factory.cmdsByName["env"] = application.NewEnv(ui, config) - factory.cmdsByName["events"] = application.NewEvents(ui, config, repoLocator.GetAppEventsRepository()) - factory.cmdsByName["files"] = application.NewFiles(ui, config, repoLocator.GetAppFilesRepository()) - factory.cmdsByName["login"] = NewLogin(ui, configRepo, repoLocator.GetAuthenticationRepository(), repoLocator.GetEndpointRepository(), repoLocator.GetOrganizationRepository(), repoLocator.GetSpaceRepository()) - factory.cmdsByName["logout"] = NewLogout(ui, configRepo) - factory.cmdsByName["logs"] = application.NewLogs(ui, config, repoLocator.GetLogsRepository()) - factory.cmdsByName["marketplace"] = service.NewMarketplaceServices(ui, config, repoLocator.GetServiceRepository()) - factory.cmdsByName["map-domain"] = domain.NewDomainMapper(ui, config, repoLocator.GetDomainRepository(), true) - factory.cmdsByName["org"] = organization.NewShowOrg(ui, config) - factory.cmdsByName["org-users"] = user.NewOrgUsers(ui, config, repoLocator.GetUserRepository()) - factory.cmdsByName["orgs"] = organization.NewListOrgs(ui, config, repoLocator.GetOrganizationRepository()) - factory.cmdsByName["passwd"] = NewPassword(ui, repoLocator.GetPasswordRepository(), configRepo) - factory.cmdsByName["quotas"] = organization.NewListQuotas(ui, config, repoLocator.GetQuotaRepository()) - factory.cmdsByName["rename"] = application.NewRenameApp(ui, config, repoLocator.GetApplicationRepository()) - factory.cmdsByName["rename-org"] = organization.NewRenameOrg(ui, config, repoLocator.GetOrganizationRepository()) - factory.cmdsByName["rename-service"] = service.NewRenameService(ui, config, repoLocator.GetServiceRepository()) - factory.cmdsByName["rename-service-broker"] = servicebroker.NewRenameServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) - factory.cmdsByName["rename-space"] = space.NewRenameSpace(ui, config, repoLocator.GetSpaceRepository(), configRepo) - factory.cmdsByName["routes"] = route.NewListRoutes(ui, config, repoLocator.GetRouteRepository()) - factory.cmdsByName["service"] = service.NewShowService(ui) - factory.cmdsByName["service-auth-tokens"] = serviceauthtoken.NewListServiceAuthTokens(ui, config, repoLocator.GetServiceAuthTokenRepository()) - factory.cmdsByName["service-brokers"] = servicebroker.NewListServiceBrokers(ui, config, repoLocator.GetServiceBrokerRepository()) - factory.cmdsByName["services"] = service.NewListServices(ui, config, repoLocator.GetServiceSummaryRepository()) - factory.cmdsByName["set-env"] = application.NewSetEnv(ui, config, repoLocator.GetApplicationRepository()) - factory.cmdsByName["set-org-role"] = user.NewSetOrgRole(ui, config, repoLocator.GetUserRepository()) - factory.cmdsByName["set-quota"] = organization.NewSetQuota(ui, config, repoLocator.GetQuotaRepository()) - factory.cmdsByName["set-space-role"] = user.NewSetSpaceRole(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) - factory.cmdsByName["share-domain"] = domain.NewShareDomain(ui, config, repoLocator.GetDomainRepository()) - factory.cmdsByName["space"] = space.NewShowSpace(ui, config) - factory.cmdsByName["space-users"] = user.NewSpaceUsers(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) - factory.cmdsByName["spaces"] = space.NewListSpaces(ui, config, repoLocator.GetSpaceRepository()) - factory.cmdsByName["stacks"] = NewStacks(ui, config, repoLocator.GetStackRepository()) - factory.cmdsByName["target"] = NewTarget(ui, configRepo, repoLocator.GetOrganizationRepository(), repoLocator.GetSpaceRepository()) - factory.cmdsByName["unbind-service"] = service.NewUnbindService(ui, config, repoLocator.GetServiceBindingRepository()) - factory.cmdsByName["unmap-domain"] = domain.NewDomainMapper(ui, config, repoLocator.GetDomainRepository(), false) - factory.cmdsByName["unset-env"] = application.NewUnsetEnv(ui, config, repoLocator.GetApplicationRepository()) - factory.cmdsByName["unset-org-role"] = user.NewUnsetOrgRole(ui, config, repoLocator.GetUserRepository()) - factory.cmdsByName["unset-space-role"] = user.NewUnsetSpaceRole(ui, config, repoLocator.GetSpaceRepository(), repoLocator.GetUserRepository()) - factory.cmdsByName["update-buildpack"] = buildpack.NewUpdateBuildpack(ui, repoLocator.GetBuildpackRepository(), repoLocator.GetBuildpackBitsRepository()) - factory.cmdsByName["update-service-broker"] = servicebroker.NewUpdateServiceBroker(ui, config, repoLocator.GetServiceBrokerRepository()) - factory.cmdsByName["update-service-auth-token"] = serviceauthtoken.NewUpdateServiceAuthToken(ui, config, repoLocator.GetServiceAuthTokenRepository()) - factory.cmdsByName["update-user-provided-service"] = service.NewUpdateUserProvidedService(ui, config, repoLocator.GetUserProvidedServiceInstanceRepository()) - - createRoute := route.NewCreateRoute(ui, config, repoLocator.GetRouteRepository()) - factory.cmdsByName["create-route"] = createRoute - factory.cmdsByName["map-route"] = route.NewRouteMapper(ui, config, repoLocator.GetRouteRepository(), createRoute, true) - factory.cmdsByName["unmap-route"] = route.NewRouteMapper(ui, config, repoLocator.GetRouteRepository(), createRoute, false) - - start := application.NewStart(ui, config, repoLocator.GetApplicationRepository(), repoLocator.GetAppInstancesRepository(), repoLocator.GetLogsRepository()) - stop := application.NewStop(ui, config, repoLocator.GetApplicationRepository()) - restart := application.NewRestart(ui, start, stop) - - factory.cmdsByName["start"] = start - factory.cmdsByName["stop"] = stop - factory.cmdsByName["restart"] = restart - factory.cmdsByName["push"] = application.NewPush(ui, config, start, stop, repoLocator.GetApplicationRepository(), repoLocator.GetDomainRepository(), repoLocator.GetRouteRepository(), repoLocator.GetStackRepository(), repoLocator.GetApplicationBitsRepository()) - factory.cmdsByName["scale"] = application.NewScale(ui, config, restart, repoLocator.GetApplicationRepository()) - - return -} - -func (f ConcreteFactory) GetByCmdName(cmdName string) (cmd Command, err error) { - cmd, found := f.cmdsByName[cmdName] - if !found { - err = errors.New("Command not found") - } - return -} diff --git a/src/cf/commands/login.go b/src/cf/commands/login.go deleted file mode 100644 index 9156582c9f6..00000000000 --- a/src/cf/commands/login.go +++ /dev/null @@ -1,318 +0,0 @@ -package commands - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strconv" - "strings" -) - -const maxLoginTries = 3 -const maxChoices = 50 - -type Login struct { - ui terminal.UI - config *configuration.Configuration - configRepo configuration.ConfigurationRepository - authenticator api.AuthenticationRepository - endpointRepo api.EndpointRepository - orgRepo api.OrganizationRepository - spaceRepo api.SpaceRepository -} - -func NewLogin(ui terminal.UI, - configRepo configuration.ConfigurationRepository, - authenticator api.AuthenticationRepository, - endpointRepo api.EndpointRepository, - orgRepo api.OrganizationRepository, - spaceRepo api.SpaceRepository) (cmd Login) { - - cmd.ui = ui - cmd.configRepo = configRepo - cmd.config, _ = configRepo.Get() - cmd.authenticator = authenticator - cmd.endpointRepo = endpointRepo - cmd.orgRepo = orgRepo - cmd.spaceRepo = spaceRepo - - return -} - -func (cmd Login) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd Login) Run(c *cli.Context) { - oldUserName := cmd.config.Username() - - apiResponse := cmd.setApi(c) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Invalid API endpoint.\n%s", apiResponse.Message) - return - } - - apiResponse = cmd.authenticate(c) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Unable to authenticate.") - return - } - - userChanged := (cmd.config.Username() != oldUserName && oldUserName != "") - - apiResponse = cmd.setOrganization(c, userChanged) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - apiResponse = cmd.setSpace(c, userChanged) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.ShowConfiguration(cmd.config) - return -} - -func (cmd Login) setApi(c *cli.Context) (apiResponse net.ApiResponse) { - api := c.String("a") - if api == "" { - api = cmd.config.Target - } - - if api == "" { - api = cmd.ui.Ask("API endpoint%s", terminal.PromptColor(">")) - } else { - cmd.ui.Say("API endpoint: %s", terminal.EntityNameColor(api)) - } - - endpoint, apiResponse := cmd.endpointRepo.UpdateEndpoint(api) - - if !strings.HasPrefix(endpoint, "https://") { - cmd.ui.Say(terminal.WarningColor("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended\n")) - } - - return -} - -func (cmd Login) authenticate(c *cli.Context) (apiResponse net.ApiResponse) { - username := c.String("u") - if username == "" { - username = cmd.ui.Ask("Username%s", terminal.PromptColor(">")) - } - - password := c.String("p") - - for i := 0; i < maxLoginTries; i++ { - if password == "" || i > 0 { - password = cmd.ui.AskForPassword("Password%s", terminal.PromptColor(">")) - } - - cmd.ui.Say("Authenticating...") - - apiResponse = cmd.authenticator.Authenticate(username, password) - if apiResponse.IsSuccessful() { - cmd.ui.Ok() - cmd.ui.Say("") - break - } - - cmd.ui.Say(apiResponse.Message) - } - return -} - -func (cmd Login) setOrganization(c *cli.Context, userChanged bool) (apiResponse net.ApiResponse) { - orgName := c.String("o") - - if orgName == "" { - // If the user is changing, clear out the org - if userChanged { - err := cmd.configRepo.SetOrganization(cf.OrganizationFields{}) - if err != nil { - apiResponse = net.NewApiResponseWithError("%s", err) - return - } - } - - // Reuse org in config - if cmd.config.HasOrganization() && !userChanged { - return - } - - stopChan := make(chan bool) - defer close(stopChan) - - orgsChan, statusChan := cmd.orgRepo.ListOrgs(stopChan) - - availableOrgs := []cf.Organization{} - - for orgs := range orgsChan { - availableOrgs = append(availableOrgs, orgs...) - if len(availableOrgs) > maxChoices { - stopChan <- true - break - } - } - - apiResponse = <-statusChan - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error finding avilable orgs\n%s", apiResponse.Message) - return - } - - // Target only org if possible - if len(availableOrgs) == 1 { - return cmd.targetOrganization(availableOrgs[0]) - } - - orgName = cmd.promptForOrgName(availableOrgs) - } - - // Find org - org, apiResponse := cmd.orgRepo.FindByName(orgName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error finding org %s\n%s", terminal.EntityNameColor(orgName), apiResponse.Message) - return - } - - return cmd.targetOrganization(org) -} - -func (cmd Login) promptForOrgName(orgs []cf.Organization) string { - orgNames := []string{} - for _, org := range orgs { - orgNames = append(orgNames, org.Name) - } - - return cmd.promptForName(orgNames, "Select an org:", "Org") -} - -func (cmd Login) targetOrganization(org cf.Organization) (apiResponse net.ApiResponse) { - err := cmd.configRepo.SetOrganization(org.OrganizationFields) - if err != nil { - apiResponse = net.NewApiResponseWithMessage("Error setting org %s in config file\n%s", - terminal.EntityNameColor(org.Name), - err.Error(), - ) - return - } - - cmd.ui.Say("Targeted org %s\n", terminal.EntityNameColor(org.Name)) - return -} - -func (cmd Login) setSpace(c *cli.Context, userChanged bool) (apiResponse net.ApiResponse) { - spaceName := c.String("s") - - if spaceName == "" { - // If user is changing, clear the space - if userChanged { - err := cmd.configRepo.SetSpace(cf.SpaceFields{}) - if err != nil { - apiResponse = net.NewApiResponseWithError("%s", err) - return - } - } - // Reuse space in config - if cmd.config.HasSpace() && !userChanged { - return - } - - stopChan := make(chan bool) - defer close(stopChan) - - spacesChan, statusChan := cmd.spaceRepo.ListSpaces(stopChan) - - var availableSpaces []cf.Space - - for spaces := range spacesChan { - availableSpaces = append(availableSpaces, spaces...) - if len(availableSpaces) > maxChoices { - stopChan <- true - break - } - } - - apiResponse = <-statusChan - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error finding avilable spaces\n%s", apiResponse.Message) - return - } - - // Target only space if possible - if len(availableSpaces) == 1 { - return cmd.targetSpace(availableSpaces[0]) - } - - spaceName = cmd.promptForSpaceName(availableSpaces) - } - - // Find space - space, apiResponse := cmd.spaceRepo.FindByName(spaceName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error finding space %s\n%s", terminal.EntityNameColor(spaceName), apiResponse.Message) - return - } - - return cmd.targetSpace(space) -} - -func (cmd Login) promptForSpaceName(spaces []cf.Space) string { - spaceNames := []string{} - for _, space := range spaces { - spaceNames = append(spaceNames, space.Name) - } - - return cmd.promptForName(spaceNames, "Select a space:", "Space") -} - -func (cmd Login) targetSpace(space cf.Space) (apiResponse net.ApiResponse) { - err := cmd.configRepo.SetSpace(space.SpaceFields) - if err != nil { - apiResponse = net.NewApiResponseWithMessage("Error setting space %s in config file\n%s", - terminal.EntityNameColor(space.Name), - err.Error(), - ) - return - } - - cmd.ui.Say("Targeted space %s\n", terminal.EntityNameColor(space.Name)) - return -} - -func (cmd Login) promptForName(names []string, listPrompt, itemPrompt string) string { - nameIndex := 0 - var nameString string - for nameIndex < 1 || nameIndex > len(names) { - var err error - - // list header - cmd.ui.Say(listPrompt) - - // only display list if it is shorter than maxChoices - if len(names) < maxChoices { - for i, name := range names { - cmd.ui.Say("%d. %s", i+1, name) - } - } else { - cmd.ui.Say("There are too many options to display, please type in the name.") - } - - nameString = cmd.ui.Ask("%s%s", itemPrompt, terminal.PromptColor(">")) - nameIndex, err = strconv.Atoi(nameString) - - if err != nil { - nameIndex = 1 - return nameString - } - } - - return names[nameIndex-1] -} diff --git a/src/cf/commands/login_test.go b/src/cf/commands/login_test.go deleted file mode 100644 index e26ff1c3e7b..00000000000 --- a/src/cf/commands/login_test.go +++ /dev/null @@ -1,483 +0,0 @@ -package commands_test - -import ( - "cf" - . "cf/commands" - "cf/configuration" - "github.com/stretchr/testify/assert" - "strconv" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testterm "testhelpers/terminal" - "testing" -) - -type LoginTestContext struct { - Flags []string - Inputs []string - Config configuration.Configuration - - configRepo testconfig.FakeConfigRepository - ui *testterm.FakeUI - authRepo *testapi.FakeAuthenticationRepository - endpointRepo *testapi.FakeEndpointRepo - orgRepo *testapi.FakeOrgRepository - spaceRepo *testapi.FakeSpaceRepository -} - -func defaultBeforeBlock(*LoginTestContext) {} - -func callLogin(t *testing.T, c *LoginTestContext, beforeBlock func(*LoginTestContext)) { - - c.configRepo = testconfig.FakeConfigRepository{} - c.ui = &testterm.FakeUI{ - Inputs: c.Inputs, - } - c.authRepo = &testapi.FakeAuthenticationRepository{ - AccessToken: "my_access_token", - RefreshToken: "my_refresh_token", - ConfigRepo: c.configRepo, - } - c.endpointRepo = &testapi.FakeEndpointRepo{} - - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - c.orgRepo = &testapi.FakeOrgRepository{ - FindByNameOrganization: org, - } - - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - - c.spaceRepo = &testapi.FakeSpaceRepository{ - FindByNameSpace: space, - } - - c.configRepo.Delete() - config, _ := c.configRepo.Get() - config.Target = c.Config.Target - config.OrganizationFields = c.Config.OrganizationFields - config.SpaceFields = c.Config.SpaceFields - - beforeBlock(c) - - l := NewLogin(c.ui, c.configRepo, c.authRepo, c.endpointRepo, c.orgRepo, c.spaceRepo) - l.Run(testcmd.NewContext("login", c.Flags)) -} - -func TestSuccessfullyLoggingInWithNumericalPrompts(t *testing.T) { - OUT_OF_RANGE_CHOICE := "3" - c := LoginTestContext{ - Inputs: []string{"api.example.com", "user@example.com", "password", OUT_OF_RANGE_CHOICE, "2", OUT_OF_RANGE_CHOICE, "1"}, - } - - org1 := cf.Organization{} - org1.Guid = "some-org-guid" - org1.Name = "some-org" - - org2 := cf.Organization{} - org2.Guid = "my-org-guid" - org2.Name = "my-org" - - space1 := cf.Space{} - space1.Guid = "my-space-guid" - space1.Name = "my-space" - - space2 := cf.Space{} - space2.Guid = "some-space-guid" - space2.Name = "some-space" - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.Organizations = []cf.Organization{org1, org2} - c.spaceRepo.Spaces = []cf.Space{space1, space2} - }) - - savedConfig := testconfig.SavedConfiguration - - expectedOutputs := []string{ - "Select an org:", - "1. some-org", - "2. my-org", - "Select a space:", - "1. my-space", - "2. some-space", - } - testassert.SliceContains(t, c.ui.Outputs, expectedOutputs) - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.Equal(t, c.orgRepo.FindByNameName, "my-org") - assert.Equal(t, c.spaceRepo.FindByNameName, "my-space") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithStringPrompts(t *testing.T) { - c := LoginTestContext{ - Inputs: []string{"api.example.com", "user@example.com", "password", "my-org", "my-space"}, - } - - org1 := cf.Organization{} - org1.Guid = "some-org-guid" - org1.Name = "some-org" - - org2 := cf.Organization{} - org2.Guid = "my-org-guid" - org2.Name = "my-org" - - space1 := cf.Space{} - space1.Guid = "my-space-guid" - space1.Name = "my-space" - - space2 := cf.Space{} - space2.Guid = "some-space-guid" - space2.Name = "some-space" - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.Organizations = []cf.Organization{org1, org2} - c.spaceRepo.Spaces = []cf.Space{space1, space2} - }) - - savedConfig := testconfig.SavedConfiguration - - expectedOutputs := []string{ - "Select an org:", - "1. some-org", - "2. my-org", - "Select a space:", - "1. my-space", - "2. some-space", - } - - testassert.SliceContains(t, c.ui.Outputs, expectedOutputs) - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.Equal(t, c.orgRepo.FindByNameName, "my-org") - assert.Equal(t, c.spaceRepo.FindByNameName, "my-space") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestLoggingInWithTooManyOrgsDoesNotShowOrgList(t *testing.T) { - c := LoginTestContext{ - Inputs: []string{"api.example.com", "user@example.com", "password", "my-org-1", "my-space"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - for i := 0; i < 60; i++ { - id := strconv.Itoa(i) - org := cf.Organization{} - org.Guid = "my-org-guid-" + id - org.Name = "my-org-" + id - c.orgRepo.Organizations = append(c.orgRepo.Organizations, org) - } - - c.orgRepo.FindByNameOrganization = c.orgRepo.Organizations[1] - - space1 := cf.Space{} - space1.Guid = "my-space-guid" - space1.Name = "my-space" - - space2 := cf.Space{} - space2.Guid = "some-space-guid" - space2.Name = "some-space" - - c.spaceRepo.Spaces = []cf.Space{space1, space2} - }) - - savedConfig := testconfig.SavedConfiguration - - assert.True(t, len(c.ui.Outputs) < 50) - - assert.Equal(t, c.orgRepo.FindByNameName, "my-org-1") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid-1") -} - -func TestSuccessfullyLoggingInWithFlags(t *testing.T) { - c := LoginTestContext{ - Flags: []string{"-a", "api.example.com", "-u", "user@example.com", "-p", "password", "-o", "my-org", "-s", "my-space"}, - } - - callLogin(t, &c, defaultBeforeBlock) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithEndpointSetInConfig(t *testing.T) { - existingConfig := configuration.Configuration{ - Target: "http://api.example.com", - } - - c := LoginTestContext{ - Flags: []string{"-o", "my-org", "-s", "my-space"}, - Inputs: []string{"user@example.com", "password"}, - Config: existingConfig, - } - - callLogin(t, &c, defaultBeforeBlock) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "http://api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithOrgSetInConfig(t *testing.T) { - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - existingConfig := configuration.Configuration{OrganizationFields: org} - - c := LoginTestContext{ - Flags: []string{"-s", "my-space"}, - Inputs: []string{"http://api.example.com", "user@example.com", "password"}, - Config: existingConfig, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.FindByNameOrganization = cf.Organization{} - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "http://api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithOrgAndSpaceSetInConfig(t *testing.T) { - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - space.Name = "my-space" - - existingConfig := configuration.Configuration{ - OrganizationFields: org, - SpaceFields: space, - } - - c := LoginTestContext{ - Inputs: []string{"http://api.example.com", "user@example.com", "password"}, - Config: existingConfig, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.FindByNameOrganization = cf.Organization{} - c.spaceRepo.FindByNameInOrgSpace = cf.Space{} - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "http://api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithOnlyOneOrg(t *testing.T) { - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - c := LoginTestContext{ - Flags: []string{"-s", "my-space"}, - Inputs: []string{"http://api.example.com", "user@example.com", "password"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.FindByNameOrganization = cf.Organization{} - c.orgRepo.Organizations = []cf.Organization{org} - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "http://api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestSuccessfullyLoggingInWithOnlyOneSpace(t *testing.T) { - space := cf.Space{} - space.Guid = "my-space-guid" - space.Name = "my-space" - - c := LoginTestContext{ - Flags: []string{"-o", "my-org"}, - Inputs: []string{"http://api.example.com", "user@example.com", "password"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.spaceRepo.Spaces = []cf.Space{space} - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - assert.Equal(t, c.endpointRepo.UpdateEndpointEndpoint, "http://api.example.com") - assert.Equal(t, c.authRepo.Email, "user@example.com") - assert.Equal(t, c.authRepo.Password, "password") - - assert.True(t, c.ui.ShowConfigurationCalled) -} - -func TestUnsuccessfullyLoggingInWithAuthError(t *testing.T) { - c := LoginTestContext{ - Flags: []string{"-u", "user@example.com"}, - Inputs: []string{"api.example.com", "password", "password2", "password3"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.authRepo.AuthError = true - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Empty(t, savedConfig.OrganizationFields.Guid) - assert.Empty(t, savedConfig.SpaceFields.Guid) - assert.Empty(t, savedConfig.AccessToken) - assert.Empty(t, savedConfig.RefreshToken) - - failIndex := len(c.ui.Outputs) - 2 - assert.Equal(t, c.ui.Outputs[failIndex], "FAILED") - assert.Equal(t, len(c.ui.PasswordPrompts), 3) -} - -func TestUnsuccessfullyLoggingInWithUpdateEndpointError(t *testing.T) { - c := LoginTestContext{ - Inputs: []string{"api.example.com"}, - } - callLogin(t, &c, func(c *LoginTestContext) { - c.endpointRepo.UpdateEndpointError = true - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Empty(t, savedConfig.Target) - assert.Empty(t, savedConfig.OrganizationFields.Guid) - assert.Empty(t, savedConfig.SpaceFields.Guid) - assert.Empty(t, savedConfig.AccessToken) - assert.Empty(t, savedConfig.RefreshToken) - - failIndex := len(c.ui.Outputs) - 2 - assert.Equal(t, c.ui.Outputs[failIndex], "FAILED") -} - -func TestUnsuccessfullyLoggingInWithOrgFindByNameErr(t *testing.T) { - c := LoginTestContext{ - Flags: []string{"-u", "user@example.com", "-o", "my-org", "-s", "my-space"}, - Inputs: []string{"api.example.com", "user@example.com", "password"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.orgRepo.FindByNameErr = true - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Empty(t, savedConfig.OrganizationFields.Guid) - assert.Empty(t, savedConfig.SpaceFields.Guid) - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - failIndex := len(c.ui.Outputs) - 2 - assert.Equal(t, c.ui.Outputs[failIndex], "FAILED") -} - -func TestUnsuccessfullyLoggingInWithSpaceFindByNameErr(t *testing.T) { - c := LoginTestContext{ - Flags: []string{"-u", "user@example.com", "-o", "my-org", "-s", "my-space"}, - Inputs: []string{"api.example.com", "user@example.com", "password"}, - } - - callLogin(t, &c, func(c *LoginTestContext) { - c.spaceRepo.FindByNameErr = true - }) - - savedConfig := testconfig.SavedConfiguration - - assert.Equal(t, savedConfig.Target, "api.example.com") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-org-guid") - assert.Empty(t, savedConfig.SpaceFields.Guid) - assert.Equal(t, savedConfig.AccessToken, "my_access_token") - assert.Equal(t, savedConfig.RefreshToken, "my_refresh_token") - - failIndex := len(c.ui.Outputs) - 2 - assert.Equal(t, c.ui.Outputs[failIndex], "FAILED") -} diff --git a/src/cf/commands/logout.go b/src/cf/commands/logout.go deleted file mode 100644 index 1e3f05fcac8..00000000000 --- a/src/cf/commands/logout.go +++ /dev/null @@ -1,35 +0,0 @@ -package commands - -import ( - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type Logout struct { - ui terminal.UI - configRepo configuration.ConfigurationRepository -} - -func NewLogout(ui terminal.UI, configRepo configuration.ConfigurationRepository) (cmd Logout) { - cmd.ui = ui - cmd.configRepo = configRepo - return -} - -func (cmd Logout) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd Logout) Run(c *cli.Context) { - cmd.ui.Say("Logging out...") - err := cmd.configRepo.ClearSession() - - if err != nil { - cmd.ui.Failed(err.Error()) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/logout_test.go b/src/cf/commands/logout_test.go deleted file mode 100644 index 38c6b6fe7ff..00000000000 --- a/src/cf/commands/logout_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package commands_test - -import ( - "cf" - "cf/commands" - "github.com/stretchr/testify/assert" - testconfig "testhelpers/configuration" - testterm "testhelpers/terminal" - "testing" -) - -func TestLogoutClearsAccessTokenOrgAndSpace(t *testing.T) { - org := cf.OrganizationFields{} - org.Name = "MyOrg" - - space := cf.SpaceFields{} - space.Name = "MySpace" - - configRepo := &testconfig.FakeConfigRepository{} - config, _ := configRepo.Get() - config.AccessToken = "MyAccessToken" - config.OrganizationFields = org - config.SpaceFields = space - - ui := new(testterm.FakeUI) - - l := commands.NewLogout(ui, configRepo) - l.Run(nil) - - updatedConfig, err := configRepo.Get() - assert.NoError(t, err) - - assert.Empty(t, updatedConfig.AccessToken) - assert.Equal(t, updatedConfig.OrganizationFields, cf.OrganizationFields{}) - assert.Equal(t, updatedConfig.SpaceFields, cf.SpaceFields{}) -} diff --git a/src/cf/commands/organization/create_org.go b/src/cf/commands/organization/create_org.go deleted file mode 100644 index 2654ad71239..00000000000 --- a/src/cf/commands/organization/create_org.go +++ /dev/null @@ -1,60 +0,0 @@ -package organization - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateOrg struct { - ui terminal.UI - config *configuration.Configuration - orgRepo api.OrganizationRepository -} - -func NewCreateOrg(ui terminal.UI, config *configuration.Configuration, orgRepo api.OrganizationRepository) (cmd CreateOrg) { - cmd.ui = ui - cmd.config = config - cmd.orgRepo = orgRepo - return -} - -func (cmd CreateOrg) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-org") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd CreateOrg) Run(c *cli.Context) { - name := c.Args()[0] - - cmd.ui.Say("Creating org %s as %s...", - terminal.EntityNameColor(name), - terminal.EntityNameColor(cmd.config.Username()), - ) - apiResponse := cmd.orgRepo.Create(name) - if apiResponse.IsNotSuccessful() { - if apiResponse.ErrorCode == cf.ORG_EXISTS { - cmd.ui.Ok() - cmd.ui.Warn("Org %s already exists", name) - return - } - - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("\nTIP: Use '%s' to target new org", terminal.CommandColor(cf.Name()+" target -o "+name)) -} diff --git a/src/cf/commands/organization/create_org_test.go b/src/cf/commands/organization/create_org_test.go deleted file mode 100644 index b4167a730f7..00000000000 --- a/src/cf/commands/organization/create_org_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package organization_test - -import ( - "cf" - . "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateOrgFailsWithUsage(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - fakeUI := callCreateOrg(t, []string{}, reqFactory, orgRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callCreateOrg(t, []string{"my-org"}, reqFactory, orgRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestCreateOrgRequirements(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callCreateOrg(t, []string{"my-org"}, reqFactory, orgRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callCreateOrg(t, []string{"my-org"}, reqFactory, orgRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateOrg(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - fakeUI := callCreateOrg(t, []string{"my-org"}, reqFactory, orgRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating org") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Equal(t, orgRepo.CreateName, "my-org") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestCreateOrgWhenAlreadyExists(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{CreateOrgExists: true} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - fakeUI := callCreateOrg(t, []string{"my-org"}, reqFactory, orgRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating org") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-org") - assert.Contains(t, fakeUI.Outputs[2], "already exists") -} - -func callCreateOrg(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, orgRepo *testapi.FakeOrgRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-org", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - space := cf.SpaceFields{} - space.Name = "my-space" - - organization := cf.OrganizationFields{} - organization.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: organization, - AccessToken: token, - } - - cmd := NewCreateOrg(fakeUI, config, orgRepo) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/delete_org.go b/src/cf/commands/organization/delete_org.go deleted file mode 100644 index 65cf338f830..00000000000 --- a/src/cf/commands/organization/delete_org.go +++ /dev/null @@ -1,94 +0,0 @@ -package organization - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteOrg struct { - ui terminal.UI - config *configuration.Configuration - orgRepo api.OrganizationRepository - orgReq requirements.OrganizationRequirement - configRepo configuration.ConfigurationRepository -} - -func NewDeleteOrg(ui terminal.UI, config *configuration.Configuration, sR api.OrganizationRepository, cR configuration.ConfigurationRepository) (cmd *DeleteOrg) { - cmd = new(DeleteOrg) - cmd.ui = ui - cmd.config = config - cmd.orgRepo = sR - cmd.configRepo = cR - return -} - -func (cmd *DeleteOrg) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-org") - return - - } - return -} - -func (cmd *DeleteOrg) Run(c *cli.Context) { - orgName := c.Args()[0] - - force := c.Bool("f") - - if !force { - response := cmd.ui.Confirm( - "Really delete org %s and everything associated with it?%s", - terminal.EntityNameColor(orgName), - terminal.PromptColor(">"), - ) - - if !response { - return - } - } - - cmd.ui.Say("Deleting org %s as %s...", - terminal.EntityNameColor(orgName), - terminal.EntityNameColor(cmd.config.Username()), - ) - - org, apiResponse := cmd.orgRepo.FindByName(orgName) - - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Org %s does not exist.", orgName) - return - } - - apiResponse = cmd.orgRepo.Delete(org.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - config, err := cmd.configRepo.Get() - if err != nil { - cmd.ui.Failed("Couldn't reset your target. You should logout and log in again.") - return - } - - if org.Guid == config.OrganizationFields.Guid { - config.OrganizationFields = cf.OrganizationFields{} - config.SpaceFields = cf.SpaceFields{} - cmd.configRepo.Save() - } - - cmd.ui.Ok() - return -} diff --git a/src/cf/commands/organization/delete_org_test.go b/src/cf/commands/organization/delete_org_test.go deleted file mode 100644 index 46cc4adf8f8..00000000000 --- a/src/cf/commands/organization/delete_org_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package organization_test - -import ( - "cf" - . "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteOrgConfirmingWithY(t *testing.T) { - org := cf.Organization{} - org.Name = "org-to-delete" - org.Guid = "org-to-delete-guid" - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - - ui := deleteOrg(t, "y", []string{org.Name}, orgRepo) - - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Equal(t, orgRepo.FindByNameName, "org-to-delete") - assert.Equal(t, orgRepo.DeletedOrganizationGuid, "org-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteOrgConfirmingWithYes(t *testing.T) { - org := cf.Organization{} - org.Name = "org-to-delete" - org.Guid = "org-to-delete-guid" - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - - ui := deleteOrg(t, "Yes", []string{"org-to-delete"}, orgRepo) - - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Contains(t, ui.Outputs[0], "Deleting org") - assert.Contains(t, ui.Outputs[0], "org-to-delete") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, orgRepo.FindByNameName, "org-to-delete") - assert.Equal(t, orgRepo.DeletedOrganizationGuid, "org-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteTargetedOrganizationClearsConfig(t *testing.T) { - configRepo := &testconfig.FakeConfigRepository{} - config, _ := configRepo.Get() - - organizationFields := cf.OrganizationFields{} - organizationFields.Name = "org-to-delete" - organizationFields.Guid = "org-to-delete-guid" - config.OrganizationFields = organizationFields - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "space-to-delete" - config.SpaceFields = spaceFields - configRepo.Save() - - org := cf.Organization{} - org.OrganizationFields = organizationFields - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - deleteOrg(t, "Yes", []string{"org-to-delete"}, orgRepo) - - updatedConfig, err := configRepo.Get() - assert.NoError(t, err) - - assert.Equal(t, updatedConfig.OrganizationFields, cf.OrganizationFields{}) - assert.Equal(t, updatedConfig.SpaceFields, cf.SpaceFields{}) -} - -func TestDeleteUntargetedOrganizationDoesNotClearConfig(t *testing.T) { - org := cf.Organization{} - org.Name = "org-to-delete" - org.Guid = "org-to-delete-guid" - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - - configRepo := &testconfig.FakeConfigRepository{} - config, _ := configRepo.Get() - otherOrgFields := cf.OrganizationFields{} - otherOrgFields.Guid = "some-other-org-guid" - otherOrgFields.Name = "some-other-org" - config.OrganizationFields = otherOrgFields - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "some-other-space" - config.SpaceFields = spaceFields - configRepo.Save() - - deleteOrg(t, "Yes", []string{"org-to-delete"}, orgRepo) - - updatedConfig, err := configRepo.Get() - assert.NoError(t, err) - - assert.Equal(t, updatedConfig.OrganizationFields.Name, "some-other-org") - assert.Equal(t, updatedConfig.SpaceFields.Name, "some-other-space") -} - -func TestDeleteOrgWithForceOption(t *testing.T) { - org := cf.Organization{} - org.Name = "org-to-delete" - org.Guid = "org-to-delete-guid" - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - - ui := deleteOrg(t, "Yes", []string{"-f", "org-to-delete"}, orgRepo) - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "org-to-delete") - assert.Equal(t, orgRepo.FindByNameName, "org-to-delete") - assert.Equal(t, orgRepo.DeletedOrganizationGuid, "org-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteOrgCommandFailsWithUsage(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - ui := deleteOrg(t, "Yes", []string{}, orgRepo) - assert.True(t, ui.FailedWithUsage) - - ui = deleteOrg(t, "Yes", []string{"org-to-delete"}, orgRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestDeleteOrgWhenOrgDoesNotExist(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{FindByNameNotFound: true} - ui := deleteOrg(t, "y", []string{"org-to-delete"}, orgRepo) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "org-to-delete") - assert.Equal(t, orgRepo.FindByNameName, "org-to-delete") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "org-to-delete") - assert.Contains(t, ui.Outputs[2], "does not exist.") -} - -func deleteOrg(t *testing.T, confirmation string, args []string, orgRepo *testapi.FakeOrgRepository) (ui *testterm.FakeUI) { - reqFactory := &testreq.FakeReqFactory{} - configRepo := &testconfig.FakeConfigRepository{} - - ui = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - ctxt := testcmd.NewContext("delete-org", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := NewDeleteOrg(ui, config, orgRepo, configRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/list_orgs.go b/src/cf/commands/organization/list_orgs.go deleted file mode 100644 index 70c136b0d12..00000000000 --- a/src/cf/commands/organization/list_orgs.go +++ /dev/null @@ -1,60 +0,0 @@ -package organization - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListOrgs struct { - ui terminal.UI - config *configuration.Configuration - orgRepo api.OrganizationRepository -} - -func NewListOrgs(ui terminal.UI, config *configuration.Configuration, orgRepo api.OrganizationRepository) (cmd ListOrgs) { - cmd.ui = ui - cmd.config = config - cmd.orgRepo = orgRepo - return -} - -func (cmd ListOrgs) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd ListOrgs) Run(c *cli.Context) { - cmd.ui.Say("Getting orgs as %s...\n", terminal.EntityNameColor(cmd.config.Username())) - - stopChan := make(chan bool) - defer close(stopChan) - - orgsChan, statusChan := cmd.orgRepo.ListOrgs(stopChan) - - table := cmd.ui.Table([]string{"name"}) - noOrgs := true - - for orgs := range orgsChan { - rows := [][]string{} - for _, org := range orgs { - rows = append(rows, []string{org.Name}) - } - table.Print(rows) - noOrgs = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching orgs.\n%s", apiStatus.Message) - return - } - - if noOrgs { - cmd.ui.Say("No orgs found") - } -} diff --git a/src/cf/commands/organization/list_orgs_test.go b/src/cf/commands/organization/list_orgs_test.go deleted file mode 100644 index 801da854451..00000000000 --- a/src/cf/commands/organization/list_orgs_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package organization_test - -import ( - "cf" - "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListOrgsRequirements(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - config := &configuration.Configuration{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callListOrgs(config, reqFactory, orgRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callListOrgs(config, reqFactory, orgRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListAllPagesOfOrgs(t *testing.T) { - org1 := cf.Organization{} - org1.Name = "Organization-1" - - org2 := cf.Organization{} - org2.Name = "Organization-2" - - org3 := cf.Organization{} - org3.Name = "Organization-3" - - orgRepo := &testapi.FakeOrgRepository{ - Organizations: []cf.Organization{org1, org2, org3}, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - tokenInfo := configuration.TokenInfo{Username: "my-user"} - accessToken, err := testconfig.CreateAccessTokenWithTokenInfo(tokenInfo) - assert.NoError(t, err) - config := &configuration.Configuration{AccessToken: accessToken} - - ui := callListOrgs(config, reqFactory, orgRepo) - - testassert.SliceContains(t, ui.Outputs, []string{ - "Getting orgs as my-user", - "Organization-1", - "Organization-2", - "Organization-3", - }) -} - -func TestListNoOrgs(t *testing.T) { - orgs := []cf.Organization{} - orgRepo := &testapi.FakeOrgRepository{ - Organizations: orgs, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - tokenInfo := configuration.TokenInfo{Username: "my-user"} - accessToken, err := testconfig.CreateAccessTokenWithTokenInfo(tokenInfo) - assert.NoError(t, err) - config := &configuration.Configuration{AccessToken: accessToken} - - ui := callListOrgs(config, reqFactory, orgRepo) - - testassert.SliceContains(t, ui.Outputs, []string{ - "Getting orgs as my-user", - "No orgs found", - }) -} - -func callListOrgs(config *configuration.Configuration, reqFactory *testreq.FakeReqFactory, orgRepo *testapi.FakeOrgRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{} - ctxt := testcmd.NewContext("orgs", []string{}) - cmd := organization.NewListOrgs(fakeUI, config, orgRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/list_quotas.go b/src/cf/commands/organization/list_quotas.go deleted file mode 100644 index c6512d24bf2..00000000000 --- a/src/cf/commands/organization/list_quotas.go +++ /dev/null @@ -1,57 +0,0 @@ -package organization - -import ( - "cf/api" - "cf/configuration" - "cf/formatters" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListQuotas struct { - ui terminal.UI - config *configuration.Configuration - quotaRepo api.QuotaRepository -} - -func NewListQuotas(ui terminal.UI, config *configuration.Configuration, quotaRepo api.QuotaRepository) (cmd *ListQuotas) { - cmd = new(ListQuotas) - cmd.ui = ui - cmd.config = config - cmd.quotaRepo = quotaRepo - return -} - -func (cmd *ListQuotas) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd *ListQuotas) Run(c *cli.Context) { - cmd.ui.Say("Getting quotas as %s...", terminal.EntityNameColor(cmd.config.Username())) - - quotas, apiResponse := cmd.quotaRepo.FindAll() - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - []string{"name", "memory limit"}, - } - - for _, quota := range quotas { - table = append(table, []string{ - quota.Name, - formatters.ByteSize(quota.MemoryLimit * formatters.MEGABYTE), - }) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/organization/list_quotas_test.go b/src/cf/commands/organization/list_quotas_test.go deleted file mode 100644 index 5bc146a679e..00000000000 --- a/src/cf/commands/organization/list_quotas_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package organization_test - -import ( - "cf" - "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListQuotasRequirements(t *testing.T) { - quotaRepo := &testapi.FakeQuotaRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callListQuotas(t, reqFactory, quotaRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callListQuotas(t, reqFactory, quotaRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListQuotas(t *testing.T) { - quota := cf.QuotaFields{} - quota.Name = "quota-name" - quota.MemoryLimit = 1024 - - quotaRepo := &testapi.FakeQuotaRepository{FindAllQuotas: []cf.QuotaFields{quota}} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - ui := callListQuotas(t, reqFactory, quotaRepo) - - assert.Contains(t, ui.Outputs[0], "Getting quotas as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[3], "name") - assert.Contains(t, ui.Outputs[3], "memory limit") - assert.Contains(t, ui.Outputs[4], "quota-name") - assert.Contains(t, ui.Outputs[4], "1G") -} - -func callListQuotas(t *testing.T, reqFactory *testreq.FakeReqFactory, quotaRepo *testapi.FakeQuotaRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{} - ctxt := testcmd.NewContext("quotas", []string{}) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := organization.NewListQuotas(fakeUI, config, quotaRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/rename_org.go b/src/cf/commands/organization/rename_org.go deleted file mode 100644 index d8abd895dd0..00000000000 --- a/src/cf/commands/organization/rename_org.go +++ /dev/null @@ -1,57 +0,0 @@ -package organization - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RenameOrg struct { - ui terminal.UI - config *configuration.Configuration - orgRepo api.OrganizationRepository - orgReq requirements.OrganizationRequirement -} - -func NewRenameOrg(ui terminal.UI, config *configuration.Configuration, orgRepo api.OrganizationRepository) (cmd *RenameOrg) { - cmd = new(RenameOrg) - cmd.ui = ui - cmd.config = config - cmd.orgRepo = orgRepo - return -} - -func (cmd *RenameOrg) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "rename-org") - return - } - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.orgReq, - } - return -} - -func (cmd *RenameOrg) Run(c *cli.Context) { - org := cmd.orgReq.GetOrganization() - newName := c.Args()[1] - - cmd.ui.Say("Renaming org %s to %s as %s...", - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(newName), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.orgRepo.Rename(org.Guid, newName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Ok() -} diff --git a/src/cf/commands/organization/rename_org_test.go b/src/cf/commands/organization/rename_org_test.go deleted file mode 100644 index e7f798607ef..00000000000 --- a/src/cf/commands/organization/rename_org_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package organization_test - -import ( - "cf" - "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRenameOrgFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - orgRepo := &testapi.FakeOrgRepository{} - - fakeUI := callRenameOrg(t, []string{}, reqFactory, orgRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callRenameOrg(t, []string{"foo"}, reqFactory, orgRepo) - assert.True(t, fakeUI.FailedWithUsage) -} - -func TestRenameOrgRequirements(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callRenameOrg(t, []string{"my-org", "my-new-org"}, reqFactory, orgRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.OrganizationName, "my-org") -} - -func TestRenameOrgRun(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{} - - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Organization: org} - ui := callRenameOrg(t, []string{"my-org", "my-new-org"}, reqFactory, orgRepo) - - assert.Contains(t, ui.Outputs[0], "Renaming org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-new-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, orgRepo.RenameOrganizationGuid, "my-org-guid") - assert.Equal(t, orgRepo.RenameNewName, "my-new-org") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callRenameOrg(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, orgRepo *testapi.FakeOrgRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("rename-org", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := organization.NewRenameOrg(ui, config, orgRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/set_quota.go b/src/cf/commands/organization/set_quota.go deleted file mode 100644 index 9c61cb1bb29..00000000000 --- a/src/cf/commands/organization/set_quota.go +++ /dev/null @@ -1,66 +0,0 @@ -package organization - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type SetQuota struct { - ui terminal.UI - config *configuration.Configuration - quotaRepo api.QuotaRepository - orgReq requirements.OrganizationRequirement -} - -func NewSetQuota(ui terminal.UI, config *configuration.Configuration, quotaRepo api.QuotaRepository) (cmd *SetQuota) { - cmd = new(SetQuota) - cmd.ui = ui - cmd.config = config - cmd.quotaRepo = quotaRepo - return -} - -func (cmd *SetQuota) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "set-quota") - return - } - - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.orgReq, - } - return -} - -func (cmd *SetQuota) Run(c *cli.Context) { - org := cmd.orgReq.GetOrganization() - quotaName := c.Args()[1] - quota, apiResponse := cmd.quotaRepo.FindByName(quotaName) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Say("Setting quota %s to org %s as %s...", - terminal.EntityNameColor(quota.Name), - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse = cmd.quotaRepo.Update(org.Guid, quota.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/organization/set_quota_test.go b/src/cf/commands/organization/set_quota_test.go deleted file mode 100644 index 929c3c49e48..00000000000 --- a/src/cf/commands/organization/set_quota_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package organization_test - -import ( - "cf" - "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSetQuotaFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - quotaRepo := &testapi.FakeQuotaRepository{} - - fakeUI := callSetQuota(t, []string{}, reqFactory, quotaRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callSetQuota(t, []string{"org"}, reqFactory, quotaRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callSetQuota(t, []string{"org", "quota"}, reqFactory, quotaRepo) - assert.False(t, fakeUI.FailedWithUsage) - - fakeUI = callSetQuota(t, []string{"org", "quota", "extra-stuff"}, reqFactory, quotaRepo) - assert.True(t, fakeUI.FailedWithUsage) -} - -func TestSetQuotaRequirements(t *testing.T) { - quotaRepo := &testapi.FakeQuotaRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callSetQuota(t, []string{"my-org", "my-quota"}, reqFactory, quotaRepo) - - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.OrganizationName, "my-org") - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callSetQuota(t, []string{"my-org", "my-quota"}, reqFactory, quotaRepo) - - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestSetQuota(t *testing.T) { - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - - quota := cf.QuotaFields{} - quota.Name = "my-found-quota" - quota.Guid = "my-quota-guid" - - quotaRepo := &testapi.FakeQuotaRepository{FindByNameQuota: quota} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Organization: org} - - ui := callSetQuota(t, []string{"my-org", "my-quota"}, reqFactory, quotaRepo) - - assert.Equal(t, quotaRepo.FindByNameName, "my-quota") - - assert.Contains(t, ui.Outputs[0], "Setting quota") - assert.Contains(t, ui.Outputs[0], "my-found-quota") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, quotaRepo.UpdateOrgGuid, "my-org-guid") - assert.Equal(t, quotaRepo.UpdateQuotaGuid, "my-quota-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callSetQuota(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, quotaRepo *testapi.FakeQuotaRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("set-quota", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := organization.NewSetQuota(ui, config, quotaRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/organization/show_org.go b/src/cf/commands/organization/show_org.go deleted file mode 100644 index 8bb46b7d0db..00000000000 --- a/src/cf/commands/organization/show_org.go +++ /dev/null @@ -1,62 +0,0 @@ -package organization - -import ( - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" - "strings" -) - -type ShowOrg struct { - ui terminal.UI - config *configuration.Configuration - orgReq requirements.OrganizationRequirement -} - -func NewShowOrg(ui terminal.UI, config *configuration.Configuration) (cmd *ShowOrg) { - cmd = new(ShowOrg) - cmd.ui = ui - cmd.config = config - return -} - -func (cmd *ShowOrg) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "org") - return - } - - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.orgReq, - } - - return -} - -func (cmd *ShowOrg) Run(c *cli.Context) { - org := cmd.orgReq.GetOrganization() - cmd.ui.Say("Getting info for org %s as %s...", - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - cmd.ui.Ok() - cmd.ui.Say("\n%s:", terminal.EntityNameColor(org.Name)) - - domains := []string{} - for _, domain := range org.Domains { - domains = append(domains, domain.Name) - } - - spaces := []string{} - for _, space := range org.Spaces { - spaces = append(spaces, space.Name) - } - - cmd.ui.Say(" domains: %s", terminal.EntityNameColor(strings.Join(domains, ", "))) - cmd.ui.Say(" spaces: %s", terminal.EntityNameColor(strings.Join(spaces, ", "))) -} diff --git a/src/cf/commands/organization/show_org_test.go b/src/cf/commands/organization/show_org_test.go deleted file mode 100644 index 57749bebc54..00000000000 --- a/src/cf/commands/organization/show_org_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package organization_test - -import ( - "cf" - . "cf/commands/organization" - "cf/configuration" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestShowOrgRequirements(t *testing.T) { - args := []string{"my-org"} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - callShowOrg(t, args, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callShowOrg(t, args, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestShowOrgFailsWithUsage(t *testing.T) { - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - reqFactory := &testreq.FakeReqFactory{Organization: org, LoginSuccess: true} - - args := []string{"my-org"} - ui := callShowOrg(t, args, reqFactory) - assert.False(t, ui.FailedWithUsage) - - args = []string{} - ui = callShowOrg(t, args, reqFactory) - assert.True(t, ui.FailedWithUsage) -} - -func TestRunWhenOrganizationExists(t *testing.T) { - developmentSpaceFields := cf.SpaceFields{} - developmentSpaceFields.Name = "development" - stagingSpaceFields := cf.SpaceFields{} - stagingSpaceFields.Name = "staging" - domainFields := cf.DomainFields{} - domainFields.Name = "cfapps.io" - cfAppDomainFields := cf.DomainFields{} - cfAppDomainFields.Name = "cf-app.com" - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - org.Spaces = []cf.SpaceFields{developmentSpaceFields, stagingSpaceFields} - org.Domains = []cf.DomainFields{domainFields, cfAppDomainFields} - - reqFactory := &testreq.FakeReqFactory{Organization: org, LoginSuccess: true} - - args := []string{"my-org"} - ui := callShowOrg(t, args, reqFactory) - - assert.Equal(t, reqFactory.OrganizationName, "my-org") - - assert.Equal(t, len(ui.Outputs), 5) - assert.Contains(t, ui.Outputs[0], "Getting info for org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "my-org") - assert.Contains(t, ui.Outputs[3], " domains:") - assert.Contains(t, ui.Outputs[3], "cfapps.io, cf-app.com") - assert.Contains(t, ui.Outputs[4], " spaces:") - assert.Contains(t, ui.Outputs[4], "development, staging") -} - -func callShowOrg(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("org", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - spaceFields := cf.SpaceFields{} - spaceFields.Name = "my-space" - - orgFields := cf.OrganizationFields{} - orgFields.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: spaceFields, - OrganizationFields: orgFields, - AccessToken: token, - } - - cmd := NewShowOrg(ui, config) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/password.go b/src/cf/commands/password.go deleted file mode 100644 index ee16aca060a..00000000000 --- a/src/cf/commands/password.go +++ /dev/null @@ -1,64 +0,0 @@ -package commands - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type Password struct { - ui terminal.UI - pwdRepo api.PasswordRepository - configRepo configuration.ConfigurationRepository -} - -func NewPassword(ui terminal.UI, pwdRepo api.PasswordRepository, configRepo configuration.ConfigurationRepository) (cmd Password) { - cmd.ui = ui - cmd.pwdRepo = pwdRepo - cmd.configRepo = configRepo - return -} - -func (cmd Password) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewValidAccessTokenRequirement(), - } - return -} - -func (cmd Password) Run(c *cli.Context) { - oldPassword := cmd.ui.AskForPassword("Current Password%s", terminal.PromptColor(">")) - newPassword := cmd.ui.AskForPassword("New Password%s", terminal.PromptColor(">")) - verifiedPassword := cmd.ui.AskForPassword("Verify Password%s", terminal.PromptColor(">")) - - if verifiedPassword != newPassword { - cmd.ui.Failed("Password verification does not match") - return - } - - score, apiResponse := cmd.pwdRepo.GetScore(newPassword) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Say("Your password strength is: %s", score) - - cmd.ui.Say("Changing password...") - apiResponse = cmd.pwdRepo.UpdatePassword(oldPassword, newPassword) - - if apiResponse.IsNotSuccessful() { - if apiResponse.StatusCode == 401 { - cmd.ui.Failed("Current password did not match") - } else { - cmd.ui.Failed(apiResponse.Message) - } - return - } - - cmd.ui.Ok() - - cmd.configRepo.ClearSession() - cmd.ui.Say("Please log in again") -} diff --git a/src/cf/commands/password_test.go b/src/cf/commands/password_test.go deleted file mode 100644 index eabcb75481c..00000000000 --- a/src/cf/commands/password_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package commands_test - -import ( - "cf" - . "cf/commands" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestPasswordRequiresValidAccessToken(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{ValidAccessTokenSuccess: false} - configRepo := &testconfig.FakeConfigRepository{} - callPassword([]string{}, reqFactory, &testapi.FakePasswordRepo{}, configRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{ValidAccessTokenSuccess: true} - callPassword([]string{"", "", ""}, reqFactory, &testapi.FakePasswordRepo{}, configRepo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestPasswordCanBeChanged(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{ValidAccessTokenSuccess: true} - pwdRepo := &testapi.FakePasswordRepo{Score: "meh"} - configRepo := &testconfig.FakeConfigRepository{} - ui := callPassword([]string{"old-password", "new-password", "new-password"}, reqFactory, pwdRepo, configRepo) - - assert.Contains(t, ui.PasswordPrompts[0], "Current Password") - assert.Contains(t, ui.PasswordPrompts[1], "New Password") - assert.Contains(t, ui.PasswordPrompts[2], "Verify Password") - - assert.Equal(t, pwdRepo.ScoredPassword, "new-password") - assert.Contains(t, ui.Outputs[0], "Your password strength is: meh") - - assert.Contains(t, ui.Outputs[1], "Changing password...") - assert.Equal(t, pwdRepo.UpdateNewPassword, "new-password") - assert.Equal(t, pwdRepo.UpdateOldPassword, "old-password") - assert.Contains(t, ui.Outputs[2], "OK") - - assert.Contains(t, ui.Outputs[3], "Please log in again") - - updatedConfig, err := configRepo.Get() - assert.NoError(t, err) - assert.Empty(t, updatedConfig.AccessToken) - assert.Equal(t, updatedConfig.OrganizationFields, cf.OrganizationFields{}) - assert.Equal(t, updatedConfig.SpaceFields, cf.SpaceFields{}) -} - -func TestPasswordVerification(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{ValidAccessTokenSuccess: true} - pwdRepo := &testapi.FakePasswordRepo{Score: "meh"} - configRepo := &testconfig.FakeConfigRepository{} - ui := callPassword([]string{"old-password", "new-password", "new-password-with-error"}, reqFactory, pwdRepo, configRepo) - - assert.Contains(t, ui.PasswordPrompts[0], "Current Password") - assert.Contains(t, ui.PasswordPrompts[1], "New Password") - assert.Contains(t, ui.PasswordPrompts[2], "Verify Password") - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "Password verification does not match") - - assert.Equal(t, pwdRepo.UpdateNewPassword, "") -} - -func TestWhenCurrentPasswordDoesNotMatch(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{ValidAccessTokenSuccess: true} - pwdRepo := &testapi.FakePasswordRepo{UpdateUnauthorized: true, Score: "meh"} - configRepo := &testconfig.FakeConfigRepository{} - ui := callPassword([]string{"old-password", "new-password", "new-password"}, reqFactory, pwdRepo, configRepo) - - assert.Contains(t, ui.PasswordPrompts[0], "Current Password") - assert.Contains(t, ui.PasswordPrompts[1], "New Password") - assert.Contains(t, ui.PasswordPrompts[2], "Verify Password") - - assert.Equal(t, pwdRepo.ScoredPassword, "new-password") - assert.Contains(t, ui.Outputs[0], "Your password strength is: meh") - - assert.Contains(t, ui.Outputs[1], "Changing password...") - assert.Equal(t, pwdRepo.UpdateNewPassword, "new-password") - assert.Equal(t, pwdRepo.UpdateOldPassword, "old-password") - assert.Contains(t, ui.Outputs[2], "FAILED") - assert.Contains(t, ui.Outputs[3], "Current password did not match") -} - -func callPassword(inputs []string, reqFactory *testreq.FakeReqFactory, pwdRepo *testapi.FakePasswordRepo, configRepo *testconfig.FakeConfigRepository) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{Inputs: inputs} - - ctxt := testcmd.NewContext("passwd", []string{}) - cmd := NewPassword(ui, pwdRepo, configRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/route/create_route.go b/src/cf/commands/route/create_route.go deleted file mode 100644 index d946904ac4c..00000000000 --- a/src/cf/commands/route/create_route.go +++ /dev/null @@ -1,97 +0,0 @@ -package route - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/net" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RouteCreator interface { - CreateRoute(hostName string, domain cf.DomainFields, space cf.SpaceFields) (route cf.Route, apiResponse net.ApiResponse) -} - -type CreateRoute struct { - ui terminal.UI - config *configuration.Configuration - routeRepo api.RouteRepository - spaceReq requirements.SpaceRequirement - domainReq requirements.DomainRequirement -} - -func NewCreateRoute(ui terminal.UI, config *configuration.Configuration, routeRepo api.RouteRepository) (cmd *CreateRoute) { - cmd = new(CreateRoute) - cmd.ui = ui - cmd.config = config - cmd.routeRepo = routeRepo - return -} - -func (cmd *CreateRoute) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-route") - return - } - - spaceName := c.Args()[0] - domainName := c.Args()[1] - - cmd.spaceReq = reqFactory.NewSpaceRequirement(spaceName) - cmd.domainReq = reqFactory.NewDomainRequirement(domainName) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - cmd.spaceReq, - cmd.domainReq, - } - return -} - -func (cmd *CreateRoute) Run(c *cli.Context) { - hostName := c.String("n") - space := cmd.spaceReq.GetSpace() - domain := cmd.domainReq.GetDomain() - - _, apiResponse := cmd.CreateRoute(hostName, domain.DomainFields, space.SpaceFields) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } -} - -func (cmd *CreateRoute) CreateRoute(hostName string, domain cf.DomainFields, space cf.SpaceFields) (route cf.Route, apiResponse net.ApiResponse) { - cmd.ui.Say("Creating route %s for org %s / space %s as %s...", - terminal.EntityNameColor(domain.UrlForHost(hostName)), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - _, apiResponse = cmd.routeRepo.CreateInSpace(hostName, domain.Guid, space.Guid) - if apiResponse.IsNotSuccessful() { - var findApiResponse net.ApiResponse - route, findApiResponse = cmd.routeRepo.FindByHostAndDomain(hostName, domain.Name) - - if findApiResponse.IsNotSuccessful() || - route.Space.Guid != space.Guid || - route.Domain.Guid != domain.Guid || - route.Host != hostName { - return - } - - apiResponse = net.NewSuccessfulApiResponse() - cmd.ui.Ok() - cmd.ui.Warn("Route %s already exists", route.URL()) - return - } - - cmd.ui.Ok() - return -} diff --git a/src/cf/commands/route/create_route_test.go b/src/cf/commands/route/create_route_test.go deleted file mode 100644 index 60662c47691..00000000000 --- a/src/cf/commands/route/create_route_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package route_test - -import ( - "cf" - . "cf/commands/route" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateRouteRequirements(t *testing.T) { - routeRepo := &testapi.FakeRouteRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callCreateRoute(t, []string{"my-space", "example.com", "-n", "foo"}, reqFactory, routeRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callCreateRoute(t, []string{"my-space", "example.com", "-n", "foo"}, reqFactory, routeRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - callCreateRoute(t, []string{"my-space", "example.com", "-n", "foo"}, reqFactory, routeRepo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateRouteFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - routeRepo := &testapi.FakeRouteRepository{} - - ui := callCreateRoute(t, []string{""}, reqFactory, routeRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateRoute(t, []string{"my-space"}, reqFactory, routeRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateRoute(t, []string{"my-space", "example.com", "host"}, reqFactory, routeRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateRoute(t, []string{"my-space", "example.com", "-n", "host"}, reqFactory, routeRepo) - assert.False(t, ui.FailedWithUsage) - - ui = callCreateRoute(t, []string{"my-space", "example.com"}, reqFactory, routeRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestCreateRoute(t *testing.T) { - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - space.Name = "my-space" - domain := cf.DomainFields{} - domain.Guid = "domain-guid" - domain.Name = "example.com" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - TargetedOrgSuccess: true, - Domain: cf.Domain{DomainFields: domain}, - Space: cf.Space{SpaceFields: space}, - } - routeRepo := &testapi.FakeRouteRepository{} - - ui := callCreateRoute(t, []string{"-n", "host", "my-space", "example.com"}, reqFactory, routeRepo) - - assert.Contains(t, ui.Outputs[0], "Creating route") - assert.Contains(t, ui.Outputs[0], "host.example.com") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, routeRepo.CreateInSpaceHost, "host") - assert.Equal(t, routeRepo.CreateInSpaceDomainGuid, "domain-guid") - assert.Equal(t, routeRepo.CreateInSpaceSpaceGuid, "my-space-guid") - -} - -func TestCreateRouteIsIdempotent(t *testing.T) { - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - space.Name = "my-space" - domain := cf.DomainFields{} - domain.Guid = "domain-guid" - domain.Name = "example.com" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - TargetedOrgSuccess: true, - Domain: cf.Domain{DomainFields: domain}, - Space: cf.Space{SpaceFields: space}, - } - - route := cf.Route{} - route.Guid = "my-route-guid" - route.Host = "host" - route.Domain = domain - route.Space = space - routeRepo := &testapi.FakeRouteRepository{ - CreateInSpaceErr: true, - FindByHostAndDomainRoute: route, - } - - ui := callCreateRoute(t, []string{"-n", "host", "my-space", "example.com"}, reqFactory, routeRepo) - - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "host.example.com") - assert.Contains(t, ui.Outputs[2], "already exists") - assert.Equal(t, routeRepo.CreateInSpaceHost, "host") - assert.Equal(t, routeRepo.CreateInSpaceDomainGuid, "domain-guid") - assert.Equal(t, routeRepo.CreateInSpaceSpaceGuid, "my-space-guid") - -} - -func TestRouteCreator(t *testing.T) { - space := cf.SpaceFields{} - space.Guid = "my-space-guid" - space.Name = "my-space" - domain := cf.DomainFields{} - domain.Guid = "domain-guid" - domain.Name = "example.com" - - createdRoute := cf.RouteFields{} - createdRoute.Host = "my-host" - createdRoute.Guid = "my-route-guid" - routeRepo := &testapi.FakeRouteRepository{ - CreateInSpaceCreatedRoute: createdRoute, - } - - ui := new(testterm.FakeUI) - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateRoute(ui, config, routeRepo) - _, apiResponse := cmd.CreateRoute("my-host", domain, space) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Contains(t, ui.Outputs[0], "Creating route") - assert.Contains(t, ui.Outputs[0], "my-host.example.com") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, routeRepo.CreateInSpaceHost, "my-host") - assert.Equal(t, routeRepo.CreateInSpaceDomainGuid, "domain-guid") - assert.Equal(t, routeRepo.CreateInSpaceSpaceGuid, "my-space-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callCreateRoute(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, routeRepo *testapi.FakeRouteRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-route", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateRoute(fakeUI, config, routeRepo) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/route/delete_route.go b/src/cf/commands/route/delete_route.go deleted file mode 100644 index 986d9ae28ac..00000000000 --- a/src/cf/commands/route/delete_route.go +++ /dev/null @@ -1,81 +0,0 @@ -package route - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteRoute struct { - ui terminal.UI - config *configuration.Configuration - routeRepo api.RouteRepository -} - -func NewDeleteRoute(ui terminal.UI, config *configuration.Configuration, routeRepo api.RouteRepository) (cmd *DeleteRoute) { - cmd = new(DeleteRoute) - cmd.ui = ui - cmd.config = config - cmd.routeRepo = routeRepo - return -} - -func (cmd *DeleteRoute) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-route") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd *DeleteRoute) Run(c *cli.Context) { - host := c.String("n") - domainName := c.Args()[0] - - url := domainName - if host != "" { - url = host + "." + domainName - } - force := c.Bool("f") - if !force { - response := cmd.ui.Confirm( - "Really delete route %s?%s", - terminal.EntityNameColor(url), - terminal.PromptColor(">"), - ) - - if !response { - return - } - } - - cmd.ui.Say("Deleting route %s...", terminal.EntityNameColor(url)) - - route, apiResponse := cmd.routeRepo.FindByHostAndDomain(host, domainName) - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Route %s does not exist.", url) - return - } - - apiResponse = cmd.routeRepo.Delete(route.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/route/delete_route_test.go b/src/cf/commands/route/delete_route_test.go deleted file mode 100644 index 99eaac5cfc6..00000000000 --- a/src/cf/commands/route/delete_route_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package route_test - -import ( - "cf" - . "cf/commands/route" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteRouteRequirements(t *testing.T) { - routeRepo := &testapi.FakeRouteRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - callDeleteRoute(t, "y", []string{"-n", "my-host", "example.com"}, reqFactory, routeRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false} - callDeleteRoute(t, "y", []string{"-n", "my-host", "example.com"}, reqFactory, routeRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteRouteFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - routeRepo := &testapi.FakeRouteRepository{} - ui := callDeleteRoute(t, "y", []string{}, reqFactory, routeRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callDeleteRoute(t, "y", []string{"example.com"}, reqFactory, routeRepo) - assert.False(t, ui.FailedWithUsage) - - ui = callDeleteRoute(t, "y", []string{"-n", "my-host", "example.com"}, reqFactory, routeRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestDeleteRouteWithConfirmation(t *testing.T) { - domain := cf.DomainFields{} - domain.Guid = "domain-guid" - domain.Name = "example.com" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - route := cf.Route{} - route.Guid = "route-guid" - route.Host = "my-host" - route.Domain = domain - routeRepo := &testapi.FakeRouteRepository{ - FindByHostAndDomainRoute: route, - } - - ui := callDeleteRoute(t, "y", []string{"-n", "my-host", "example.com"}, reqFactory, routeRepo) - - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Contains(t, ui.Outputs[0], "Deleting route") - assert.Contains(t, ui.Outputs[0], "my-host.example.com") - assert.Equal(t, routeRepo.DeleteRouteGuid, "route-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteRouteWithForce(t *testing.T) { - domain := cf.DomainFields{} - domain.Guid = "domain-guid" - domain.Name = "example.com" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - route := cf.Route{} - route.Guid = "route-guid" - route.Host = "my-host" - route.Domain = domain - routeRepo := &testapi.FakeRouteRepository{ - FindByHostAndDomainRoute: route, - } - - ui := callDeleteRoute(t, "", []string{"-f", "-n", "my-host", "example.com"}, reqFactory, routeRepo) - - assert.Equal(t, len(ui.Prompts), 0) - - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "my-host.example.com") - assert.Equal(t, routeRepo.DeleteRouteGuid, "route-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteRouteWhenRouteDoesNotExist(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - routeRepo := &testapi.FakeRouteRepository{ - FindByHostAndDomainNotFound: true, - } - - ui := callDeleteRoute(t, "y", []string{"-n", "my-host", "example.com"}, reqFactory, routeRepo) - - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "my-host.example.com") - - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "my-host") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func callDeleteRoute(t *testing.T, confirmation string, args []string, reqFactory *testreq.FakeReqFactory, routeRepo *testapi.FakeRouteRepository) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - ctxt := testcmd.NewContext("delete-route", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteRoute(ui, config, routeRepo) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/route/list_routes.go b/src/cf/commands/route/list_routes.go deleted file mode 100644 index d7108437eed..00000000000 --- a/src/cf/commands/route/list_routes.go +++ /dev/null @@ -1,68 +0,0 @@ -package route - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListRoutes struct { - ui terminal.UI - routeRepo api.RouteRepository - config *configuration.Configuration -} - -func NewListRoutes(ui terminal.UI, config *configuration.Configuration, routeRepo api.RouteRepository) (cmd *ListRoutes) { - cmd = new(ListRoutes) - cmd.ui = ui - cmd.config = config - cmd.routeRepo = routeRepo - return -} - -func (cmd ListRoutes) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd ListRoutes) Run(c *cli.Context) { - cmd.ui.Say("Getting routes as %s ...\n", - terminal.EntityNameColor(cmd.config.Username()), - ) - - stopChan := make(chan bool) - defer close(stopChan) - - routesChan, statusChan := cmd.routeRepo.ListRoutes(stopChan) - - table := cmd.ui.Table([]string{"host", "domain", "apps"}) - noRoutes := true - - for routes := range routesChan { - rows := [][]string{} - for _, route := range routes { - appNames := "" - for _, app := range route.Apps { - appNames = appNames + ", " + app.Name - } - rows = append(rows, []string{ - route.Host, - route.Domain.Name, - appNames, - }) - } - table.Print(rows) - noRoutes = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching routes.\n%s", apiStatus.Message) - return - } - - if noRoutes { - cmd.ui.Say("No routes found") - } -} diff --git a/src/cf/commands/route/list_routes_test.go b/src/cf/commands/route/list_routes_test.go deleted file mode 100644 index e39baf62073..00000000000 --- a/src/cf/commands/route/list_routes_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package route_test - -import ( - "cf" - . "cf/commands/route" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListingRoutes(t *testing.T) { - domain := cf.DomainFields{} - domain.Name = "example.com" - domain2 := cf.DomainFields{} - domain2.Name = "cfapps.com" - domain3 := cf.DomainFields{} - domain3.Name = "another-example.com" - - app1 := cf.ApplicationFields{} - app1.Name = "dora" - app2 := cf.ApplicationFields{} - app2.Name = "dora2" - - app3 := cf.ApplicationFields{} - app3.Name = "my-app" - app4 := cf.ApplicationFields{} - app4.Name = "my-app2" - - app5 := cf.ApplicationFields{} - app5.Name = "july" - - route := cf.Route{} - route.Host = "hostname-1" - route.Domain = domain - route.Apps = []cf.ApplicationFields{app1, app2} - route2 := cf.Route{} - route2.Host = "hostname-2" - route2.Domain = domain2 - route2.Apps = []cf.ApplicationFields{app3, app4} - route3 := cf.Route{} - route3.Host = "hostname-3" - route3.Domain = domain3 - route3.Apps = []cf.ApplicationFields{app5} - routes := []cf.Route{route, route2, route3} - - routeRepo := &testapi.FakeRouteRepository{Routes: routes} - - ui := callListRoutes(t, []string{}, &testreq.FakeReqFactory{}, routeRepo) - - assert.Contains(t, ui.Outputs[0], "Getting routes") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Contains(t, ui.Outputs[1], "host") - assert.Contains(t, ui.Outputs[1], "domain") - assert.Contains(t, ui.Outputs[1], "apps") - - assert.Contains(t, ui.Outputs[2], "hostname-1") - assert.Contains(t, ui.Outputs[2], "example.com") - assert.Contains(t, ui.Outputs[2], "dora, dora2") - - assert.Contains(t, ui.Outputs[3], "hostname-2") - assert.Contains(t, ui.Outputs[3], "cfapps.com") - assert.Contains(t, ui.Outputs[3], "my-app, my-app2") - - assert.Contains(t, ui.Outputs[4], "hostname-3") - assert.Contains(t, ui.Outputs[4], "another-example.com") - assert.Contains(t, ui.Outputs[4], "july") -} - -func TestListingRoutesWhenNoneExist(t *testing.T) { - routes := []cf.Route{} - routeRepo := &testapi.FakeRouteRepository{Routes: routes} - - ui := callListRoutes(t, []string{}, &testreq.FakeReqFactory{}, routeRepo) - - assert.Contains(t, ui.Outputs[0], "Getting routes") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "No routes found") -} - -func TestListingRoutesWhenFindFails(t *testing.T) { - routeRepo := &testapi.FakeRouteRepository{ListErr: true} - - ui := callListRoutes(t, []string{}, &testreq.FakeReqFactory{}, routeRepo) - - assert.Contains(t, ui.Outputs[0], "Getting routes") - assert.Contains(t, ui.Outputs[1], "FAILED") -} - -func callListRoutes(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, routeRepo *testapi.FakeRouteRepository) (ui *testterm.FakeUI) { - - ui = &testterm.FakeUI{} - - ctxt := testcmd.NewContext("list-routes", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewListRoutes(ui, config, routeRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/route/route_mapper.go b/src/cf/commands/route/route_mapper.go deleted file mode 100644 index 81f5edbbb35..00000000000 --- a/src/cf/commands/route/route_mapper.go +++ /dev/null @@ -1,98 +0,0 @@ -package route - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RouteMapper struct { - ui terminal.UI - config *configuration.Configuration - routeRepo api.RouteRepository - appReq requirements.ApplicationRequirement - domainReq requirements.DomainRequirement - routeCreator RouteCreator - bind bool -} - -func NewRouteMapper(ui terminal.UI, config *configuration.Configuration, routeRepo api.RouteRepository, routeCreator RouteCreator, bind bool) (cmd *RouteMapper) { - cmd = new(RouteMapper) - cmd.ui = ui - cmd.config = config - cmd.routeRepo = routeRepo - cmd.routeCreator = routeCreator - cmd.bind = bind - return -} - -func (cmd *RouteMapper) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - if cmd.bind { - cmd.ui.FailWithUsage(c, "map-route") - } else { - cmd.ui.FailWithUsage(c, "unmap-route") - } - return - } - - appName := c.Args()[0] - domainName := c.Args()[1] - - cmd.appReq = reqFactory.NewApplicationRequirement(appName) - cmd.domainReq = reqFactory.NewDomainRequirement(domainName) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.appReq, - cmd.domainReq, - } - return -} - -func (cmd *RouteMapper) Run(c *cli.Context) { - - // resolve the route we will bind to - hostName := c.String("n") - domain := cmd.domainReq.GetDomain() - - route, apiResponse := cmd.routeCreator.CreateRoute(hostName, domain.DomainFields, cmd.config.SpaceFields) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error resolving route:\n%s", apiResponse.Message) - } - - app := cmd.appReq.GetApplication() - - if cmd.bind { - cmd.ui.Say("Adding route %s to app %s in org %s / space %s as %s...", - terminal.EntityNameColor(route.URL()), - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse = cmd.routeRepo.Bind(route.Guid, app.Guid) - } else { - cmd.ui.Say("Removing route %s from app %s in org %s / space %s as %s...", - terminal.EntityNameColor(route.URL()), - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse = cmd.routeRepo.Unbind(route.Guid, app.Guid) - } - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/route/route_mapper_test.go b/src/cf/commands/route/route_mapper_test.go deleted file mode 100644 index 99f2af81266..00000000000 --- a/src/cf/commands/route/route_mapper_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package route_test - -import ( - "cf" - . "cf/commands/route" - "cf/configuration" - "github.com/codegangsta/cli" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRouteMapperFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - routeRepo := &testapi.FakeRouteRepository{} - - fakeUI := callRouteMapper(t, []string{}, reqFactory, routeRepo, &testcmd.FakeRouteCreator{}, true) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callRouteMapper(t, []string{"foo"}, reqFactory, routeRepo, &testcmd.FakeRouteCreator{}, true) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callRouteMapper(t, []string{"foo", "bar"}, reqFactory, routeRepo, &testcmd.FakeRouteCreator{}, true) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestRouteMapperRequirements(t *testing.T) { - routeRepo := &testapi.FakeRouteRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - callRouteMapper(t, []string{"-n", "my-host", "my-app", "my-domain.com"}, reqFactory, routeRepo, &testcmd.FakeRouteCreator{}, true) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, reqFactory.DomainName, "my-domain.com") -} - -func TestRouteMapperWhenBinding(t *testing.T) { - - domain := cf.Domain{} - domain.Guid = "my-domain-guid" - domain.Name = "example.com" - route := cf.Route{} - route.Guid = "my-route-guid" - route.Host = "foo" - route.Domain = domain.DomainFields - - app := cf.Application{} - app.Guid = "my-app-guid" - app.Name = "my-app" - - routeRepo := &testapi.FakeRouteRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Application: app, Domain: domain} - routeCreator := &testcmd.FakeRouteCreator{ReservedRoute: route} - - ui := callRouteMapper(t, []string{"-n", "my-host", "my-app", "my-domain.com"}, reqFactory, routeRepo, routeCreator, true) - - assert.Contains(t, ui.Outputs[0], "Adding route") - assert.Contains(t, ui.Outputs[0], "foo.example.com") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, routeRepo.BoundRouteGuid, "my-route-guid") - assert.Equal(t, routeRepo.BoundAppGuid, "my-app-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestRouteMapperWhenUnbinding(t *testing.T) { - domain := cf.Domain{} - domain.Guid = "my-domain-guid" - domain.Name = "example.com" - - route := cf.Route{} - route.Guid = "my-route-guid" - route.Host = "foo" - route.Domain = domain.DomainFields - - app := cf.Application{} - app.Guid = "my-app-guid" - app.Name = "my-app" - - routeRepo := &testapi.FakeRouteRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Application: app, Domain: domain} - routeCreator := &testcmd.FakeRouteCreator{ReservedRoute: route} - - ui := callRouteMapper(t, []string{"-n", "my-host", "my-app", "my-domain.com"}, reqFactory, routeRepo, routeCreator, false) - - assert.Contains(t, ui.Outputs[0], "Removing route") - assert.Contains(t, ui.Outputs[0], "foo.example.com") - assert.Contains(t, ui.Outputs[0], "my-app") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, routeRepo.UnboundRouteGuid, "my-route-guid") - assert.Equal(t, routeRepo.UnboundAppGuid, "my-app-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestRouteMapperWhenRouteNotReserved(t *testing.T) { - domain := cf.DomainFields{} - domain.Name = "my-domain.com" - route := cf.Route{} - route.Guid = "my-app-guid" - route.Host = "my-host" - route.Domain = domain - app := cf.Application{} - app.Guid = "my-app-guid" - app.Name = "my-app" - - routeRepo := &testapi.FakeRouteRepository{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Application: app} - routeCreator := &testcmd.FakeRouteCreator{ReservedRoute: route} - - callRouteMapper(t, []string{"-n", "my-host", "my-app", "my-domain.com"}, reqFactory, routeRepo, routeCreator, true) - - assert.Equal(t, routeCreator.ReservedRoute, route) -} - -func callRouteMapper(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, routeRepo *testapi.FakeRouteRepository, createRoute *testcmd.FakeRouteCreator, bind bool) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - var ctxt *cli.Context - if bind { - ctxt = testcmd.NewContext("map-route", args) - } else { - ctxt = testcmd.NewContext("unmap-route", args) - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewRouteMapper(ui, config, routeRepo, createRoute, bind) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/runner.go b/src/cf/commands/runner.go deleted file mode 100644 index c5b081da722..00000000000 --- a/src/cf/commands/runner.go +++ /dev/null @@ -1,54 +0,0 @@ -package commands - -import ( - "cf/requirements" - "errors" - "fmt" - "github.com/codegangsta/cli" - "os" -) - -type Command interface { - GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) - Run(c *cli.Context) -} - -type Runner interface { - RunCmdByName(cmdName string, c *cli.Context) (err error) -} - -type ConcreteRunner struct { - cmdFactory Factory - reqFactory requirements.Factory -} - -func NewRunner(cmdFactory Factory, reqFactory requirements.Factory) (runner ConcreteRunner) { - runner.cmdFactory = cmdFactory - runner.reqFactory = reqFactory - return -} - -func (runner ConcreteRunner) RunCmdByName(cmdName string, c *cli.Context) (err error) { - cmd, err := runner.cmdFactory.GetByCmdName(cmdName) - if err != nil { - fmt.Printf("Error finding command %s\n", cmdName) - os.Exit(1) - return - } - - requirements, err := cmd.GetRequirements(runner.reqFactory, c) - if err != nil { - return - } - - for _, requirement := range requirements { - success := requirement.Execute() - if !success { - err = errors.New("Error in requirement") - return - } - } - - cmd.Run(c) - return -} diff --git a/src/cf/commands/runner_test.go b/src/cf/commands/runner_test.go deleted file mode 100644 index e0da56d831d..00000000000 --- a/src/cf/commands/runner_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package commands_test - -import ( - . "cf/commands" - "cf/requirements" - "github.com/codegangsta/cli" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - "testing" -) - -type TestCommandFactory struct { - Cmd Command - CmdName string -} - -func (f *TestCommandFactory) GetByCmdName(cmdName string) (cmd Command, err error) { - f.CmdName = cmdName - cmd = f.Cmd - return -} - -type TestCommand struct { - Reqs []requirements.Requirement - WasRunWith *cli.Context -} - -func (cmd *TestCommand) GetRequirements(factory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = cmd.Reqs - return -} - -func (cmd *TestCommand) Run(c *cli.Context) { - cmd.WasRunWith = c -} - -type TestRequirement struct { - Passes bool - WasExecuted bool -} - -func (r *TestRequirement) Execute() (success bool) { - r.WasExecuted = true - - if !r.Passes { - return false - } - - return true -} - -func TestRun(t *testing.T) { - passingReq := TestRequirement{Passes: true} - failingReq := TestRequirement{Passes: false} - lastReq := TestRequirement{Passes: true} - - cmd := TestCommand{ - Reqs: []requirements.Requirement{&passingReq, &failingReq, &lastReq}, - } - - cmdFactory := &TestCommandFactory{Cmd: &cmd} - runner := NewRunner(cmdFactory, nil) - - ctxt := testcmd.NewContext("login", []string{}) - - err := runner.RunCmdByName("some-cmd", ctxt) - - assert.Equal(t, cmdFactory.CmdName, "some-cmd") - - assert.True(t, passingReq.WasExecuted, ctxt) - assert.True(t, failingReq.WasExecuted, ctxt) - - assert.False(t, lastReq.WasExecuted) - assert.Nil(t, cmd.WasRunWith) - - assert.Error(t, err) -} diff --git a/src/cf/commands/service/bind_service.go b/src/cf/commands/service/bind_service.go deleted file mode 100644 index 756cc57bad2..00000000000 --- a/src/cf/commands/service/bind_service.go +++ /dev/null @@ -1,71 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type BindService struct { - ui terminal.UI - config *configuration.Configuration - serviceBindingRepo api.ServiceBindingRepository - appReq requirements.ApplicationRequirement - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewBindService(ui terminal.UI, config *configuration.Configuration, serviceBindingRepo api.ServiceBindingRepository) (cmd *BindService) { - cmd = new(BindService) - cmd.ui = ui - cmd.config = config - cmd.serviceBindingRepo = serviceBindingRepo - return -} - -func (cmd *BindService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "bind-service") - return - } - appName := c.Args()[0] - serviceName := c.Args()[1] - - cmd.appReq = reqFactory.NewApplicationRequirement(appName) - cmd.serviceInstanceReq = reqFactory.NewServiceInstanceRequirement(serviceName) - - reqs = []requirements.Requirement{cmd.appReq, cmd.serviceInstanceReq} - return -} - -func (cmd *BindService) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - instance := cmd.serviceInstanceReq.GetServiceInstance() - - cmd.ui.Say("Binding service %s to app %s in org %s / space %s as %s...", - terminal.EntityNameColor(instance.Name), - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.serviceBindingRepo.Create(instance.Guid, app.Guid) - if apiResponse.IsNotSuccessful() && apiResponse.ErrorCode != "90003" { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - - if apiResponse.ErrorCode == "90003" { - cmd.ui.Warn("App %s is already bound to %s.", app.Name, instance.Name) - return - } - - cmd.ui.Say("TIP: Use 'cf push' to ensure your env variable changes take effect") -} diff --git a/src/cf/commands/service/bind_service_test.go b/src/cf/commands/service/bind_service_test.go deleted file mode 100644 index e4c0372e2a1..00000000000 --- a/src/cf/commands/service/bind_service_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestBindCommand(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{ - Application: app, - ServiceInstance: serviceInstance, - } - serviceBindingRepo := &testapi.FakeServiceBindingRepo{} - fakeUI := callBindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, reqFactory.ServiceInstanceName, "my-service") - - assert.Contains(t, fakeUI.Outputs[0], "Binding service") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - assert.Contains(t, fakeUI.Outputs[0], "my-app") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - - assert.Equal(t, serviceBindingRepo.CreateServiceInstanceGuid, "my-service-guid") - assert.Equal(t, serviceBindingRepo.CreateApplicationGuid, "my-app-guid") - - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "TIP") - assert.Equal(t, len(fakeUI.Outputs), 3) -} - -func TestBindCommandIfServiceIsAlreadyBound(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{ - Application: app, - ServiceInstance: serviceInstance, - } - serviceBindingRepo := &testapi.FakeServiceBindingRepo{CreateErrorCode: "90003"} - fakeUI := callBindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - - assert.Equal(t, len(fakeUI.Outputs), 3) - assert.Contains(t, fakeUI.Outputs[0], "Binding service") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-app") - assert.Contains(t, fakeUI.Outputs[2], "is already bound") - assert.Contains(t, fakeUI.Outputs[2], "my-service") -} - -func TestBindCommandFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceBindingRepo := &testapi.FakeServiceBindingRepo{} - - fakeUI := callBindService(t, []string{"my-service"}, reqFactory, serviceBindingRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callBindService(t, []string{"my-app"}, reqFactory, serviceBindingRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callBindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func callBindService(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, serviceBindingRepo api.ServiceBindingRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("bind-service", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewBindService(fakeUI, config, serviceBindingRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/create_service.go b/src/cf/commands/service/create_service.go deleted file mode 100644 index 4cea2232432..00000000000 --- a/src/cf/commands/service/create_service.go +++ /dev/null @@ -1,101 +0,0 @@ -package service - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "fmt" - "github.com/codegangsta/cli" -) - -type CreateService struct { - ui terminal.UI - config *configuration.Configuration - serviceRepo api.ServiceRepository -} - -func NewCreateService(ui terminal.UI, config *configuration.Configuration, serviceRepo api.ServiceRepository) (cmd CreateService) { - cmd.ui = ui - cmd.config = config - cmd.serviceRepo = serviceRepo - return -} - -func (cmd CreateService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 3 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-service") - return - } - - return -} - -func (cmd CreateService) Run(c *cli.Context) { - offeringName := c.Args()[0] - planName := c.Args()[1] - name := c.Args()[2] - - cmd.ui.Say("Creating service %s in org %s / space %s as %s...", - terminal.EntityNameColor(name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - offerings, apiResponse := cmd.serviceRepo.GetServiceOfferings() - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - offering, err := findOffering(offerings, offeringName) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } - - plan, err := findPlan(offering.Plans, planName) - if err != nil { - cmd.ui.Failed(err.Error()) - return - } - - var identicalAlreadyExists bool - identicalAlreadyExists, apiResponse = cmd.serviceRepo.CreateServiceInstance(name, plan.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - - if identicalAlreadyExists { - cmd.ui.Warn("Service %s already exists", name) - } -} - -func findOffering(offerings []cf.ServiceOffering, name string) (offering cf.ServiceOffering, err error) { - for _, offering := range offerings { - if name == offering.Label { - return offering, nil - } - } - - err = errors.New(fmt.Sprintf("Could not find offering with name %s", name)) - return -} - -func findPlan(plans []cf.ServicePlanFields, name string) (plan cf.ServicePlanFields, err error) { - for _, plan := range plans { - if name == plan.Name { - return plan, nil - } - } - - err = errors.New(fmt.Sprintf("Could not find plan with name %s", name)) - return -} diff --git a/src/cf/commands/service/create_service_test.go b/src/cf/commands/service/create_service_test.go deleted file mode 100644 index b9d0e119812..00000000000 --- a/src/cf/commands/service/create_service_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateService(t *testing.T) { - offering := cf.ServiceOffering{} - offering.Label = "cleardb" - plan := cf.ServicePlanFields{} - plan.Name = "spark" - plan.Guid = "cleardb-spark-guid" - offering.Plans = []cf.ServicePlanFields{plan} - offering2 := cf.ServiceOffering{} - offering2.Label = "postgres" - serviceOfferings := []cf.ServiceOffering{offering, offering2} - serviceRepo := &testapi.FakeServiceRepo{ServiceOfferings: serviceOfferings} - fakeUI := callCreateService(t, - []string{"cleardb", "spark", "my-cleardb-service"}, - []string{}, - serviceRepo, - ) - - assert.Contains(t, fakeUI.Outputs[0], "Creating service") - assert.Contains(t, fakeUI.Outputs[0], "my-cleardb-service") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Equal(t, serviceRepo.CreateServiceInstanceName, "my-cleardb-service") - assert.Equal(t, serviceRepo.CreateServiceInstancePlanGuid, "cleardb-spark-guid") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestCreateServiceWhenServiceAlreadyExists(t *testing.T) { - offering := cf.ServiceOffering{} - offering.Label = "cleardb" - plan := cf.ServicePlanFields{} - plan.Name = "spark" - plan.Guid = "cleardb-spark-guid" - offering.Plans = []cf.ServicePlanFields{plan} - offering2 := cf.ServiceOffering{} - offering2.Label = "postgres" - serviceOfferings := []cf.ServiceOffering{offering, offering2} - serviceRepo := &testapi.FakeServiceRepo{ServiceOfferings: serviceOfferings, CreateServiceAlreadyExists: true} - fakeUI := callCreateService(t, - []string{"cleardb", "spark", "my-cleardb-service"}, - []string{}, - serviceRepo, - ) - - assert.Contains(t, fakeUI.Outputs[0], "Creating service") - assert.Contains(t, fakeUI.Outputs[0], "my-cleardb-service") - assert.Equal(t, serviceRepo.CreateServiceInstanceName, "my-cleardb-service") - assert.Equal(t, serviceRepo.CreateServiceInstancePlanGuid, "cleardb-spark-guid") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-cleardb-service") - assert.Contains(t, fakeUI.Outputs[2], "already exists") -} - -func callCreateService(t *testing.T, args []string, inputs []string, serviceRepo api.ServiceRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{Inputs: inputs} - ctxt := testcmd.NewContext("create-service", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateService(fakeUI, config, serviceRepo) - reqFactory := &testreq.FakeReqFactory{} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/create_user_provided_service.go b/src/cf/commands/service/create_user_provided_service.go deleted file mode 100644 index 96f1177a3a6..00000000000 --- a/src/cf/commands/service/create_user_provided_service.go +++ /dev/null @@ -1,72 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "encoding/json" - "errors" - "github.com/codegangsta/cli" - "strings" -) - -type CreateUserProvidedService struct { - ui terminal.UI - config *configuration.Configuration - userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository -} - -func NewCreateUserProvidedService(ui terminal.UI, config *configuration.Configuration, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (cmd CreateUserProvidedService) { - cmd.ui = ui - cmd.config = config - cmd.userProvidedServiceInstanceRepo = userProvidedServiceInstanceRepo - return -} - -func (cmd CreateUserProvidedService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-user-provided-service") - return - } - - return -} - -func (cmd CreateUserProvidedService) Run(c *cli.Context) { - name := c.Args()[0] - drainUrl := c.String("l") - - params := c.String("p") - params = strings.Trim(params, `"`) - paramsMap := make(map[string]string) - - err := json.Unmarshal([]byte(params), ¶msMap) - if err != nil && params != "" { - paramsMap = cmd.mapValuesFromPrompt(params, paramsMap) - } - - cmd.ui.Say("Creating user provided service %s in org %s / space %s as %s...", - terminal.EntityNameColor(name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.userProvidedServiceInstanceRepo.Create(name, drainUrl, paramsMap) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} - -func (cmd CreateUserProvidedService) mapValuesFromPrompt(params string, paramsMap map[string]string) map[string]string { - for _, param := range strings.Split(params, ",") { - param = strings.Trim(param, " ") - paramsMap[param] = cmd.ui.Ask("%s%s", param, terminal.PromptColor(">")) - } - return paramsMap -} diff --git a/src/cf/commands/service/create_user_provided_service_test.go b/src/cf/commands/service/create_user_provided_service_test.go deleted file mode 100644 index 5d842d119d4..00000000000 --- a/src/cf/commands/service/create_user_provided_service_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateUserProvidedServiceWithParameterList(t *testing.T) { - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - fakeUI := callCreateUserProvidedService(t, - []string{"-p", `"foo, bar, baz"`, "my-custom-service"}, - []string{"foo value", "bar value", "baz value"}, - repo, - ) - - assert.Contains(t, fakeUI.Prompts[0], "foo") - assert.Contains(t, fakeUI.Prompts[1], "bar") - assert.Contains(t, fakeUI.Prompts[2], "baz") - - assert.Equal(t, repo.CreateName, "my-custom-service") - assert.Equal(t, repo.CreateParams, map[string]string{ - "foo": "foo value", - "bar": "bar value", - "baz": "baz value", - }) - - assert.Contains(t, fakeUI.Outputs[0], "Creating user provided service") - assert.Contains(t, fakeUI.Outputs[0], "my-custom-service") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestCreateUserProvidedServiceWithJson(t *testing.T) { - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - fakeUI := callCreateUserProvidedService(t, - []string{"-p", `{"foo": "foo value", "bar": "bar value", "baz": "baz value"}`, "my-custom-service"}, - []string{}, - repo, - ) - - assert.Empty(t, fakeUI.Prompts) - - assert.Equal(t, repo.CreateName, "my-custom-service") - assert.Equal(t, repo.CreateParams, map[string]string{ - "foo": "foo value", - "bar": "bar value", - "baz": "baz value", - }) - - assert.Contains(t, fakeUI.Outputs[0], "Creating user provided service") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestCreateUserProvidedServiceWithNoSecondArgument(t *testing.T) { - userProvidedServiceInstanceRepo := &testapi.FakeUserProvidedServiceInstanceRepo{} - fakeUI := callCreateUserProvidedService(t, - []string{"my-custom-service"}, - []string{}, - userProvidedServiceInstanceRepo, - ) - - assert.Contains(t, fakeUI.Outputs[0], "Creating user provided service") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestCreateUserProvidedServiceWithSyslogDrain(t *testing.T) { - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - - fakeUI := callCreateUserProvidedService(t, - []string{"-l", "syslog://example.com", "-p", `{"foo": "foo value", "bar": "bar value", "baz": "baz value"}`, "my-custom-service"}, - []string{}, - repo, - ) - assert.Equal(t, repo.CreateDrainUrl, "syslog://example.com") - assert.Contains(t, fakeUI.Outputs[0], "Creating user provided service") - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func callCreateUserProvidedService(t *testing.T, args []string, inputs []string, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{Inputs: inputs} - ctxt := testcmd.NewContext("create-user-provided-service", args) - reqFactory := &testreq.FakeReqFactory{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateUserProvidedService(fakeUI, config, userProvidedServiceInstanceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/delete_service.go b/src/cf/commands/service/delete_service.go deleted file mode 100644 index e43f7016959..00000000000 --- a/src/cf/commands/service/delete_service.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteService struct { - ui terminal.UI - config *configuration.Configuration - serviceRepo api.ServiceRepository - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewDeleteService(ui terminal.UI, config *configuration.Configuration, serviceRepo api.ServiceRepository) (cmd *DeleteService) { - cmd = new(DeleteService) - cmd.ui = ui - cmd.config = config - cmd.serviceRepo = serviceRepo - return -} - -func (cmd *DeleteService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - var serviceName string - - if len(c.Args()) == 1 { - serviceName = c.Args()[0] - } - - if serviceName == "" { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-service") - return - } - - return -} - -func (cmd *DeleteService) Run(c *cli.Context) { - serviceName := c.Args()[0] - force := c.Bool("f") - - if !force { - answer := cmd.ui.Confirm("Are you sure you want to delete the service %s ?", terminal.EntityNameColor(serviceName)) - if !answer { - return - } - } - - cmd.ui.Say("Deleting service %s in org %s / space %s as %s...", - terminal.EntityNameColor(serviceName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - instance, apiResponse := cmd.serviceRepo.FindInstanceByName(serviceName) - - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Service %s does not exist.", serviceName) - return - } - - apiResponse = cmd.serviceRepo.DeleteService(instance) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/service/delete_service_test.go b/src/cf/commands/service/delete_service_test.go deleted file mode 100644 index 7a598880ccd..00000000000 --- a/src/cf/commands/service/delete_service_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteServiceCommandWithY(t *testing.T) { - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{} - serviceRepo := &testapi.FakeServiceRepo{FindInstanceByNameServiceInstance: serviceInstance} - fakeUI := callDeleteService(t, "Y", []string{"my-service"}, reqFactory, serviceRepo) - - assert.Contains(t, fakeUI.Prompts[0], "Are you sure") - - assert.Contains(t, fakeUI.Outputs[0], "Deleting service") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - - assert.Equal(t, serviceRepo.DeleteServiceServiceInstance, serviceInstance) - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestDeleteServiceCommandWithYes(t *testing.T) { - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{} - serviceRepo := &testapi.FakeServiceRepo{FindInstanceByNameServiceInstance: serviceInstance} - fakeUI := callDeleteService(t, "Yes", []string{"my-service"}, reqFactory, serviceRepo) - - assert.Contains(t, fakeUI.Prompts[0], "Are you sure") - - assert.Contains(t, fakeUI.Outputs[0], "Deleting service") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - - assert.Equal(t, serviceRepo.DeleteServiceServiceInstance, serviceInstance) - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestDeleteServiceCommandOnNonExistentService(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceRepo := &testapi.FakeServiceRepo{FindInstanceByNameNotFound: true} - fakeUI := callDeleteService(t, "", []string{"-f", "my-service"}, reqFactory, serviceRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Deleting service") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-service") - assert.Contains(t, fakeUI.Outputs[2], "not exist") -} - -func TestDeleteServiceCommandFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceRepo := &testapi.FakeServiceRepo{} - - fakeUI := callDeleteService(t, "", []string{"-f"}, reqFactory, serviceRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callDeleteService(t, "", []string{"-f", "my-service"}, reqFactory, serviceRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestDeleteServiceForceFlagSkipsConfirmation(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceRepo := &testapi.FakeServiceRepo{} - - ui := callDeleteService(t, "", []string{"-f", "foo.com"}, reqFactory, serviceRepo) - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting service") - assert.Contains(t, ui.Outputs[0], "foo.com") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callDeleteService(t *testing.T, confirmation string, args []string, reqFactory *testreq.FakeReqFactory, serviceRepo api.ServiceRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - ctxt := testcmd.NewContext("delete-service", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteService(fakeUI, config, serviceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/list_services.go b/src/cf/commands/service/list_services.go deleted file mode 100644 index 3deb967307f..00000000000 --- a/src/cf/commands/service/list_services.go +++ /dev/null @@ -1,68 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" - "strings" -) - -type ListServices struct { - ui terminal.UI - config *configuration.Configuration - serviceSummaryRepo api.ServiceSummaryRepository -} - -func NewListServices(ui terminal.UI, config *configuration.Configuration, serviceSummaryRepo api.ServiceSummaryRepository) (cmd ListServices) { - cmd.ui = ui - cmd.config = config - cmd.serviceSummaryRepo = serviceSummaryRepo - return -} - -func (cmd ListServices) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd ListServices) Run(c *cli.Context) { - cmd.ui.Say("Getting services in org %s / space %s as %s...", - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - serviceInstances, apiResponse := cmd.serviceSummaryRepo.GetSummariesInCurrentSpace() - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - []string{"name", "service", "plan", "bound apps"}, - } - - for _, instance := range serviceInstances { - var serviceColumn string - - if instance.IsUserProvided() { - serviceColumn = "user-provided" - } else { - serviceColumn = instance.ServiceOffering.Label - } - - table = append(table, []string{ - instance.Name, - serviceColumn, - instance.ServicePlan.Name, - strings.Join(instance.ApplicationNames, ", "), - }) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/service/list_services_test.go b/src/cf/commands/service/list_services_test.go deleted file mode 100644 index 167b628b1f5..00000000000 --- a/src/cf/commands/service/list_services_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package service_test - -import ( - "cf" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testterm "testhelpers/terminal" - "testing" -) - -func TestServices(t *testing.T) { - plan := cf.ServicePlanFields{} - plan.Guid = "spark-guid" - plan.Name = "spark" - - offering := cf.ServiceOfferingFields{} - offering.Label = "cleardb" - - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service-1" - serviceInstance.ServicePlan = plan - serviceInstance.ApplicationNames = []string{"cli1", "cli2"} - serviceInstance.ServiceOffering = offering - - plan2 := cf.ServicePlanFields{} - plan2.Guid = "spark-guid-2" - plan2.Name = "spark-2" - - serviceInstance2 := cf.ServiceInstance{} - serviceInstance2.Name = "my-service-2" - serviceInstance2.ServicePlan = plan2 - serviceInstance2.ApplicationNames = []string{"cli1"} - serviceInstance2.ServiceOffering = offering - - serviceInstance3 := cf.ServiceInstance{} - serviceInstance3.Name = "my-service-provided-by-user" - - serviceInstances := []cf.ServiceInstance{serviceInstance, serviceInstance2, serviceInstance3} - serviceSummaryRepo := &testapi.FakeServiceSummaryRepo{ - GetSummariesInCurrentSpaceInstances: serviceInstances, - } - ui := &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewListServices(ui, config, serviceSummaryRepo) - cmd.Run(testcmd.NewContext("services", []string{})) - - assert.Contains(t, ui.Outputs[0], "Getting services in org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[4], "my-service-1") - assert.Contains(t, ui.Outputs[4], "cleardb") - assert.Contains(t, ui.Outputs[4], "spark") - assert.Contains(t, ui.Outputs[4], "cli1, cli2") - - assert.Contains(t, ui.Outputs[5], "my-service-2") - assert.Contains(t, ui.Outputs[5], "cleardb") - assert.Contains(t, ui.Outputs[5], "spark-2") - assert.Contains(t, ui.Outputs[5], "cli1") - - assert.Contains(t, ui.Outputs[6], "my-service-provided-by-user") - assert.Contains(t, ui.Outputs[6], "user-provided") -} diff --git a/src/cf/commands/service/marketplace_services.go b/src/cf/commands/service/marketplace_services.go deleted file mode 100644 index 1595baae90e..00000000000 --- a/src/cf/commands/service/marketplace_services.go +++ /dev/null @@ -1,68 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type MarketplaceServices struct { - ui terminal.UI - config *configuration.Configuration - serviceRepo api.ServiceRepository -} - -func NewMarketplaceServices(ui terminal.UI, config *configuration.Configuration, serviceRepo api.ServiceRepository) (cmd MarketplaceServices) { - cmd.ui = ui - cmd.config = config - cmd.serviceRepo = serviceRepo - return -} - -func (cmd MarketplaceServices) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd MarketplaceServices) Run(c *cli.Context) { - if cmd.config.HasSpace() { - cmd.ui.Say("Getting services from marketplace in org %s / space %s as %s...", - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - } else { - cmd.ui.Say("Getting services from marketplace...") - } - - serviceOfferings, apiResponse := cmd.serviceRepo.GetServiceOfferings() - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - []string{"service", "plans", "description"}, - } - - for _, offering := range serviceOfferings { - planNames := "" - for _, plan := range offering.Plans { - planNames = planNames + ", " + plan.Name - } - - table = append(table, []string{ - offering.Label, - planNames, - offering.Description, - }) - } - - cmd.ui.DisplayTable(table) - return -} diff --git a/src/cf/commands/service/marketplace_services_test.go b/src/cf/commands/service/marketplace_services_test.go deleted file mode 100644 index f98ad13bb9c..00000000000 --- a/src/cf/commands/service/marketplace_services_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package service_test - -import ( - "cf" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestMarketplaceServices(t *testing.T) { - plan := cf.ServicePlanFields{} - plan.Name = "service-plan-a" - plan2 := cf.ServicePlanFields{} - plan2.Name = "service-plan-b" - plan3 := cf.ServicePlanFields{} - plan3.Name = "service-plan-c" - plan4 := cf.ServicePlanFields{} - plan4.Name = "service-plan-d" - - offering := cf.ServiceOffering{} - offering.Label = "my-service-offering-1" - offering.Description = "service offering 1 description" - offering.Plans = []cf.ServicePlanFields{plan, plan2} - - offering2 := cf.ServiceOffering{} - offering2.Label = "my-service-offering-2" - offering2.Description = "service offering 2 description" - offering2.Plans = []cf.ServicePlanFields{plan3, plan4} - - serviceOfferings := []cf.ServiceOffering{offering, offering2} - serviceRepo := &testapi.FakeServiceRepo{ServiceOfferings: serviceOfferings} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - ui := callMarketplaceServices(t, config, serviceRepo) - - assert.Contains(t, ui.Outputs[0], "Getting services from marketplace in org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[4], "my-service-offering-1") - assert.Contains(t, ui.Outputs[4], "service offering 1 description") - assert.Contains(t, ui.Outputs[4], "service-plan-a, service-plan-b") - - assert.Contains(t, ui.Outputs[5], "my-service-offering-2") - assert.Contains(t, ui.Outputs[5], "service offering 2 description") - assert.Contains(t, ui.Outputs[5], "service-plan-c, service-plan-d") -} - -func TestMarketplaceServicesWhenNotLoggedIn(t *testing.T) { - serviceOfferings := []cf.ServiceOffering{} - serviceRepo := &testapi.FakeServiceRepo{ServiceOfferings: serviceOfferings} - - config := &configuration.Configuration{} - - ui := callMarketplaceServices(t, config, serviceRepo) - - assert.Contains(t, ui.Outputs[0], "Getting services from marketplace...") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callMarketplaceServices(t *testing.T, config *configuration.Configuration, serviceRepo *testapi.FakeServiceRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - ctxt := testcmd.NewContext("marketplace", []string{}) - reqFactory := &testreq.FakeReqFactory{} - - cmd := NewMarketplaceServices(ui, config, serviceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/rename_service.go b/src/cf/commands/service/rename_service.go deleted file mode 100644 index ac4bdaa05f2..00000000000 --- a/src/cf/commands/service/rename_service.go +++ /dev/null @@ -1,69 +0,0 @@ -package service - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RenameService struct { - ui terminal.UI - config *configuration.Configuration - serviceRepo api.ServiceRepository - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewRenameService(ui terminal.UI, config *configuration.Configuration, serviceRepo api.ServiceRepository) (cmd *RenameService) { - cmd = new(RenameService) - cmd.ui = ui - cmd.config = config - cmd.serviceRepo = serviceRepo - return -} - -func (cmd *RenameService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("incorrect usage") - cmd.ui.FailWithUsage(c, "rename-service") - return - } - - cmd.serviceInstanceReq = reqFactory.NewServiceInstanceRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.serviceInstanceReq, - } - - return -} - -func (cmd *RenameService) Run(c *cli.Context) { - newName := c.Args()[1] - serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() - - cmd.ui.Say("Renaming service %s to %s in org %s / space %s as %s...", - terminal.EntityNameColor(serviceInstance.Name), - terminal.EntityNameColor(newName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - apiResponse := cmd.serviceRepo.RenameService(serviceInstance, newName) - - if apiResponse.IsNotSuccessful() { - if apiResponse.ErrorCode == cf.SERVICE_INSTANCE_NAME_TAKEN { - cmd.ui.Failed("%s\nTIP: Use '%s services' to view all services in this org and space.", apiResponse.Message, cf.Name()) - } else { - cmd.ui.Failed(apiResponse.Message) - } - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/service/rename_service_test.go b/src/cf/commands/service/rename_service_test.go deleted file mode 100644 index cf317604105..00000000000 --- a/src/cf/commands/service/rename_service_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package service_test - -import ( - "cf" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRenameServiceFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - - fakeUI, _ := callRenameService(t, []string{}, reqFactory) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI, _ = callRenameService(t, []string{"my-service"}, reqFactory) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI, _ = callRenameService(t, []string{"my-service", "new-name", "extra"}, reqFactory) - assert.True(t, fakeUI.FailedWithUsage) -} - -func TestRenameServiceRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true} - callRenameService(t, []string{"my-service", "new-name"}, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false} - callRenameService(t, []string{"my-service", "new-name"}, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.ServiceInstanceName, "my-service") -} - -func TestRenameService(t *testing.T) { - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "different-name" - serviceInstance.Guid = "different-name-guid" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true, ServiceInstance: serviceInstance} - fakeUI, fakeServiceRepo := callRenameService(t, []string{"my-service", "new-name"}, reqFactory) - - assert.Contains(t, fakeUI.Outputs[0], "Renaming service") - assert.Contains(t, fakeUI.Outputs[0], "different-name") - assert.Contains(t, fakeUI.Outputs[0], "new-name") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Equal(t, fakeUI.Outputs[1], "OK") - - assert.Equal(t, fakeServiceRepo.RenameServiceServiceInstance, serviceInstance) - assert.Equal(t, fakeServiceRepo.RenameServiceNewName, "new-name") -} - -func callRenameService(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI, serviceRepo *testapi.FakeServiceRepo) { - ui = &testterm.FakeUI{} - serviceRepo = &testapi.FakeServiceRepo{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewRenameService(ui, config, serviceRepo) - ctxt := testcmd.NewContext("rename-service", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/service/show_service.go b/src/cf/commands/service/show_service.go deleted file mode 100644 index 8467a928eaf..00000000000 --- a/src/cf/commands/service/show_service.go +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import ( - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type ShowService struct { - ui terminal.UI - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewShowService(ui terminal.UI) (cmd *ShowService) { - cmd = new(ShowService) - cmd.ui = ui - return -} - -func (cmd *ShowService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "service") - return - } - - cmd.serviceInstanceReq = reqFactory.NewServiceInstanceRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedSpaceRequirement(), - cmd.serviceInstanceReq, - } - return -} - -func (cmd *ShowService) Run(c *cli.Context) { - serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() - - cmd.ui.Say("") - cmd.ui.Say("Service instance: %s", terminal.EntityNameColor(serviceInstance.Name)) - - if serviceInstance.IsUserProvided() { - cmd.ui.Say("Service: %s", terminal.EntityNameColor("user-provided")) - } else { - cmd.ui.Say("Service: %s", terminal.EntityNameColor(serviceInstance.ServiceOffering.Label)) - cmd.ui.Say("Plan: %s", terminal.EntityNameColor(serviceInstance.ServicePlan.Name)) - cmd.ui.Say("Description: %s", terminal.EntityNameColor(serviceInstance.ServiceOffering.Description)) - cmd.ui.Say("Documentation url: %s", terminal.EntityNameColor(serviceInstance.ServiceOffering.DocumentationUrl)) - } -} diff --git a/src/cf/commands/service/show_service_test.go b/src/cf/commands/service/show_service_test.go deleted file mode 100644 index f8977fb712e..00000000000 --- a/src/cf/commands/service/show_service_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package service_test - -import ( - "cf" - . "cf/commands/service" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestShowServiceRequirements(t *testing.T) { - args := []string{"service1"} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - callShowService(args, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: false} - callShowService(args, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedSpaceSuccess: true} - callShowService(args, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.ServiceInstanceName, "service1") -} - -func TestShowServiceFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedSpaceSuccess: true} - - ui := callShowService([]string{}, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callShowService([]string{"my-service"}, reqFactory) - assert.False(t, ui.FailedWithUsage) -} - -func TestShowServiceOutput(t *testing.T) { - offering := cf.ServiceOfferingFields{} - offering.Label = "mysql" - offering.DocumentationUrl = "http://documentation.url" - offering.Description = "the-description" - - plan := cf.ServicePlanFields{} - plan.Guid = "plan-guid" - plan.Name = "plan-name" - - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "service1" - serviceInstance.Guid = "service1-guid" - serviceInstance.ServicePlan = plan - serviceInstance.ServiceOffering = offering - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - TargetedSpaceSuccess: true, - ServiceInstance: serviceInstance, - } - ui := callShowService([]string{"service1"}, reqFactory) - - assert.Contains(t, ui.Outputs[0], "") - assert.Contains(t, ui.Outputs[1], "Service instance: ") - assert.Contains(t, ui.Outputs[1], "service1") - assert.Contains(t, ui.Outputs[2], "Service: ") - assert.Contains(t, ui.Outputs[2], "mysql") - assert.Contains(t, ui.Outputs[3], "Plan: ") - assert.Contains(t, ui.Outputs[3], "plan-name") - assert.Contains(t, ui.Outputs[4], "Description: ") - assert.Contains(t, ui.Outputs[4], "the-description") - assert.Contains(t, ui.Outputs[5], "Documentation url: ") - assert.Contains(t, ui.Outputs[5], "http://documentation.url") -} - -func TestShowUserProvidedServiceOutput(t *testing.T) { - serviceInstance2 := cf.ServiceInstance{} - serviceInstance2.Name = "service1" - serviceInstance2.Guid = "service1-guid" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - TargetedSpaceSuccess: true, - ServiceInstance: serviceInstance2, - } - ui := callShowService([]string{"service1"}, reqFactory) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Contains(t, ui.Outputs[0], "") - assert.Contains(t, ui.Outputs[1], "Service instance: ") - assert.Contains(t, ui.Outputs[1], "service1") - assert.Contains(t, ui.Outputs[2], "Service: ") - assert.Contains(t, ui.Outputs[2], "user-provided") -} - -func callShowService(args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("service", args) - cmd := NewShowService(ui) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/unbind_service.go b/src/cf/commands/service/unbind_service.go deleted file mode 100644 index 4d00e52cf9a..00000000000 --- a/src/cf/commands/service/unbind_service.go +++ /dev/null @@ -1,69 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UnbindService struct { - ui terminal.UI - config *configuration.Configuration - serviceBindingRepo api.ServiceBindingRepository - appReq requirements.ApplicationRequirement - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewUnbindService(ui terminal.UI, config *configuration.Configuration, serviceBindingRepo api.ServiceBindingRepository) (cmd *UnbindService) { - cmd = new(UnbindService) - cmd.ui = ui - cmd.config = config - cmd.serviceBindingRepo = serviceBindingRepo - return -} - -func (cmd *UnbindService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "unbind-service") - return - } - - appName := c.Args()[0] - serviceName := c.Args()[1] - - cmd.appReq = reqFactory.NewApplicationRequirement(appName) - cmd.serviceInstanceReq = reqFactory.NewServiceInstanceRequirement(serviceName) - - reqs = []requirements.Requirement{cmd.appReq, cmd.serviceInstanceReq} - return -} - -func (cmd *UnbindService) Run(c *cli.Context) { - app := cmd.appReq.GetApplication() - instance := cmd.serviceInstanceReq.GetServiceInstance() - - cmd.ui.Say("Unbinding app %s from service %s in org %s / space %s as %s...", - terminal.EntityNameColor(app.Name), - terminal.EntityNameColor(instance.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - found, apiResponse := cmd.serviceBindingRepo.Delete(instance, app.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - - if !found { - cmd.ui.Warn("Binding between %s and %s did not exist", instance.Name, app.Name) - } - -} diff --git a/src/cf/commands/service/unbind_service_test.go b/src/cf/commands/service/unbind_service_test.go deleted file mode 100644 index abc2bdb6e46..00000000000 --- a/src/cf/commands/service/unbind_service_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUnbindCommand(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{ - Application: app, - ServiceInstance: serviceInstance, - } - serviceBindingRepo := &testapi.FakeServiceBindingRepo{} - fakeUI := callUnbindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, reqFactory.ServiceInstanceName, "my-service") - - assert.Contains(t, fakeUI.Outputs[0], "Unbinding app") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - assert.Contains(t, fakeUI.Outputs[0], "my-app") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - - assert.Equal(t, serviceBindingRepo.DeleteServiceInstance, serviceInstance) - assert.Equal(t, serviceBindingRepo.DeleteApplicationGuid, "my-app-guid") - - assert.Contains(t, fakeUI.Outputs[1], "OK") -} - -func TestUnbindCommandWhenBindingIsNonExistent(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "my-service" - serviceInstance.Guid = "my-service-guid" - reqFactory := &testreq.FakeReqFactory{ - Application: app, - ServiceInstance: serviceInstance, - } - serviceBindingRepo := &testapi.FakeServiceBindingRepo{DeleteBindingNotFound: true} - fakeUI := callUnbindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - - assert.Equal(t, reqFactory.ApplicationName, "my-app") - assert.Equal(t, reqFactory.ServiceInstanceName, "my-service") - - assert.Contains(t, fakeUI.Outputs[0], "Unbinding app") - assert.Contains(t, fakeUI.Outputs[0], "my-service") - assert.Contains(t, fakeUI.Outputs[0], "my-app") - - assert.Equal(t, serviceBindingRepo.DeleteServiceInstance, serviceInstance) - assert.Equal(t, serviceBindingRepo.DeleteApplicationGuid, "my-app-guid") - - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-service") - assert.Contains(t, fakeUI.Outputs[2], "my-app") - assert.Contains(t, fakeUI.Outputs[2], "did not exist") -} - -func TestUnbindCommandFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceBindingRepo := &testapi.FakeServiceBindingRepo{} - - fakeUI := callUnbindService(t, []string{"my-service"}, reqFactory, serviceBindingRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callUnbindService(t, []string{"my-app"}, reqFactory, serviceBindingRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callUnbindService(t, []string{"my-app", "my-service"}, reqFactory, serviceBindingRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func callUnbindService(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, serviceBindingRepo api.ServiceBindingRepository) (fakeUI *testterm.FakeUI) { - fakeUI = new(testterm.FakeUI) - ctxt := testcmd.NewContext("unbind-service", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewUnbindService(fakeUI, config, serviceBindingRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/service/update_user_provided_service.go b/src/cf/commands/service/update_user_provided_service.go deleted file mode 100644 index 694508294ba..00000000000 --- a/src/cf/commands/service/update_user_provided_service.go +++ /dev/null @@ -1,86 +0,0 @@ -package service - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "encoding/json" - "errors" - "github.com/codegangsta/cli" -) - -type UpdateUserProvidedService struct { - ui terminal.UI - config *configuration.Configuration - userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository - serviceInstanceReq requirements.ServiceInstanceRequirement -} - -func NewUpdateUserProvidedService(ui terminal.UI, config *configuration.Configuration, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (cmd *UpdateUserProvidedService) { - cmd = new(UpdateUserProvidedService) - cmd.ui = ui - cmd.config = config - cmd.userProvidedServiceInstanceRepo = userProvidedServiceInstanceRepo - return -} - -func (cmd *UpdateUserProvidedService) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "update-user-provided-service") - return - } - - cmd.serviceInstanceReq = reqFactory.NewServiceInstanceRequirement(c.Args()[0]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.serviceInstanceReq, - } - - return -} - -func (cmd *UpdateUserProvidedService) Run(c *cli.Context) { - - serviceInstance := cmd.serviceInstanceReq.GetServiceInstance() - if !serviceInstance.IsUserProvided() { - cmd.ui.Failed("Service Instance is not user provided") - return - } - - drainUrl := c.String("l") - params := c.String("p") - - paramsMap := make(map[string]string) - if params != "" { - - err := json.Unmarshal([]byte(params), ¶msMap) - if err != nil { - cmd.ui.Failed("JSON is invalid: %s", err.Error()) - return - } - } - - cmd.ui.Say("Updating user provided service %s in org %s / space %s as %s...", - terminal.EntityNameColor(serviceInstance.Name), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - serviceInstance.Params = paramsMap - serviceInstance.SysLogDrainUrl = drainUrl - - apiResponse := cmd.userProvidedServiceInstanceRepo.Update(serviceInstance.ServiceInstanceFields) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - if params == "" && drainUrl == "" { - cmd.ui.Warn("No flags specified. No changes were made.") - } -} diff --git a/src/cf/commands/service/update_user_provided_service_test.go b/src/cf/commands/service/update_user_provided_service_test.go deleted file mode 100644 index 46b1becadac..00000000000 --- a/src/cf/commands/service/update_user_provided_service_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package service_test - -import ( - "cf" - "cf/api" - . "cf/commands/service" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUpdateUserProvidedServiceFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - userProvidedServiceInstanceRepo := &testapi.FakeUserProvidedServiceInstanceRepo{} - - fakeUI := callUpdateUserProvidedService(t, []string{}, reqFactory, userProvidedServiceInstanceRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callUpdateUserProvidedService(t, []string{"foo"}, reqFactory, userProvidedServiceInstanceRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestUpdateUserProvidedServiceRequirements(t *testing.T) { - args := []string{"service-name"} - reqFactory := &testreq.FakeReqFactory{} - userProvidedServiceInstanceRepo := &testapi.FakeUserProvidedServiceInstanceRepo{} - - reqFactory.LoginSuccess = false - callUpdateUserProvidedService(t, args, reqFactory, userProvidedServiceInstanceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callUpdateUserProvidedService(t, args, reqFactory, userProvidedServiceInstanceRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.ServiceInstanceName, "service-name") -} - -func TestUpdateUserProvidedServiceWhenNoFlagsArePresent(t *testing.T) { - args := []string{"service-name"} - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "found-service-name" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - ServiceInstance: serviceInstance, - } - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - ui := callUpdateUserProvidedService(t, args, reqFactory, repo) - - assert.Contains(t, ui.Outputs[0], "Updating user provided service") - assert.Contains(t, ui.Outputs[0], "found-service-name") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "No changes") -} - -func TestUpdateUserProvidedServiceWithJson(t *testing.T) { - args := []string{"-p", `{"foo":"bar"}`, "-l", "syslog://example.com", "service-name"} - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "found-service-name" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - ServiceInstance: serviceInstance, - } - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - ui := callUpdateUserProvidedService(t, args, reqFactory, repo) - - assert.Contains(t, ui.Outputs[0], "Updating user provided service") - assert.Contains(t, ui.Outputs[0], "found-service-name") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, repo.UpdateServiceInstance.Name, serviceInstance.Name) - assert.Equal(t, repo.UpdateServiceInstance.Params, map[string]string{"foo": "bar"}) - assert.Equal(t, repo.UpdateServiceInstance.SysLogDrainUrl, "syslog://example.com") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestUpdateUserProvidedServiceWithoutJson(t *testing.T) { - args := []string{"-l", "syslog://example.com", "service-name"} - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "found-service-name" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - ServiceInstance: serviceInstance, - } - repo := &testapi.FakeUserProvidedServiceInstanceRepo{} - ui := callUpdateUserProvidedService(t, args, reqFactory, repo) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestUpdateUserProvidedServiceWithInvalidJson(t *testing.T) { - args := []string{"-p", `{"foo":"ba`, "service-name"} - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "found-service-name" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - ServiceInstance: serviceInstance, - } - userProvidedServiceInstanceRepo := &testapi.FakeUserProvidedServiceInstanceRepo{} - - ui := callUpdateUserProvidedService(t, args, reqFactory, userProvidedServiceInstanceRepo) - - assert.NotEqual(t, userProvidedServiceInstanceRepo.UpdateServiceInstance, serviceInstance) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "JSON is invalid") -} - -func TestUpdateUserProvidedServiceWithAServiceInstanceThatIsNotUserProvided(t *testing.T) { - args := []string{"-p", `{"foo":"bar"}`, "service-name"} - plan := cf.ServicePlanFields{} - plan.Guid = "my-plan-guid" - serviceInstance := cf.ServiceInstance{} - serviceInstance.Name = "found-service-name" - serviceInstance.ServicePlan = plan - - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - ServiceInstance: serviceInstance, - } - userProvidedServiceInstanceRepo := &testapi.FakeUserProvidedServiceInstanceRepo{} - - ui := callUpdateUserProvidedService(t, args, reqFactory, userProvidedServiceInstanceRepo) - - assert.NotEqual(t, userProvidedServiceInstanceRepo.UpdateServiceInstance, serviceInstance) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "Service Instance is not user provided") -} - -func callUpdateUserProvidedService(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, userProvidedServiceInstanceRepo api.UserProvidedServiceInstanceRepository) (fakeUI *testterm.FakeUI) { - fakeUI = &testterm.FakeUI{} - ctxt := testcmd.NewContext("update-user-provided-service", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewUpdateUserProvidedService(fakeUI, config, userProvidedServiceInstanceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/serviceauthtoken/create_service_auth_token.go b/src/cf/commands/serviceauthtoken/create_service_auth_token.go deleted file mode 100644 index 1d9aa53b37c..00000000000 --- a/src/cf/commands/serviceauthtoken/create_service_auth_token.go +++ /dev/null @@ -1,55 +0,0 @@ -package serviceauthtoken - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateServiceAuthTokenFields struct { - ui terminal.UI - config *configuration.Configuration - authTokenRepo api.ServiceAuthTokenRepository -} - -func NewCreateServiceAuthToken(ui terminal.UI, config *configuration.Configuration, authTokenRepo api.ServiceAuthTokenRepository) (cmd CreateServiceAuthTokenFields) { - cmd.ui = ui - cmd.config = config - cmd.authTokenRepo = authTokenRepo - return -} - -func (cmd CreateServiceAuthTokenFields) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 3 { - err = errors.New("Incorrect usage") - cmd.ui.FailWithUsage(c, "create-service-auth-token") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd CreateServiceAuthTokenFields) Run(c *cli.Context) { - cmd.ui.Say("Creating service auth token as %s...", terminal.EntityNameColor(cmd.config.Username())) - - serviceAuthTokenRepo := cf.ServiceAuthTokenFields{ - Label: c.Args()[0], - Provider: c.Args()[1], - Token: c.Args()[2], - } - - apiResponse := cmd.authTokenRepo.Create(serviceAuthTokenRepo) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/serviceauthtoken/create_service_auth_token_test.go b/src/cf/commands/serviceauthtoken/create_service_auth_token_test.go deleted file mode 100644 index 824cd7459d8..00000000000 --- a/src/cf/commands/serviceauthtoken/create_service_auth_token_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package serviceauthtoken_test - -import ( - "cf" - . "cf/commands/serviceauthtoken" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateServiceAuthTokenFailsWithUsage(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - - ui := callCreateServiceAuthToken(t, []string{}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceAuthToken(t, []string{"arg1"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceAuthToken(t, []string{"arg1", "arg2"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceAuthToken(t, []string{"arg1", "arg2", "arg3"}, reqFactory, authTokenRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestCreateServiceAuthTokenRequirements(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - args := []string{"arg1", "arg2", "arg3"} - - reqFactory.LoginSuccess = true - callCreateServiceAuthToken(t, args, reqFactory, authTokenRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = false - callCreateServiceAuthToken(t, args, reqFactory, authTokenRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateServiceAuthToken(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider", "a value"} - - ui := callCreateServiceAuthToken(t, args, reqFactory, authTokenRepo) - assert.Contains(t, ui.Outputs[0], "Creating service auth token as") - assert.Contains(t, ui.Outputs[0], "my-user") - authToken := cf.ServiceAuthTokenFields{} - authToken.Label = "a label" - authToken.Provider = "a provider" - authToken.Token = "a value" - assert.Equal(t, authTokenRepo.CreatedServiceAuthTokenFields, authToken) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callCreateServiceAuthToken(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, authTokenRepo *testapi.FakeAuthTokenRepo) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateServiceAuthToken(ui, config, authTokenRepo) - ctxt := testcmd.NewContext("create-service-auth-token", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/serviceauthtoken/delete_service_auth_token.go b/src/cf/commands/serviceauthtoken/delete_service_auth_token.go deleted file mode 100644 index 2729be48394..00000000000 --- a/src/cf/commands/serviceauthtoken/delete_service_auth_token.go +++ /dev/null @@ -1,71 +0,0 @@ -package serviceauthtoken - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "fmt" - "github.com/codegangsta/cli" -) - -type DeleteServiceAuthTokenFields struct { - ui terminal.UI - config *configuration.Configuration - authTokenRepo api.ServiceAuthTokenRepository -} - -func NewDeleteServiceAuthToken(ui terminal.UI, config *configuration.Configuration, authTokenRepo api.ServiceAuthTokenRepository) (cmd DeleteServiceAuthTokenFields) { - cmd.ui = ui - cmd.config = config - cmd.authTokenRepo = authTokenRepo - return -} - -func (cmd DeleteServiceAuthTokenFields) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect usage") - cmd.ui.FailWithUsage(c, "delete-service-auth-token") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - return -} - -func (cmd DeleteServiceAuthTokenFields) Run(c *cli.Context) { - tokenLabel := c.Args()[0] - tokenProvider := c.Args()[1] - - if c.Bool("f") == false { - response := cmd.ui.Confirm( - "Are you sure you want to delete %s?%s", - terminal.EntityNameColor(fmt.Sprintf("%s %s", tokenLabel, tokenProvider)), - terminal.PromptColor(">"), - ) - if response == false { - return - } - } - - cmd.ui.Say("Deleting service auth token as %s", terminal.EntityNameColor(cmd.config.Username())) - token, apiResponse := cmd.authTokenRepo.FindByLabelAndProvider(tokenLabel, tokenProvider) - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Service Auth Token %s %s does not exist.", tokenLabel, tokenProvider) - return - } - - apiResponse = cmd.authTokenRepo.Delete(token) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/serviceauthtoken/delete_service_auth_token_test.go b/src/cf/commands/serviceauthtoken/delete_service_auth_token_test.go deleted file mode 100644 index 6b1d690cc37..00000000000 --- a/src/cf/commands/serviceauthtoken/delete_service_auth_token_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package serviceauthtoken_test - -import ( - "cf" - . "cf/commands/serviceauthtoken" - "cf/configuration" - "cf/net" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteServiceAuthTokenFailsWithUsage(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - - ui := callDeleteServiceAuthToken(t, []string{}, []string{"Y"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callDeleteServiceAuthToken(t, []string{"arg1"}, []string{"Y"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callDeleteServiceAuthToken(t, []string{"arg1", "arg2"}, []string{"Y"}, reqFactory, authTokenRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestDeleteServiceAuthTokenRequirements(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - args := []string{"arg1", "arg2"} - - reqFactory.LoginSuccess = true - callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = false - callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteServiceAuthToken(t *testing.T) { - expectedToken := cf.ServiceAuthTokenFields{} - expectedToken.Label = "a label" - expectedToken.Provider = "a provider" - - authTokenRepo := &testapi.FakeAuthTokenRepo{ - FindByLabelAndProviderServiceAuthTokenFields: expectedToken, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider"} - - ui := callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - assert.Contains(t, ui.Outputs[0], "Deleting service auth token as") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, authTokenRepo.FindByLabelAndProviderLabel, "a label") - assert.Equal(t, authTokenRepo.FindByLabelAndProviderProvider, "a provider") - assert.Equal(t, authTokenRepo.DeletedServiceAuthTokenFields, expectedToken) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteServiceAuthTokenWithN(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider"} - - ui := callDeleteServiceAuthToken(t, args, []string{"N"}, reqFactory, authTokenRepo) - - assert.Contains(t, ui.Prompts[0], "Are you sure you want to delete") - assert.Contains(t, ui.Prompts[0], "a label a provider") - assert.Equal(t, len(ui.Outputs), 0) - assert.Equal(t, authTokenRepo.DeletedServiceAuthTokenFields, cf.ServiceAuthTokenFields{}) -} - -func TestDeleteServiceAuthTokenWithY(t *testing.T) { - expectedToken := cf.ServiceAuthTokenFields{} - expectedToken.Label = "a label" - expectedToken.Provider = "a provider" - - authTokenRepo := &testapi.FakeAuthTokenRepo{ - FindByLabelAndProviderServiceAuthTokenFields: expectedToken, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider"} - - ui := callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - - assert.Contains(t, ui.Prompts[0], "delete") - assert.Contains(t, ui.Prompts[0], "a label") - assert.Contains(t, ui.Prompts[0], "a provider") - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Equal(t, authTokenRepo.DeletedServiceAuthTokenFields, expectedToken) - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteServiceAuthTokenWithForce(t *testing.T) { - expectedToken := cf.ServiceAuthTokenFields{} - expectedToken.Label = "a label" - expectedToken.Provider = "a provider" - - authTokenRepo := &testapi.FakeAuthTokenRepo{ - FindByLabelAndProviderServiceAuthTokenFields: expectedToken, - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"-f", "a label", "a provider"} - ui := callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, authTokenRepo.DeletedServiceAuthTokenFields, expectedToken) -} - -func TestDeleteServiceAuthTokenWhenTokenDoesNotExist(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{ - FindByLabelAndProviderApiResponse: net.NewNotFoundApiResponse("not found"), - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider"} - - ui := callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - assert.Contains(t, ui.Outputs[0], "Deleting service auth token as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func TestDeleteServiceAuthTokenFailsWithError(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{ - FindByLabelAndProviderApiResponse: net.NewApiResponseWithMessage("OH NOES"), - } - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider"} - - ui := callDeleteServiceAuthToken(t, args, []string{"Y"}, reqFactory, authTokenRepo) - assert.Contains(t, ui.Outputs[0], "Deleting service auth token as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "FAILED") - assert.Contains(t, ui.Outputs[2], "OH NOES") -} - -func callDeleteServiceAuthToken(t *testing.T, args []string, inputs []string, reqFactory *testreq.FakeReqFactory, authTokenRepo *testapi.FakeAuthTokenRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{ - Inputs: inputs, - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteServiceAuthToken(ui, config, authTokenRepo) - ctxt := testcmd.NewContext("delete-service-auth-token", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/serviceauthtoken/list_service_auth_tokens.go b/src/cf/commands/serviceauthtoken/list_service_auth_tokens.go deleted file mode 100644 index 60638047183..00000000000 --- a/src/cf/commands/serviceauthtoken/list_service_auth_tokens.go +++ /dev/null @@ -1,50 +0,0 @@ -package serviceauthtoken - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListServiceAuthTokens struct { - ui terminal.UI - config *configuration.Configuration - authTokenRepo api.ServiceAuthTokenRepository -} - -func NewListServiceAuthTokens(ui terminal.UI, config *configuration.Configuration, authTokenRepo api.ServiceAuthTokenRepository) (cmd ListServiceAuthTokens) { - cmd.ui = ui - cmd.config = config - cmd.authTokenRepo = authTokenRepo - return -} - -func (cmd ListServiceAuthTokens) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd ListServiceAuthTokens) Run(c *cli.Context) { - cmd.ui.Say("Getting service auth tokens as %s...", terminal.EntityNameColor(cmd.config.Username())) - authTokens, apiResponse := cmd.authTokenRepo.FindAll() - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - {"label", "provider"}, - } - - for _, authToken := range authTokens { - table = append(table, []string{authToken.Label, authToken.Provider}) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/serviceauthtoken/list_service_auth_tokens_test.go b/src/cf/commands/serviceauthtoken/list_service_auth_tokens_test.go deleted file mode 100644 index 3df3c503437..00000000000 --- a/src/cf/commands/serviceauthtoken/list_service_auth_tokens_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package serviceauthtoken_test - -import ( - "cf" - . "cf/commands/serviceauthtoken" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListServiceAuthTokensRequirements(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - - reqFactory.LoginSuccess = false - callListServiceAuthTokens(t, reqFactory, authTokenRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callListServiceAuthTokens(t, reqFactory, authTokenRepo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestListServiceAuthTokens(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - authTokenRepo := &testapi.FakeAuthTokenRepo{} - authToken := cf.ServiceAuthTokenFields{} - authToken.Label = "a label" - authToken.Provider = "a provider" - authToken2 := cf.ServiceAuthTokenFields{} - authToken2.Label = "a second label" - authToken2.Provider = "a second provider" - authTokenRepo.FindAllAuthTokens = []cf.ServiceAuthTokenFields{authToken, authToken2} - - ui := callListServiceAuthTokens(t, reqFactory, authTokenRepo) - assert.Contains(t, ui.Outputs[0], "Getting service auth tokens as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Contains(t, ui.Outputs[3], "label") - assert.Contains(t, ui.Outputs[3], "provider") - - assert.Contains(t, ui.Outputs[4], "a label") - assert.Contains(t, ui.Outputs[4], "a provider") - - assert.Contains(t, ui.Outputs[5], "a second label") - assert.Contains(t, ui.Outputs[5], "a second provider") -} - -func callListServiceAuthTokens(t *testing.T, reqFactory *testreq.FakeReqFactory, authTokenRepo *testapi.FakeAuthTokenRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewListServiceAuthTokens(ui, config, authTokenRepo) - ctxt := testcmd.NewContext("service-auth-tokens", []string{}) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/serviceauthtoken/update_service_auth_token.go b/src/cf/commands/serviceauthtoken/update_service_auth_token.go deleted file mode 100644 index 165f306ad85..00000000000 --- a/src/cf/commands/serviceauthtoken/update_service_auth_token.go +++ /dev/null @@ -1,56 +0,0 @@ -package serviceauthtoken - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UpdateServiceAuthTokenFields struct { - ui terminal.UI - config *configuration.Configuration - authTokenRepo api.ServiceAuthTokenRepository -} - -func NewUpdateServiceAuthToken(ui terminal.UI, config *configuration.Configuration, authTokenRepo api.ServiceAuthTokenRepository) (cmd UpdateServiceAuthTokenFields) { - cmd.ui = ui - cmd.config = config - cmd.authTokenRepo = authTokenRepo - return -} - -func (cmd UpdateServiceAuthTokenFields) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 3 { - err = errors.New("Incorrect usage") - cmd.ui.FailWithUsage(c, "update-service-auth-token") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - } - return -} - -func (cmd UpdateServiceAuthTokenFields) Run(c *cli.Context) { - cmd.ui.Say("Updating service auth token as %s...", terminal.EntityNameColor(cmd.config.Username())) - - serviceAuthToken, apiResponse := cmd.authTokenRepo.FindByLabelAndProvider(c.Args()[0], c.Args()[1]) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - serviceAuthToken.Token = c.Args()[2] - - apiResponse = cmd.authTokenRepo.Update(serviceAuthToken) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/serviceauthtoken/update_service_auth_token_test.go b/src/cf/commands/serviceauthtoken/update_service_auth_token_test.go deleted file mode 100644 index c6554b9a2f3..00000000000 --- a/src/cf/commands/serviceauthtoken/update_service_auth_token_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package serviceauthtoken_test - -import ( - "cf" - . "cf/commands/serviceauthtoken" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUpdateServiceAuthTokenFailsWithUsage(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - - ui := callUpdateServiceAuthToken(t, []string{}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceAuthToken(t, []string{"MY-TOKEN-LABEL"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceAuthToken(t, []string{"MY-TOKEN-LABEL", "my-token-abc123"}, reqFactory, authTokenRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceAuthToken(t, []string{"MY-TOKEN-LABEL", "my-provider", "my-token-abc123"}, reqFactory, authTokenRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestUpdateServiceAuthTokenRequirements(t *testing.T) { - authTokenRepo := &testapi.FakeAuthTokenRepo{} - reqFactory := &testreq.FakeReqFactory{} - args := []string{"MY-TOKEN-LABLE", "my-provider", "my-token-abc123"} - - reqFactory.LoginSuccess = true - callUpdateServiceAuthToken(t, args, reqFactory, authTokenRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = false - callUpdateServiceAuthToken(t, args, reqFactory, authTokenRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestUpdateServiceAuthToken(t *testing.T) { - foundAuthToken := cf.ServiceAuthTokenFields{} - foundAuthToken.Guid = "found-auth-token-guid" - foundAuthToken.Label = "found label" - foundAuthToken.Provider = "found provider" - - authTokenRepo := &testapi.FakeAuthTokenRepo{FindByLabelAndProviderServiceAuthTokenFields: foundAuthToken} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - args := []string{"a label", "a provider", "a value"} - - ui := callUpdateServiceAuthToken(t, args, reqFactory, authTokenRepo) - expectedAuthToken := cf.ServiceAuthTokenFields{} - expectedAuthToken.Guid = "found-auth-token-guid" - expectedAuthToken.Label = "found label" - expectedAuthToken.Provider = "found provider" - expectedAuthToken.Token = "a value" - - assert.Contains(t, ui.Outputs[0], "Updating service auth token as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - - assert.Equal(t, authTokenRepo.FindByLabelAndProviderLabel, "a label") - assert.Equal(t, authTokenRepo.FindByLabelAndProviderProvider, "a provider") - assert.Equal(t, authTokenRepo.UpdatedServiceAuthTokenFields, expectedAuthToken) - assert.Equal(t, authTokenRepo.UpdatedServiceAuthTokenFields, expectedAuthToken) -} - -func callUpdateServiceAuthToken(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, authTokenRepo *testapi.FakeAuthTokenRepo) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewUpdateServiceAuthToken(ui, config, authTokenRepo) - ctxt := testcmd.NewContext("update-service-auth-token", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/servicebroker/create_service_broker.go b/src/cf/commands/servicebroker/create_service_broker.go deleted file mode 100644 index a95ee1265c4..00000000000 --- a/src/cf/commands/servicebroker/create_service_broker.go +++ /dev/null @@ -1,56 +0,0 @@ -package servicebroker - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateServiceBroker struct { - ui terminal.UI - config *configuration.Configuration - serviceBrokerRepo api.ServiceBrokerRepository -} - -func NewCreateServiceBroker(ui terminal.UI, config *configuration.Configuration, serviceBrokerRepo api.ServiceBrokerRepository) (cmd CreateServiceBroker) { - cmd.ui = ui - cmd.config = config - cmd.serviceBrokerRepo = serviceBrokerRepo - return -} - -func (cmd CreateServiceBroker) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - - if len(c.Args()) != 4 { - err = errors.New("Incorrect usage") - cmd.ui.FailWithUsage(c, "create-service-broker") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} - -func (cmd CreateServiceBroker) Run(c *cli.Context) { - name := c.Args()[0] - username := c.Args()[1] - password := c.Args()[2] - url := c.Args()[3] - - cmd.ui.Say("Creating service broker %s as %s...", - terminal.EntityNameColor(name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.serviceBrokerRepo.Create(name, url, username, password) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/servicebroker/create_service_broker_test.go b/src/cf/commands/servicebroker/create_service_broker_test.go deleted file mode 100644 index 101f60bc4e8..00000000000 --- a/src/cf/commands/servicebroker/create_service_broker_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package servicebroker_test - -import ( - "cf" - . "cf/commands/servicebroker" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateServiceBrokerFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - serviceBrokerRepo := &testapi.FakeServiceBrokerRepo{} - - ui := callCreateServiceBroker(t, []string{}, reqFactory, serviceBrokerRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceBroker(t, []string{"1arg"}, reqFactory, serviceBrokerRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceBroker(t, []string{"1arg", "2arg"}, reqFactory, serviceBrokerRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceBroker(t, []string{"1arg", "2arg", "3arg"}, reqFactory, serviceBrokerRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callCreateServiceBroker(t, []string{"1arg", "2arg", "3arg", "4arg"}, reqFactory, serviceBrokerRepo) - assert.False(t, ui.FailedWithUsage) - -} -func TestCreateServiceBrokerRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - serviceBrokerRepo := &testapi.FakeServiceBrokerRepo{} - args := []string{"1arg", "2arg", "3arg", "4arg"} - - reqFactory.LoginSuccess = false - callCreateServiceBroker(t, args, reqFactory, serviceBrokerRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callCreateServiceBroker(t, args, reqFactory, serviceBrokerRepo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestCreateServiceBroker(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - serviceBrokerRepo := &testapi.FakeServiceBrokerRepo{} - args := []string{"my-broker", "my username", "my password", "http://example.com"} - ui := callCreateServiceBroker(t, args, reqFactory, serviceBrokerRepo) - - assert.Contains(t, ui.Outputs[0], "Creating service broker ") - assert.Contains(t, ui.Outputs[0], "my-broker") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, serviceBrokerRepo.CreateName, "my-broker") - assert.Equal(t, serviceBrokerRepo.CreateUrl, "http://example.com") - assert.Equal(t, serviceBrokerRepo.CreateUsername, "my username") - assert.Equal(t, serviceBrokerRepo.CreatePassword, "my password") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callCreateServiceBroker(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, serviceBrokerRepo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("create-service-broker", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateServiceBroker(ui, config, serviceBrokerRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/servicebroker/delete_service_broker.go b/src/cf/commands/servicebroker/delete_service_broker.go deleted file mode 100644 index 4e62e99f30f..00000000000 --- a/src/cf/commands/servicebroker/delete_service_broker.go +++ /dev/null @@ -1,77 +0,0 @@ -package servicebroker - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteServiceBroker struct { - ui terminal.UI - config *configuration.Configuration - repo api.ServiceBrokerRepository -} - -func NewDeleteServiceBroker(ui terminal.UI, config *configuration.Configuration, repo api.ServiceBrokerRepository) (cmd DeleteServiceBroker) { - cmd.ui = ui - cmd.config = config - cmd.repo = repo - return -} - -func (cmd DeleteServiceBroker) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-service-broker") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} -func (cmd DeleteServiceBroker) Run(c *cli.Context) { - brokerName := c.Args()[0] - force := c.Bool("f") - - if !force { - response := cmd.ui.Confirm( - "Really delete %s?%s", - terminal.EntityNameColor(brokerName), - terminal.PromptColor(">"), - ) - if !response { - return - } - } - - cmd.ui.Say("Deleting service broker %s as %s...", - terminal.EntityNameColor(brokerName), - terminal.EntityNameColor(cmd.config.Username()), - ) - - broker, apiResponse := cmd.repo.FindByName(brokerName) - - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("Service Broker %s does not exist.", brokerName) - return - } - - apiResponse = cmd.repo.Delete(broker.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - return -} diff --git a/src/cf/commands/servicebroker/delete_service_broker_test.go b/src/cf/commands/servicebroker/delete_service_broker_test.go deleted file mode 100644 index 0aaff98279a..00000000000 --- a/src/cf/commands/servicebroker/delete_service_broker_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package servicebroker_test - -import ( - "cf" - . "cf/commands/servicebroker" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteServiceBrokerFailsWithUsage(t *testing.T) { - ui, _, _ := deleteServiceBroker(t, "y", []string{}) - assert.True(t, ui.FailedWithUsage) - - ui, _, _ = deleteServiceBroker(t, "y", []string{"my-broker"}) - assert.False(t, ui.FailedWithUsage) -} - -func TestDeleteServiceBrokerRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - repo := &testapi.FakeServiceBrokerRepo{} - - reqFactory.LoginSuccess = false - callDeleteServiceBroker(t, []string{"-f", "my-broker"}, reqFactory, repo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callDeleteServiceBroker(t, []string{"-f", "my-broker"}, reqFactory, repo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteConfirmingWithY(t *testing.T) { - ui, _, repo := deleteServiceBroker(t, "y", []string{"service-broker-to-delete"}) - - assert.Equal(t, repo.FindByNameName, "service-broker-to-delete") - assert.Equal(t, repo.DeletedServiceBrokerGuid, "service-broker-to-delete-guid") - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[0], "Deleting service broker") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteConfirmingWithYes(t *testing.T) { - ui, _, repo := deleteServiceBroker(t, "Yes", []string{"service-broker-to-delete"}) - - assert.Equal(t, repo.FindByNameName, "service-broker-to-delete") - assert.Equal(t, repo.DeletedServiceBrokerGuid, "service-broker-to-delete-guid") - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[0], "Deleting service broker") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteWithForceOption(t *testing.T) { - serviceBroker := cf.ServiceBroker{} - serviceBroker.Name = "service-broker-to-delete" - serviceBroker.Guid = "service-broker-to-delete-guid" - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo := &testapi.FakeServiceBrokerRepo{FindByNameServiceBroker: serviceBroker} - ui := callDeleteServiceBroker(t, []string{"-f", "service-broker-to-delete"}, reqFactory, repo) - - assert.Equal(t, repo.FindByNameName, "service-broker-to-delete") - assert.Equal(t, repo.DeletedServiceBrokerGuid, "service-broker-to-delete-guid") - assert.Equal(t, len(ui.Prompts), 0) - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Outputs[0], "Deleting service broker") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteAppThatDoesNotExist(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - repo := &testapi.FakeServiceBrokerRepo{FindByNameNotFound: true} - ui := callDeleteServiceBroker(t, []string{"-f", "service-broker-to-delete"}, reqFactory, repo) - - assert.Equal(t, repo.FindByNameName, "service-broker-to-delete") - assert.Equal(t, repo.DeletedServiceBrokerGuid, "") - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "service-broker-to-delete") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func callDeleteServiceBroker(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, repo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete-service-broker", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteServiceBroker(ui, config, repo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} - -func deleteServiceBroker(t *testing.T, confirmation string, args []string) (ui *testterm.FakeUI, reqFactory *testreq.FakeReqFactory, repo *testapi.FakeServiceBrokerRepo) { - serviceBroker := cf.ServiceBroker{} - serviceBroker.Name = "service-broker-to-delete" - serviceBroker.Guid = "service-broker-to-delete-guid" - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true} - repo = &testapi.FakeServiceBrokerRepo{FindByNameServiceBroker: serviceBroker} - ui = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space2 := cf.SpaceFields{} - space2.Name = "my-space" - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - } - - ctxt := testcmd.NewContext("delete-service-broker", args) - cmd := NewDeleteServiceBroker(ui, config, repo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/servicebroker/list_service_brokers.go b/src/cf/commands/servicebroker/list_service_brokers.go deleted file mode 100644 index 178a22bf130..00000000000 --- a/src/cf/commands/servicebroker/list_service_brokers.go +++ /dev/null @@ -1,60 +0,0 @@ -package servicebroker - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListServiceBrokers struct { - ui terminal.UI - config *configuration.Configuration - repo api.ServiceBrokerRepository -} - -func NewListServiceBrokers(ui terminal.UI, config *configuration.Configuration, repo api.ServiceBrokerRepository) (cmd ListServiceBrokers) { - cmd.ui = ui - cmd.config = config - cmd.repo = repo - return -} - -func (cmd ListServiceBrokers) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd ListServiceBrokers) Run(c *cli.Context) { - cmd.ui.Say("Getting service brokers as %s...\n", terminal.EntityNameColor(cmd.config.Username())) - - stopChan := make(chan bool) - defer close(stopChan) - - serviceBrokersChan, statusChan := cmd.repo.ListServiceBrokers(stopChan) - - table := cmd.ui.Table([]string{"name", "url"}) - noServiceBrokers := true - - for serviceBrokers := range serviceBrokersChan { - rows := [][]string{} - for _, serviceBroker := range serviceBrokers { - rows = append(rows, []string{ - serviceBroker.Name, - serviceBroker.Url, - }) - } - table.Print(rows) - noServiceBrokers = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching service brokers.\n%s", apiStatus.Message) - return - } - - if noServiceBrokers { - cmd.ui.Say("No service brokers found") - } -} diff --git a/src/cf/commands/servicebroker/list_service_brokers_test.go b/src/cf/commands/servicebroker/list_service_brokers_test.go deleted file mode 100644 index 8d62768aa7d..00000000000 --- a/src/cf/commands/servicebroker/list_service_brokers_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package servicebroker_test - -import ( - "cf" - . "cf/commands/servicebroker" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestListServiceBrokers(t *testing.T) { - broker := cf.ServiceBroker{} - broker.Name = "service-broker-to-list-a" - broker.Guid = "service-broker-to-list-guid-a" - broker.Url = "http://service-a-url.com" - broker2 := cf.ServiceBroker{} - broker2.Name = "service-broker-to-list-b" - broker2.Guid = "service-broker-to-list-guid-b" - broker2.Url = "http://service-b-url.com" - broker3 := cf.ServiceBroker{} - broker3.Name = "service-broker-to-list-c" - broker3.Guid = "service-broker-to-list-guid-c" - broker3.Url = "http://service-c-url.com" - serviceBrokers := []cf.ServiceBroker{broker, broker2, broker3} - - repo := &testapi.FakeServiceBrokerRepo{ - ServiceBrokers: serviceBrokers, - } - - ui := callListServiceBrokers(t, []string{}, repo) - - assert.Contains(t, ui.Outputs[0], "Getting service brokers as") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Contains(t, ui.Outputs[1], "name") - assert.Contains(t, ui.Outputs[1], "url") - - assert.Contains(t, ui.Outputs[2], "service-broker-to-list-a") - assert.Contains(t, ui.Outputs[2], "http://service-a-url.com") - - assert.Contains(t, ui.Outputs[3], "service-broker-to-list-b") - assert.Contains(t, ui.Outputs[3], "http://service-b-url.com") - - assert.Contains(t, ui.Outputs[4], "service-broker-to-list-c") - assert.Contains(t, ui.Outputs[4], "http://service-c-url.com") -} - -func TestListingServiceBrokersWhenNoneExist(t *testing.T) { - repo := &testapi.FakeServiceBrokerRepo{ - ServiceBrokers: []cf.ServiceBroker{}, - } - - ui := callListServiceBrokers(t, []string{}, repo) - - assert.Contains(t, ui.Outputs[0], "Getting service brokers as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "No service brokers found") -} - -func TestListingServiceBrokersWhenFindFails(t *testing.T) { - repo := &testapi.FakeServiceBrokerRepo{ListErr: true} - - ui := callListServiceBrokers(t, []string{}, repo) - - assert.Contains(t, ui.Outputs[0], "Getting service brokers as") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "FAILED") -} - -func callListServiceBrokers(t *testing.T, args []string, serviceBrokerRepo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - ctxt := testcmd.NewContext("service-brokers", args) - cmd := NewListServiceBrokers(ui, config, serviceBrokerRepo) - testcmd.RunCommand(cmd, ctxt, &testreq.FakeReqFactory{}) - - return -} diff --git a/src/cf/commands/servicebroker/rename_service_broker.go b/src/cf/commands/servicebroker/rename_service_broker.go deleted file mode 100644 index 42a6030c8d1..00000000000 --- a/src/cf/commands/servicebroker/rename_service_broker.go +++ /dev/null @@ -1,60 +0,0 @@ -package servicebroker - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RenameServiceBroker struct { - ui terminal.UI - config *configuration.Configuration - repo api.ServiceBrokerRepository -} - -func NewRenameServiceBroker(ui terminal.UI, config *configuration.Configuration, repo api.ServiceBrokerRepository) (cmd RenameServiceBroker) { - cmd.ui = ui - cmd.config = config - cmd.repo = repo - return -} - -func (cmd RenameServiceBroker) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "rename-service-broker") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} - -func (cmd RenameServiceBroker) Run(c *cli.Context) { - serviceBroker, apiResponse := cmd.repo.FindByName(c.Args()[0]) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Say("Renaming service broker %s to %s as %s", - terminal.EntityNameColor(serviceBroker.Name), - terminal.EntityNameColor(c.Args()[1]), - terminal.EntityNameColor(cmd.config.Username()), - ) - - newName := c.Args()[1] - - apiResponse = cmd.repo.Rename(serviceBroker.Guid, newName) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/servicebroker/rename_service_broker_test.go b/src/cf/commands/servicebroker/rename_service_broker_test.go deleted file mode 100644 index 063a87fddc0..00000000000 --- a/src/cf/commands/servicebroker/rename_service_broker_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package servicebroker_test - -import ( - "cf" - . "cf/commands/servicebroker" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRenameServiceBrokerFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - repo := &testapi.FakeServiceBrokerRepo{} - - ui := callRenameServiceBroker(t, []string{}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callRenameServiceBroker(t, []string{"arg1"}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callRenameServiceBroker(t, []string{"arg1", "arg2"}, reqFactory, repo) - assert.False(t, ui.FailedWithUsage) -} - -func TestRenameServiceBrokerRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - repo := &testapi.FakeServiceBrokerRepo{} - args := []string{"arg1", "arg2"} - - reqFactory.LoginSuccess = false - callRenameServiceBroker(t, args, reqFactory, repo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callRenameServiceBroker(t, args, reqFactory, repo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestRenameServiceBroker(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - broker := cf.ServiceBroker{} - broker.Name = "my-found-broker" - broker.Guid = "my-found-broker-guid" - repo := &testapi.FakeServiceBrokerRepo{ - FindByNameServiceBroker: broker, - } - args := []string{"my-broker", "my-new-broker"} - - ui := callRenameServiceBroker(t, args, reqFactory, repo) - - assert.Equal(t, repo.FindByNameName, "my-broker") - - assert.Contains(t, ui.Outputs[0], "Renaming service broker") - assert.Contains(t, ui.Outputs[0], "my-found-broker") - assert.Contains(t, ui.Outputs[0], "my-new-broker") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, repo.RenamedServiceBrokerGuid, "my-found-broker-guid") - assert.Equal(t, repo.RenamedServiceBrokerName, "my-new-broker") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callRenameServiceBroker(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, repo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewRenameServiceBroker(ui, config, repo) - ctxt := testcmd.NewContext("rename-service-broker", args) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/servicebroker/update_service_broker.go b/src/cf/commands/servicebroker/update_service_broker.go deleted file mode 100644 index 0991112c2e9..00000000000 --- a/src/cf/commands/servicebroker/update_service_broker.go +++ /dev/null @@ -1,61 +0,0 @@ -package servicebroker - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UpdateServiceBroker struct { - ui terminal.UI - config *configuration.Configuration - repo api.ServiceBrokerRepository -} - -func NewUpdateServiceBroker(ui terminal.UI, config *configuration.Configuration, repo api.ServiceBrokerRepository) (cmd UpdateServiceBroker) { - cmd.ui = ui - cmd.config = config - cmd.repo = repo - return -} - -func (cmd UpdateServiceBroker) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 4 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "update-service-broker") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} - -func (cmd UpdateServiceBroker) Run(c *cli.Context) { - serviceBroker, apiResponse := cmd.repo.FindByName(c.Args()[0]) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Say("Updating service broker %s as %s...", - terminal.EntityNameColor(serviceBroker.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - serviceBroker.Username = c.Args()[1] - serviceBroker.Password = c.Args()[2] - serviceBroker.Url = c.Args()[3] - - apiResponse = cmd.repo.Update(serviceBroker) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/servicebroker/update_service_broker_test.go b/src/cf/commands/servicebroker/update_service_broker_test.go deleted file mode 100644 index 5037a124ec9..00000000000 --- a/src/cf/commands/servicebroker/update_service_broker_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package servicebroker_test - -import ( - "cf" - . "cf/commands/servicebroker" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUpdateServiceBrokerFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - repo := &testapi.FakeServiceBrokerRepo{} - - ui := callUpdateServiceBroker(t, []string{}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceBroker(t, []string{"arg1"}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceBroker(t, []string{"arg1", "arg2"}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceBroker(t, []string{"arg1", "arg2", "arg3"}, reqFactory, repo) - assert.True(t, ui.FailedWithUsage) - - ui = callUpdateServiceBroker(t, []string{"arg1", "arg2", "arg3", "arg4"}, reqFactory, repo) - assert.False(t, ui.FailedWithUsage) -} - -func TestUpdateServiceBrokerRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - repo := &testapi.FakeServiceBrokerRepo{} - args := []string{"arg1", "arg2", "arg3", "arg4"} - - reqFactory.LoginSuccess = false - callUpdateServiceBroker(t, args, reqFactory, repo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callUpdateServiceBroker(t, args, reqFactory, repo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestUpdateServiceBroker(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - broker := cf.ServiceBroker{} - broker.Name = "my-found-broker" - broker.Guid = "my-found-broker-guid" - repo := &testapi.FakeServiceBrokerRepo{ - FindByNameServiceBroker: broker, - } - args := []string{"my-broker", "new-username", "new-password", "new-url"} - - ui := callUpdateServiceBroker(t, args, reqFactory, repo) - - assert.Equal(t, repo.FindByNameName, "my-broker") - - assert.Contains(t, ui.Outputs[0], "Updating service broker") - assert.Contains(t, ui.Outputs[0], "my-found-broker") - assert.Contains(t, ui.Outputs[0], "my-user") - expectedServiceBroker := cf.ServiceBroker{} - expectedServiceBroker.Name = "my-found-broker" - expectedServiceBroker.Username = "new-username" - expectedServiceBroker.Password = "new-password" - expectedServiceBroker.Url = "new-url" - expectedServiceBroker.Guid = "my-found-broker-guid" - - assert.Equal(t, repo.UpdatedServiceBroker, expectedServiceBroker) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callUpdateServiceBroker(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, repo *testapi.FakeServiceBrokerRepo) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewUpdateServiceBroker(ui, config, repo) - ctxt := testcmd.NewContext("update-service-broker", args) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - return -} diff --git a/src/cf/commands/space/create_space.go b/src/cf/commands/space/create_space.go deleted file mode 100644 index 840fabb4cef..00000000000 --- a/src/cf/commands/space/create_space.go +++ /dev/null @@ -1,61 +0,0 @@ -package space - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateSpace struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository -} - -func NewCreateSpace(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository) (cmd CreateSpace) { - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - return -} - -func (cmd CreateSpace) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) == 0 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-space") - return - } - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - } - return -} - -func (cmd CreateSpace) Run(c *cli.Context) { - spaceName := c.Args()[0] - cmd.ui.Say("Creating space %s in org %s as %s...", - terminal.EntityNameColor(spaceName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.spaceRepo.Create(spaceName) - if apiResponse.IsNotSuccessful() { - if apiResponse.ErrorCode == cf.SPACE_EXISTS { - cmd.ui.Ok() - cmd.ui.Warn("Space %s already exists", spaceName) - return - } - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("\nTIP: Use '%s' to target new space", terminal.CommandColor(cf.Name()+" target -s "+spaceName)) -} diff --git a/src/cf/commands/space/create_space_test.go b/src/cf/commands/space/create_space_test.go deleted file mode 100644 index 14dba2bcf7c..00000000000 --- a/src/cf/commands/space/create_space_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package space_test - -import ( - "cf" - . "cf/commands/space" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestCreateSpaceFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - spaceRepo := &testapi.FakeSpaceRepository{} - - fakeUI := callCreateSpace(t, []string{}, reqFactory, spaceRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestCreateSpaceRequirements(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - -} - -func TestCreateSpace(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - fakeUI := callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating space") - assert.Contains(t, fakeUI.Outputs[0], "my-space") - assert.Contains(t, fakeUI.Outputs[0], "my-org") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Equal(t, spaceRepo.CreateSpaceName, "my-space") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "TIP") -} - -func TestCreateSpaceWhenItAlreadyExists(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{CreateSpaceExists: true} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - fakeUI := callCreateSpace(t, []string{"my-space"}, reqFactory, spaceRepo) - - assert.Equal(t, len(fakeUI.Outputs), 3) - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "my-space") - assert.Contains(t, fakeUI.Outputs[2], "already exists") -} - -func callCreateSpace(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-space", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateSpace(ui, config, spaceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/space/delete_space.go b/src/cf/commands/space/delete_space.go deleted file mode 100644 index 557b1ec8051..00000000000 --- a/src/cf/commands/space/delete_space.go +++ /dev/null @@ -1,90 +0,0 @@ -package space - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteSpace struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository - configRepo configuration.ConfigurationRepository - spaceReq requirements.SpaceRequirement -} - -func NewDeleteSpace(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository, configRepo configuration.ConfigurationRepository) (cmd *DeleteSpace) { - cmd = new(DeleteSpace) - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - cmd.configRepo = configRepo - return -} - -func (cmd *DeleteSpace) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "delete-space") - return - } - - cmd.spaceReq = reqFactory.NewSpaceRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - cmd.spaceReq, - } - return -} - -func (cmd *DeleteSpace) Run(c *cli.Context) { - spaceName := c.Args()[0] - force := c.Bool("f") - - cmd.ui.Say("Deleting space %s in org %s as %s...", - terminal.EntityNameColor(spaceName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - space := cmd.spaceReq.GetSpace() - - if !force { - response := cmd.ui.Confirm( - "Really delete space %s and everything associated with it?%s", - terminal.EntityNameColor(spaceName), - terminal.PromptColor(">"), - ) - if !response { - return - } - } - - apiResponse := cmd.spaceRepo.Delete(space.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - - config, err := cmd.configRepo.Get() - if err != nil { - cmd.ui.ConfigFailure(err) - return - } - - if config.SpaceFields.Name == spaceName { - config.SpaceFields = cf.SpaceFields{} - cmd.configRepo.Save() - cmd.ui.Say("TIP: No space targeted, use '%s target -s' to target a space", cf.Name()) - } - - return -} diff --git a/src/cf/commands/space/delete_space_test.go b/src/cf/commands/space/delete_space_test.go deleted file mode 100644 index 422241196a4..00000000000 --- a/src/cf/commands/space/delete_space_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package space_test - -import ( - "cf" - . "cf/commands/space" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func defaultDeleteSpaceSpace() cf.Space { - space := cf.Space{} - space.Name = "space-to-delete" - space.Guid = "space-to-delete-guid" - return space -} -func defaultDeleteSpaceReqFactory() *testreq.FakeReqFactory { - return &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true, Space: defaultDeleteSpaceSpace()} -} - -func TestDeleteSpaceRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - deleteSpace(t, []string{"y"}, []string{"my-space"}, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - deleteSpace(t, []string{"y"}, []string{"my-space"}, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - deleteSpace(t, []string{"y"}, []string{"my-space"}, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.SpaceName, "my-space") -} - -func TestDeleteSpaceConfirmingWithY(t *testing.T) { - ui, spaceRepo := deleteSpace(t, []string{"y"}, []string{"space-to-delete"}, defaultDeleteSpaceReqFactory()) - - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Contains(t, ui.Outputs[0], "Deleting space ") - assert.Contains(t, ui.Outputs[0], "space-to-delete") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, spaceRepo.DeletedSpaceGuid, "space-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteSpaceConfirmingWithYes(t *testing.T) { - ui, spaceRepo := deleteSpace(t, []string{"Yes"}, []string{"space-to-delete"}, defaultDeleteSpaceReqFactory()) - - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Contains(t, ui.Outputs[0], "Deleting space ") - assert.Contains(t, ui.Outputs[0], "space-to-delete") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, spaceRepo.DeletedSpaceGuid, "space-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteSpaceWithForceOption(t *testing.T) { - ui, spaceRepo := deleteSpace(t, []string{}, []string{"-f", "space-to-delete"}, defaultDeleteSpaceReqFactory()) - - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting") - assert.Contains(t, ui.Outputs[0], "space-to-delete") - assert.Equal(t, spaceRepo.DeletedSpaceGuid, "space-to-delete-guid") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteSpaceWhenSpaceIsTargeted(t *testing.T) { - reqFactory := defaultDeleteSpaceReqFactory() - spaceRepo := &testapi.FakeSpaceRepository{} - configRepo := &testconfig.FakeConfigRepository{} - - config, _ := configRepo.Get() - config.SpaceFields = defaultDeleteSpaceSpace().SpaceFields - configRepo.Save() - - ui := &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete", []string{"-f", "space-to-delete"}) - - cmd := NewDeleteSpace(ui, config, spaceRepo, configRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - config, _ = configRepo.Get() - assert.Equal(t, config.HasSpace(), false) -} - -func TestDeleteSpaceWhenSpaceNotTargeted(t *testing.T) { - reqFactory := defaultDeleteSpaceReqFactory() - spaceRepo := &testapi.FakeSpaceRepository{} - configRepo := &testconfig.FakeConfigRepository{} - - config, _ := configRepo.Get() - otherSpace := cf.SpaceFields{} - otherSpace.Name = "do-not-delete" - otherSpace.Guid = "do-not-delete-guid" - config.SpaceFields = otherSpace - configRepo.Save() - - ui := &testterm.FakeUI{} - ctxt := testcmd.NewContext("delete", []string{"-f", "space-to-delete"}) - - cmd := NewDeleteSpace(ui, config, spaceRepo, configRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - - config, _ = configRepo.Get() - assert.Equal(t, config.HasSpace(), true) -} - -func TestDeleteSpaceCommandWith(t *testing.T) { - ui, _ := deleteSpace(t, []string{"Yes"}, []string{}, defaultDeleteSpaceReqFactory()) - assert.True(t, ui.FailedWithUsage) - - ui, _ = deleteSpace(t, []string{"Yes"}, []string{"space-to-delete"}, defaultDeleteSpaceReqFactory()) - assert.False(t, ui.FailedWithUsage) -} - -func deleteSpace(t *testing.T, inputs []string, args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI, spaceRepo *testapi.FakeSpaceRepository) { - spaceRepo = &testapi.FakeSpaceRepository{} - configRepo := &testconfig.FakeConfigRepository{} - - ui = &testterm.FakeUI{ - Inputs: inputs, - } - ctxt := testcmd.NewContext("delete-space", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - space := cf.SpaceFields{} - space.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteSpace(ui, config, spaceRepo, configRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/space/list_spaces.go b/src/cf/commands/space/list_spaces.go deleted file mode 100644 index 6b6a9fa645e..00000000000 --- a/src/cf/commands/space/list_spaces.go +++ /dev/null @@ -1,63 +0,0 @@ -package space - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type ListSpaces struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository -} - -func NewListSpaces(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository) (cmd ListSpaces) { - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - return -} - -func (cmd ListSpaces) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - } - return -} - -func (cmd ListSpaces) Run(c *cli.Context) { - cmd.ui.Say("Getting spaces in org %s as %s...\n", - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.Username())) - - stopChan := make(chan bool) - defer close(stopChan) - - spacesChan, statusChan := cmd.spaceRepo.ListSpaces(stopChan) - - table := cmd.ui.Table([]string{"name"}) - noSpaces := true - - for spaces := range spacesChan { - rows := [][]string{} - for _, space := range spaces { - rows = append(rows, []string{space.Name}) - } - table.Print(rows) - noSpaces = false - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching spaces.\n%s", apiStatus.Message) - return - } - - if noSpaces { - cmd.ui.Say("No spaces found") - } -} diff --git a/src/cf/commands/space/list_spaces_test.go b/src/cf/commands/space/list_spaces_test.go deleted file mode 100644 index 810970014c4..00000000000 --- a/src/cf/commands/space/list_spaces_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package space_test - -import ( - "cf" - "cf/api" - . "cf/commands/space" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSpacesRequirements(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{} - config := &configuration.Configuration{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - callSpaces([]string{}, reqFactory, config, spaceRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callSpaces([]string{}, reqFactory, config, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callSpaces([]string{}, reqFactory, config, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) -} - -func TestListingSpaces(t *testing.T) { - space := cf.Space{} - space.Name = "space1" - space2 := cf.Space{} - space2.Name = "space2" - space3 := cf.Space{} - space3.Name = "space3" - spaceRepo := &testapi.FakeSpaceRepository{ - Spaces: []cf.Space{space, space2, space3}, - } - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - OrganizationFields: org, - AccessToken: token, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - ui := callSpaces([]string{}, reqFactory, config, spaceRepo) - assert.Contains(t, ui.Outputs[0], "Getting spaces in org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[2], "space1") - assert.Contains(t, ui.Outputs[3], "space2") - assert.Contains(t, ui.Outputs[4], "space3") -} - -func TestListingSpacesWhenNoSpaces(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{ - Spaces: []cf.Space{}, - } - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - - assert.NoError(t, err) - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - config := &configuration.Configuration{ - OrganizationFields: org2, - AccessToken: token, - } - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - ui := callSpaces([]string{}, reqFactory, config, spaceRepo) - assert.Contains(t, ui.Outputs[0], "Getting spaces in org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "No spaces found") -} - -func callSpaces(args []string, reqFactory *testreq.FakeReqFactory, config *configuration.Configuration, spaceRepo api.SpaceRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("spaces", args) - - cmd := NewListSpaces(ui, config, spaceRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/space/rename_space.go b/src/cf/commands/space/rename_space.go deleted file mode 100644 index 8a0550b76a7..00000000000 --- a/src/cf/commands/space/rename_space.go +++ /dev/null @@ -1,66 +0,0 @@ -package space - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type RenameSpace struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository - spaceReq requirements.SpaceRequirement - configRepo configuration.ConfigurationRepository -} - -func NewRenameSpace(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository, configRepo configuration.ConfigurationRepository) (cmd *RenameSpace) { - cmd = new(RenameSpace) - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - cmd.configRepo = configRepo - return -} - -func (cmd *RenameSpace) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "rename-space") - return - } - cmd.spaceReq = reqFactory.NewSpaceRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - cmd.spaceReq, - } - return -} - -func (cmd *RenameSpace) Run(c *cli.Context) { - space := cmd.spaceReq.GetSpace() - newName := c.Args()[1] - cmd.ui.Say("Renaming space %s to %s in org %s as %s...", - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(newName), - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.spaceRepo.Rename(space.Guid, newName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - if cmd.config.SpaceFields.Guid == space.Guid { - cmd.config.SpaceFields.Name = newName - cmd.configRepo.Save() - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/space/rename_space_test.go b/src/cf/commands/space/rename_space_test.go deleted file mode 100644 index 2d6044eb105..00000000000 --- a/src/cf/commands/space/rename_space_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package space_test - -import ( - "cf" - . "cf/commands/space" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestRenameSpaceFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - spaceRepo := &testapi.FakeSpaceRepository{} - - fakeUI := callRenameSpace(t, []string{}, reqFactory, spaceRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callRenameSpace(t, []string{"foo"}, reqFactory, spaceRepo) - assert.True(t, fakeUI.FailedWithUsage) -} - -func TestRenameSpaceRequirements(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callRenameSpace(t, []string{"my-space", "my-new-space"}, reqFactory, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callRenameSpace(t, []string{"my-space", "my-new-space"}, reqFactory, spaceRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - callRenameSpace(t, []string{"my-space", "my-new-space"}, reqFactory, spaceRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - assert.Equal(t, reqFactory.SpaceName, "my-space") -} - -func TestRenameSpaceRun(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{} - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true, Space: space} - ui := callRenameSpace(t, []string{"my-space", "my-new-space"}, reqFactory, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "Renaming space") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-new-space") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Equal(t, spaceRepo.RenameSpaceGuid, "my-space-guid") - assert.Equal(t, spaceRepo.RenameNewName, "my-new-space") - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callRenameSpace(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-space", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - space2 := cf.SpaceFields{} - space2.Name = "my-space" - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewRenameSpace(ui, config, spaceRepo, testconfig.FakeConfigRepository{}) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/space/show_space.go b/src/cf/commands/space/show_space.go deleted file mode 100644 index 5e757283ad2..00000000000 --- a/src/cf/commands/space/show_space.go +++ /dev/null @@ -1,69 +0,0 @@ -package space - -import ( - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" - "strings" -) - -type ShowSpace struct { - ui terminal.UI - config *configuration.Configuration - spaceReq requirements.SpaceRequirement -} - -func NewShowSpace(ui terminal.UI, config *configuration.Configuration) (cmd *ShowSpace) { - cmd = new(ShowSpace) - cmd.ui = ui - cmd.config = config - return -} - -func (cmd *ShowSpace) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "space") - return - } - - cmd.spaceReq = reqFactory.NewSpaceRequirement(c.Args()[0]) - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - reqFactory.NewTargetedOrgRequirement(), - cmd.spaceReq, - } - return -} - -func (cmd *ShowSpace) Run(c *cli.Context) { - space := cmd.spaceReq.GetSpace() - cmd.ui.Say("Getting info for space %s in org %s as %s...", - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(space.Organization.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - cmd.ui.Ok() - cmd.ui.Say("\n%s:", terminal.EntityNameColor(space.Name)) - cmd.ui.Say(" Org: %s", terminal.EntityNameColor(space.Organization.Name)) - - apps := []string{} - for _, app := range space.Applications { - apps = append(apps, app.Name) - } - cmd.ui.Say(" Apps: %s", terminal.EntityNameColor(strings.Join(apps, ", "))) - - domains := []string{} - for _, domain := range space.Domains { - domains = append(domains, domain.Name) - } - cmd.ui.Say(" Domains: %s", terminal.EntityNameColor(strings.Join(domains, ", "))) - - services := []string{} - for _, service := range space.ServiceInstances { - services = append(services, service.Name) - } - cmd.ui.Say(" Services: %s", terminal.EntityNameColor(strings.Join(services, ", "))) -} diff --git a/src/cf/commands/space/show_space_test.go b/src/cf/commands/space/show_space_test.go deleted file mode 100644 index 8e982539201..00000000000 --- a/src/cf/commands/space/show_space_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package space_test - -import ( - "cf" - . "cf/commands/space" - "cf/configuration" - "github.com/stretchr/testify/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestShowSpaceRequirements(t *testing.T) { - args := []string{"my-space"} - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: false, TargetedOrgSuccess: true} - callShowSpace(t, args, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: false} - callShowSpace(t, args, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true} - callShowSpace(t, args, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestShowSpaceInfoSuccess(t *testing.T) { - org := cf.OrganizationFields{} - org.Name = "my-org" - - app := cf.ApplicationFields{} - app.Name = "app1" - app.Guid = "app1-guid" - apps := []cf.ApplicationFields{app} - - domain := cf.DomainFields{} - domain.Name = "domain1" - domain.Guid = "domain1-guid" - domains := []cf.DomainFields{domain} - - serviceInstance := cf.ServiceInstanceFields{} - serviceInstance.Name = "service1" - serviceInstance.Guid = "service1-guid" - services := []cf.ServiceInstanceFields{serviceInstance} - - space := cf.Space{} - space.Name = "space1" - space.Organization = org - space.Applications = apps - space.Domains = domains - space.ServiceInstances = services - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, TargetedOrgSuccess: true, Space: space} - ui := callShowSpace(t, []string{"space1"}, reqFactory) - assert.Contains(t, ui.Outputs[0], "Getting info for space") - assert.Contains(t, ui.Outputs[0], "space1") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "space1") - assert.Contains(t, ui.Outputs[3], "Org") - assert.Contains(t, ui.Outputs[3], "my-org") - assert.Contains(t, ui.Outputs[4], "Apps") - assert.Contains(t, ui.Outputs[4], "app1") - assert.Contains(t, ui.Outputs[5], "Domains") - assert.Contains(t, ui.Outputs[5], "domain1") - assert.Contains(t, ui.Outputs[6], "Services") - assert.Contains(t, ui.Outputs[6], "service1") -} - -func callShowSpace(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("space", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - config := &configuration.Configuration{ - AccessToken: token, - OrganizationFields: org, - } - - cmd := NewShowSpace(ui, config) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/stacks.go b/src/cf/commands/stacks.go deleted file mode 100644 index f2de2506f76..00000000000 --- a/src/cf/commands/stacks.go +++ /dev/null @@ -1,57 +0,0 @@ -package commands - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "github.com/codegangsta/cli" -) - -type Stacks struct { - ui terminal.UI - config *configuration.Configuration - stacksRepo api.StackRepository -} - -func NewStacks(ui terminal.UI, config *configuration.Configuration, stacksRepo api.StackRepository) (cmd *Stacks) { - cmd = new(Stacks) - cmd.ui = ui - cmd.config = config - cmd.stacksRepo = stacksRepo - return -} - -func (cmd *Stacks) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - return -} - -func (cmd *Stacks) Run(c *cli.Context) { - cmd.ui.Say("Getting stacks in org %s / space %s as %s...", - terminal.EntityNameColor(cmd.config.OrganizationFields.Name), - terminal.EntityNameColor(cmd.config.SpaceFields.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - stacks, apiResponse := cmd.stacksRepo.FindAll() - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() - cmd.ui.Say("") - - table := [][]string{ - []string{"name", "description"}, - } - - for _, stack := range stacks { - table = append(table, []string{ - stack.Name, - stack.Description, - }) - } - - cmd.ui.DisplayTable(table) -} diff --git a/src/cf/commands/stacks_test.go b/src/cf/commands/stacks_test.go deleted file mode 100644 index 631c1b85ab2..00000000000 --- a/src/cf/commands/stacks_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package commands_test - -import ( - "cf" - . "cf/commands" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testterm "testhelpers/terminal" - "testing" -) - -func TestStacks(t *testing.T) { - stack1 := cf.Stack{} - stack1.Name = "Stack-1" - stack1.Description = "Stack 1 Description" - - stack2 := cf.Stack{} - stack2.Name = "Stack-2" - stack2.Description = "Stack 2 Description" - - stackRepo := &testapi.FakeStackRepository{ - FindAllStacks: []cf.Stack{stack1, stack2}, - } - - ui := callStacks(t, stackRepo) - - assert.Equal(t, len(ui.Outputs), 6) - assert.Contains(t, ui.Outputs[0], "Getting stacks in org") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[4], "Stack-1") - assert.Contains(t, ui.Outputs[4], "Stack 1 Description") - assert.Contains(t, ui.Outputs[5], "Stack-2") - assert.Contains(t, ui.Outputs[5], "Stack 2 Description") -} - -func callStacks(t *testing.T, stackRepo *testapi.FakeStackRepository) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - ctxt := testcmd.NewContext("stacks", []string{}) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - - space := cf.SpaceFields{} - space.Name = "my-space" - - org := cf.OrganizationFields{} - org.Name = "my-org" - - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewStacks(ui, config, stackRepo) - testcmd.RunCommand(cmd, ctxt, nil) - - return -} diff --git a/src/cf/commands/target.go b/src/cf/commands/target.go deleted file mode 100644 index 5db3a7aecd7..00000000000 --- a/src/cf/commands/target.go +++ /dev/null @@ -1,146 +0,0 @@ -package commands - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type Target struct { - ui terminal.UI - config *configuration.Configuration - configRepo configuration.ConfigurationRepository - orgRepo api.OrganizationRepository - spaceRepo api.SpaceRepository -} - -func NewTarget(ui terminal.UI, - configRepo configuration.ConfigurationRepository, - orgRepo api.OrganizationRepository, - spaceRepo api.SpaceRepository) (cmd Target) { - - cmd.ui = ui - cmd.configRepo = configRepo - cmd.config, _ = configRepo.Get() - cmd.orgRepo = orgRepo - cmd.spaceRepo = spaceRepo - - return -} - -func (cmd Target) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 0 { - err = errors.New("incorrect usage") - cmd.ui.FailWithUsage(c, "target") - return - } - - if c.String("o") != "" || c.String("s") != "" { - reqs = append(reqs, reqFactory.NewLoginRequirement()) - } - return -} - -func (cmd Target) Run(c *cli.Context) { - orgName := c.String("o") - spaceName := c.String("s") - shouldShowTarget := (orgName == "" && spaceName == "") - - if shouldShowTarget { - cmd.ui.ShowConfiguration(cmd.config) - - if !cmd.config.HasOrganization() { - cmd.ui.Say("No org targeted, use '%s' to target an org", terminal.CommandColor(cf.Name()+" target -o")) - } - if !cmd.config.HasSpace() { - cmd.ui.Say("No space targeted, use '%s' to target a space", terminal.CommandColor(cf.Name()+" target -s")) - } - return - } - - if orgName != "" { - err := cmd.setOrganization(orgName) - - if spaceName == "" && cmd.config.IsLoggedIn() { - cmd.showConfig() - cmd.ui.Say("No space targeted, use '%s' to target a space", terminal.CommandColor(cf.Name()+" target -s")) - return - } - - if err != nil { - return - } - } - - if spaceName != "" { - err := cmd.setSpace(spaceName) - - if err != nil { - return - } - } - cmd.showConfig() - return -} - -func (cmd Target) setOrganization(orgName string) (err error) { - if !cmd.config.IsLoggedIn() { - cmd.ui.Failed("You must be logged in to target an org. Use '%s'.", terminal.CommandColor(cf.Name()+" login")) - return - } - - org, apiResponse := cmd.orgRepo.FindByName(orgName) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Could not target org.\n%s", apiResponse.Message) - return - } - - err = cmd.configRepo.SetOrganization(org.OrganizationFields) - if err != nil { - cmd.ui.Failed("Error setting org in config file.\n%s", err) - return - } - return -} - -func (cmd Target) setSpace(spaceName string) (err error) { - if !cmd.config.IsLoggedIn() { - cmd.ui.Failed("You must be logged in to set a space. Use '%s login'.", cf.Name()) - return - } - - if !cmd.config.HasOrganization() { - cmd.ui.Failed("An org must be targeted before targeting a space") - return - } - - space, apiResponse := cmd.spaceRepo.FindByName(spaceName) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Unable to access space %s.\n%s", spaceName, apiResponse.Message) - return - } - - err = cmd.configRepo.SetSpace(space.SpaceFields) - if err != nil { - cmd.ui.Failed("Error setting space in config file.\n%s", err) - return - } - return -} - -func (cmd Target) saveConfig() { - err := cmd.configRepo.Save() - if err != nil { - cmd.ui.Failed(err.Error()) - return - } -} - -func (cmd Target) showConfig() { - cmd.ui.ShowConfiguration(cmd.config) -} diff --git a/src/cf/commands/target_test.go b/src/cf/commands/target_test.go deleted file mode 100644 index 2703cb0e5bf..00000000000 --- a/src/cf/commands/target_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package commands_test - -import ( - "cf" - "cf/api" - . "cf/commands" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func getTargetDependencies() (orgRepo *testapi.FakeOrgRepository, - spaceRepo *testapi.FakeSpaceRepository, - configRepo *testconfig.FakeConfigRepository, - reqFactory *testreq.FakeReqFactory) { - - orgRepo = &testapi.FakeOrgRepository{} - spaceRepo = &testapi.FakeSpaceRepository{} - configRepo = &testconfig.FakeConfigRepository{} - reqFactory = &testreq.FakeReqFactory{LoginSuccess: true} - return -} - -func TestTargetFailsWithUsage(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - ui := callTarget([]string{}, reqFactory, configRepo, orgRepo, spaceRepo) - assert.False(t, ui.FailedWithUsage) - - ui = callTarget([]string{"foo"}, reqFactory, configRepo, orgRepo, spaceRepo) - assert.True(t, ui.FailedWithUsage) -} - -func TestTargetRequirements(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - reqFactory.LoginSuccess = true - - callTarget([]string{}, reqFactory, configRepo, orgRepo, spaceRepo) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestTargetWithoutArgumentAndLoggedIn(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - config := configRepo.Login() - config.Target = "https://api.run.pivotal.io" - - ui := callTarget([]string{}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Equal(t, len(ui.Outputs), 2) - assert.Contains(t, ui.Outputs[0], "No org targeted") - assert.Contains(t, ui.Outputs[1], "No space targeted") -} - -func TestTargetOrganizationWhenUserHasAccess(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Login() - config, err := configRepo.Get() - assert.NoError(t, err) - - config.SpaceFields = cf.SpaceFields{} - config.SpaceFields.Name = "my-space" - config.SpaceFields.Guid = "my-space-guid" - - org := cf.Organization{} - org.Name = "my-organization" - org.Guid = "my-organization-guid" - - orgRepo.Organizations = []cf.Organization{org} - orgRepo.FindByNameOrganization = org - - ui := callTarget([]string{"-o", "my-organization"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Equal(t, orgRepo.FindByNameName, "my-organization") - assert.True(t, ui.ShowConfigurationCalled) - - savedConfig := testconfig.SavedConfiguration - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-organization-guid") -} - -func TestTargetOrganizationWhenUserDoesNotHaveAccess(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Delete() - configRepo.Login() - - orgs := []cf.Organization{} - orgRepo.Organizations = orgs - orgRepo.FindByNameErr = true - - ui := callTarget([]string{}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "No org targeted") - - ui = callTarget([]string{"-o", "my-organization"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "FAILED") - - ui = callTarget([]string{}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "No org targeted") -} - -func TestTargetOrganizationWhenOrgNotFound(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - configRepo.Delete() - configRepo.Login() - - config, err := configRepo.Get() - assert.NoError(t, err) - - config.OrganizationFields = cf.OrganizationFields{} - config.OrganizationFields.Guid = "previous-org-guid" - config.OrganizationFields.Name = "previous-org" - - err = configRepo.Save() - assert.NoError(t, err) - - orgRepo.FindByNameNotFound = true - - ui := callTarget([]string{"-o", "my-organization"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "my-organization") - assert.Contains(t, ui.Outputs[1], "not found") -} - -func TestTargetSpaceWhenNoOrganizationIsSelected(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Delete() - configRepo.Login() - - ui := callTarget([]string{"-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "An org must be targeted before targeting a space") - savedConfig := testconfig.SavedConfiguration - assert.Equal(t, savedConfig.OrganizationFields.Guid, "") -} - -func TestTargetSpaceWhenUserHasAccess(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Delete() - config := configRepo.Login() - config.OrganizationFields = cf.OrganizationFields{} - config.OrganizationFields.Name = "my-org" - config.OrganizationFields.Guid = "my-org-guid" - - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - - spaceRepo.Spaces = []cf.Space{space} - spaceRepo.FindByNameSpace = space - - ui := callTarget([]string{"-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Equal(t, spaceRepo.FindByNameName, "my-space") - savedConfig := testconfig.SavedConfiguration - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") - assert.True(t, ui.ShowConfigurationCalled) -} - -func TestTargetSpaceWhenUserDoesNotHaveAccess(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Delete() - config := configRepo.Login() - config.OrganizationFields = cf.OrganizationFields{} - config.OrganizationFields.Name = "my-org" - config.OrganizationFields.Guid = "my-org-guid" - - spaceRepo.FindByNameErr = true - - ui := callTarget([]string{"-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "my-space") - - savedConfig := testconfig.SavedConfiguration - assert.Equal(t, savedConfig.SpaceFields.Guid, "") - assert.True(t, ui.ShowConfigurationCalled) -} - -func TestTargetSpaceWhenSpaceNotFound(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - - configRepo.Delete() - config := configRepo.Login() - config.OrganizationFields = cf.OrganizationFields{} - config.OrganizationFields.Name = "my-org" - config.OrganizationFields.Guid = "my-org-guid" - - spaceRepo.FindByNameNotFound = true - - ui := callTarget([]string{"-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "my-space") - assert.Contains(t, ui.Outputs[1], "not found") -} - -func TestTargetOrganizationAndSpace(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - configRepo.Delete() - configRepo.Login() - - org := cf.Organization{} - org.Name = "my-organization" - org.Guid = "my-organization-guid" - orgRepo.FindByNameOrganization = org - - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - spaceRepo.FindByNameSpace = space - - ui := callTarget([]string{"-o", "my-organization", "-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - savedConfig := testconfig.SavedConfiguration - assert.True(t, ui.ShowConfigurationCalled) - - assert.Equal(t, orgRepo.FindByNameName, "my-organization") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-organization-guid") - - assert.Equal(t, spaceRepo.FindByNameName, "my-space") - assert.Equal(t, savedConfig.SpaceFields.Guid, "my-space-guid") -} - -func TestTargetOrganizationAndSpaceWhenSpaceFails(t *testing.T) { - orgRepo, spaceRepo, configRepo, reqFactory := getTargetDependencies() - configRepo.Delete() - configRepo.Login() - - org := cf.Organization{} - org.Name = "my-organization" - org.Guid = "my-organization-guid" - orgRepo.FindByNameOrganization = org - - spaceRepo.FindByNameErr = true - - ui := callTarget([]string{"-o", "my-organization", "-s", "my-space"}, reqFactory, configRepo, orgRepo, spaceRepo) - - savedConfig := testconfig.SavedConfiguration - assert.True(t, ui.ShowConfigurationCalled) - - assert.Equal(t, orgRepo.FindByNameName, "my-organization") - assert.Equal(t, savedConfig.OrganizationFields.Guid, "my-organization-guid") - assert.Equal(t, spaceRepo.FindByNameName, "my-space") - assert.Equal(t, savedConfig.SpaceFields.Guid, "") - assert.Contains(t, ui.Outputs[0], "FAILED") -} - -func callTarget(args []string, - reqFactory *testreq.FakeReqFactory, - configRepo configuration.ConfigurationRepository, - orgRepo api.OrganizationRepository, - spaceRepo api.SpaceRepository) (ui *testterm.FakeUI) { - - ui = new(testterm.FakeUI) - cmd := NewTarget(ui, configRepo, orgRepo, spaceRepo) - ctxt := testcmd.NewContext("target", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/create_user.go b/src/cf/commands/user/create_user.go deleted file mode 100644 index 8afd2272b27..00000000000 --- a/src/cf/commands/user/create_user.go +++ /dev/null @@ -1,55 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type CreateUserFields struct { - ui terminal.UI - config *configuration.Configuration - userRepo api.UserRepository -} - -func NewCreateUser(ui terminal.UI, config *configuration.Configuration, userRepo api.UserRepository) (cmd CreateUserFields) { - cmd.ui = ui - cmd.config = config - cmd.userRepo = userRepo - return -} - -func (cmd CreateUserFields) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "create-user") - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} - -func (cmd CreateUserFields) Run(c *cli.Context) { - username := c.Args()[0] - password := c.Args()[1] - - cmd.ui.Say("Creating user %s as %s...", - terminal.EntityNameColor(username), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.userRepo.Create(username, password) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed("Error creating user %s.\n%s", terminal.EntityNameColor(username), apiResponse.Message) - return - } - - cmd.ui.Ok() - - cmd.ui.Say("\nTIP: Assign roles with '%s set-org-role' and '%s set-space-role'", cf.Name(), cf.Name()) -} diff --git a/src/cf/commands/user/create_user_test.go b/src/cf/commands/user/create_user_test.go deleted file mode 100644 index 92aec39afc6..00000000000 --- a/src/cf/commands/user/create_user_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func getCreateUserDefaults() (defaultArgs []string, defaultReqs *testreq.FakeReqFactory, defaultUserRepo *testapi.FakeUserRepository) { - defaultArgs = []string{"my-user", "my-password"} - defaultReqs = &testreq.FakeReqFactory{LoginSuccess: true} - defaultUserRepo = &testapi.FakeUserRepository{} - return -} - -func TestCreateUserFailsWithUsage(t *testing.T) { - defaultArgs, defaultReqs, defaultUserRepo := getCreateUserDefaults() - - emptyArgs := []string{} - - fakeUI := callCreateUser(t, emptyArgs, defaultReqs, defaultUserRepo) - assert.True(t, fakeUI.FailedWithUsage) - - fakeUI = callCreateUser(t, defaultArgs, defaultReqs, defaultUserRepo) - assert.False(t, fakeUI.FailedWithUsage) -} - -func TestCreateUserRequirements(t *testing.T) { - defaultArgs, defaultReqs, defaultUserRepo := getCreateUserDefaults() - - callCreateUser(t, defaultArgs, defaultReqs, defaultUserRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - notLoggedInReq := &testreq.FakeReqFactory{LoginSuccess: false} - callCreateUser(t, defaultArgs, notLoggedInReq, defaultUserRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - -} - -func TestCreateUser(t *testing.T) { - defaultArgs, defaultReqs, defaultUserRepo := getCreateUserDefaults() - - fakeUI := callCreateUser(t, defaultArgs, defaultReqs, defaultUserRepo) - - assert.Contains(t, fakeUI.Outputs[0], "Creating user") - assert.Contains(t, fakeUI.Outputs[0], "my-user") - assert.Contains(t, fakeUI.Outputs[0], "current-user") - assert.Equal(t, defaultUserRepo.CreateUserUsername, "my-user") - assert.Contains(t, fakeUI.Outputs[1], "OK") - assert.Contains(t, fakeUI.Outputs[2], "TIP") -} - -func TestCreateUserWhenItAlreadyExists(t *testing.T) { - defaultArgs, defaultReqs, userAlreadyExistsRepo := getCreateUserDefaults() - - userAlreadyExistsRepo.CreateUserExists = true - - fakeUI := callCreateUser(t, defaultArgs, defaultReqs, userAlreadyExistsRepo) - - assert.Equal(t, len(fakeUI.Outputs), 3) - assert.Contains(t, fakeUI.Outputs[1], "FAILED") - assert.Contains(t, fakeUI.Outputs[2], "my-user") -} - -func callCreateUser(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("create-user", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewCreateUser(ui, config, userRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/delete_user.go b/src/cf/commands/user/delete_user.go deleted file mode 100644 index ee346d60647..00000000000 --- a/src/cf/commands/user/delete_user.go +++ /dev/null @@ -1,71 +0,0 @@ -package user - -import ( - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type DeleteUserFields struct { - ui terminal.UI - config *configuration.Configuration - userRepo api.UserRepository -} - -func NewDeleteUser(ui terminal.UI, config *configuration.Configuration, userRepo api.UserRepository) (cmd DeleteUserFields) { - cmd.ui = ui - cmd.config = config - cmd.userRepo = userRepo - return -} - -func (cmd DeleteUserFields) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Invalid usage") - cmd.ui.FailWithUsage(c, "delete-user") - return - } - - reqs = append(reqs, reqFactory.NewLoginRequirement()) - - return -} - -func (cmd DeleteUserFields) Run(c *cli.Context) { - username := c.Args()[0] - force := c.Bool("f") - - if !force && !cmd.ui.Confirm("Really delete user %s?%s", - terminal.EntityNameColor(username), - terminal.PromptColor(">"), - ) { - return - } - - cmd.ui.Say("Deleting user %s as %s...", - terminal.EntityNameColor(username), - terminal.EntityNameColor(cmd.config.Username()), - ) - - user, apiResponse := cmd.userRepo.FindByUsername(username) - if apiResponse.IsError() { - cmd.ui.Failed(apiResponse.Message) - return - } - if apiResponse.IsNotFound() { - cmd.ui.Ok() - cmd.ui.Warn("UserFields %s does not exist.", username) - return - } - - apiResponse = cmd.userRepo.Delete(user.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/user/delete_user_test.go b/src/cf/commands/user/delete_user_test.go deleted file mode 100644 index aaa1fedda2c..00000000000 --- a/src/cf/commands/user/delete_user_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestDeleteUserFailsWithUsage(t *testing.T) { - userRepo := &testapi.FakeUserRepository{} - reqFactory := &testreq.FakeReqFactory{} - - ui := callDeleteUser(t, []string{}, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callDeleteUser(t, []string{"foo"}, userRepo, reqFactory) - assert.False(t, ui.FailedWithUsage) - - ui = callDeleteUser(t, []string{"foo", "bar"}, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) -} - -func TestDeleteUserRequirements(t *testing.T) { - userRepo := &testapi.FakeUserRepository{} - reqFactory := &testreq.FakeReqFactory{} - args := []string{"-f", "my-user"} - - reqFactory.LoginSuccess = false - callDeleteUser(t, args, userRepo, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callDeleteUser(t, args, userRepo, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) -} - -func TestDeleteUserWhenConfirmingWithY(t *testing.T) { - ui, userRepo := deleteWithConfirmation(t, "Y") - - assert.Equal(t, len(ui.Outputs), 2) - assert.Equal(t, len(ui.Prompts), 1) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "Deleting user") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.FindByUsernameUsername, "my-user") - assert.Equal(t, userRepo.DeleteUserGuid, "my-found-user-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteUserWhenConfirmingWithYes(t *testing.T) { - ui, userRepo := deleteWithConfirmation(t, "Yes") - - assert.Equal(t, len(ui.Outputs), 2) - assert.Equal(t, len(ui.Prompts), 1) - assert.Contains(t, ui.Prompts[0], "Really delete") - assert.Contains(t, ui.Outputs[0], "Deleting user") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.FindByUsernameUsername, "my-user") - assert.Equal(t, userRepo.DeleteUserGuid, "my-found-user-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteUserWhenNotConfirming(t *testing.T) { - ui, userRepo := deleteWithConfirmation(t, "Nope") - - assert.Equal(t, len(ui.Outputs), 0) - assert.Contains(t, ui.Prompts[0], "Really delete") - - assert.Equal(t, userRepo.FindByUsernameUsername, "") - assert.Equal(t, userRepo.DeleteUserGuid, "") -} - -func TestDeleteUserWithForceOption(t *testing.T) { - foundUserFields := cf.UserFields{} - foundUserFields.Guid = "my-found-user-guid" - userRepo := &testapi.FakeUserRepository{FindByUsernameUserFields: foundUserFields} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - ui := callDeleteUser(t, []string{"-f", "my-user"}, userRepo, reqFactory) - - assert.Equal(t, len(ui.Outputs), 2) - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting user") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, userRepo.FindByUsernameUsername, "my-user") - assert.Equal(t, userRepo.DeleteUserGuid, "my-found-user-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func TestDeleteUserWhenUserNotFound(t *testing.T) { - userRepo := &testapi.FakeUserRepository{FindByUsernameNotFound: true} - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - ui := callDeleteUser(t, []string{"-f", "my-user"}, userRepo, reqFactory) - - assert.Equal(t, len(ui.Outputs), 3) - assert.Equal(t, len(ui.Prompts), 0) - assert.Contains(t, ui.Outputs[0], "Deleting user") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, userRepo.FindByUsernameUsername, "my-user") - assert.Equal(t, userRepo.DeleteUserGuid, "") - - assert.Contains(t, ui.Outputs[1], "OK") - assert.Contains(t, ui.Outputs[2], "does not exist") -} - -func callDeleteUser(t *testing.T, args []string, userRepo *testapi.FakeUserRepository, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org, - AccessToken: token, - } - - cmd := NewDeleteUser(ui, config, userRepo) - ctxt := testcmd.NewContext("delete-user", args) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} - -func deleteWithConfirmation(t *testing.T, confirmation string) (ui *testterm.FakeUI, userRepo *testapi.FakeUserRepository) { - ui = &testterm.FakeUI{ - Inputs: []string{confirmation}, - } - user2 := cf.UserFields{} - user2.Username = "my-found-user" - user2.Guid = "my-found-user-guid" - userRepo = &testapi.FakeUserRepository{ - FindByUsernameUserFields: user2, - } - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - space2 := cf.SpaceFields{} - space2.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewDeleteUser(ui, config, userRepo) - - ctxt := testcmd.NewContext("delete-user", []string{"my-user"}) - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true} - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/org_users.go b/src/cf/commands/user/org_users.go deleted file mode 100644 index 392b868efb4..00000000000 --- a/src/cf/commands/user/org_users.go +++ /dev/null @@ -1,81 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -var orgRoles = []string{cf.ORG_MANAGER, cf.BILLING_MANAGER, cf.ORG_AUDITOR} - -var orgRoleToDisplayName = map[string]string{ - cf.ORG_MANAGER: "ORG MANAGER", - cf.BILLING_MANAGER: "BILLING MANAGER", - cf.ORG_AUDITOR: "ORG AUDITOR", -} - -type OrgUsers struct { - ui terminal.UI - config *configuration.Configuration - orgReq requirements.OrganizationRequirement - userRepo api.UserRepository -} - -func NewOrgUsers(ui terminal.UI, config *configuration.Configuration, userRepo api.UserRepository) (cmd *OrgUsers) { - cmd = new(OrgUsers) - cmd.ui = ui - cmd.config = config - cmd.userRepo = userRepo - return -} - -func (cmd *OrgUsers) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 1 { - err = errors.New("Incorrect usage") - cmd.ui.FailWithUsage(c, "org-users") - return - } - - orgName := c.Args()[0] - cmd.orgReq = reqFactory.NewOrganizationRequirement(orgName) - reqs = append(reqs, reqFactory.NewLoginRequirement(), cmd.orgReq) - - return -} - -func (cmd *OrgUsers) Run(c *cli.Context) { - org := cmd.orgReq.GetOrganization() - - cmd.ui.Say("Getting users in org %s as %s...", - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - for _, role := range orgRoles { - stopChan := make(chan bool) - defer close(stopChan) - - displayName := orgRoleToDisplayName[role] - - usersChan, statusChan := cmd.userRepo.ListUsersInOrgForRole(org.Guid, role, stopChan) - - cmd.ui.Say("") - cmd.ui.Say("%s", terminal.HeaderColor(displayName)) - - for users := range usersChan { - for _, user := range users { - cmd.ui.Say(" %s", user.Username) - } - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching org-users for role %s.\n%s", apiStatus.Message, displayName) - return - } - } -} diff --git a/src/cf/commands/user/org_users_test.go b/src/cf/commands/user/org_users_test.go deleted file mode 100644 index d813fa650b8..00000000000 --- a/src/cf/commands/user/org_users_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestOrgUsersFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - userRepo := &testapi.FakeUserRepository{} - ui := callOrgUsers(t, []string{}, reqFactory, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callOrgUsers(t, []string{"Org1"}, reqFactory, userRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestOrgUsersRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - userRepo := &testapi.FakeUserRepository{} - args := []string{"Org1"} - - reqFactory.LoginSuccess = false - callOrgUsers(t, args, reqFactory, userRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callOrgUsers(t, args, reqFactory, userRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, "Org1", reqFactory.OrganizationName) -} - -func TestOrgUsers(t *testing.T) { - org := cf.Organization{} - org.Name = "Found Org" - org.Guid = "found-org-guid" - - userRepo := &testapi.FakeUserRepository{} - user := cf.UserFields{} - user.Username = "user1" - user2 := cf.UserFields{} - user2.Username = "user2" - user3 := cf.UserFields{} - user3.Username = "user3" - user4 := cf.UserFields{} - user4.Username = "user4" - userRepo.ListUsersByRole = map[string][]cf.UserFields{ - cf.ORG_MANAGER: []cf.UserFields{user, user2}, - cf.BILLING_MANAGER: []cf.UserFields{user4}, - cf.ORG_AUDITOR: []cf.UserFields{user3}, - } - - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - Organization: org, - } - - ui := callOrgUsers(t, []string{"Org1"}, reqFactory, userRepo) - - assert.Equal(t, userRepo.ListUsersOrganizationGuid, "found-org-guid") - - assert.Contains(t, ui.Outputs[0], "Getting users in org") - assert.Contains(t, ui.Outputs[0], "Found Org") - assert.Contains(t, ui.Outputs[0], "my-user") - - testassert.SliceContains(t, ui.Outputs, []string{ - "ORG MANAGER", - "user1", - "user2", - "BILLING MANAGER", - "user4", - "ORG AUDITOR", - "user3", - }) -} - -func callOrgUsers(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org3 := cf.OrganizationFields{} - org3.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org3, - AccessToken: token, - } - - cmd := NewOrgUsers(ui, config, userRepo) - ctxt := testcmd.NewContext("org-users", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/set_org_role.go b/src/cf/commands/user/set_org_role.go deleted file mode 100644 index e13bb8bcf6d..00000000000 --- a/src/cf/commands/user/set_org_role.go +++ /dev/null @@ -1,67 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type SetOrgRole struct { - ui terminal.UI - config *configuration.Configuration - userRepo api.UserRepository - userReq requirements.UserRequirement - orgReq requirements.OrganizationRequirement -} - -func NewSetOrgRole(ui terminal.UI, config *configuration.Configuration, userRepo api.UserRepository) (cmd *SetOrgRole) { - cmd = new(SetOrgRole) - cmd.ui = ui - cmd.config = config - cmd.userRepo = userRepo - return -} - -func (cmd *SetOrgRole) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 3 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "set-org-role") - return - } - - cmd.userReq = reqFactory.NewUserRequirement(c.Args()[0]) - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[1]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.userReq, - cmd.orgReq, - } - - return -} - -func (cmd *SetOrgRole) Run(c *cli.Context) { - user := cmd.userReq.GetUser() - org := cmd.orgReq.GetOrganization() - role := cf.UserInputToOrgRole[c.Args()[2]] - - cmd.ui.Say("Assigning role %s to user %s in org %s as %s...", - terminal.EntityNameColor(role), - terminal.EntityNameColor(user.Username), - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.userRepo.SetOrgRole(user.Guid, org.Guid, role) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/user/set_org_role_test.go b/src/cf/commands/user/set_org_role_test.go deleted file mode 100644 index e03936e4152..00000000000 --- a/src/cf/commands/user/set_org_role_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSetOrgRoleFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - userRepo := &testapi.FakeUserRepository{} - - ui := callSetOrgRole(t, []string{"my-user", "my-org", "my-role"}, reqFactory, userRepo) - assert.False(t, ui.FailedWithUsage) - - ui = callSetOrgRole(t, []string{"my-user", "my-org"}, reqFactory, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetOrgRole(t, []string{"my-user"}, reqFactory, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetOrgRole(t, []string{}, reqFactory, userRepo) - assert.True(t, ui.FailedWithUsage) -} - -func TestSetOrgRoleRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - userRepo := &testapi.FakeUserRepository{} - - reqFactory.LoginSuccess = false - callSetOrgRole(t, []string{"my-user", "my-org", "my-role"}, reqFactory, userRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callSetOrgRole(t, []string{"my-user", "my-org", "my-role"}, reqFactory, userRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.UserUsername, "my-user") - assert.Equal(t, reqFactory.OrganizationName, "my-org") -} - -func TestSetOrgRole(t *testing.T) { - org := cf.Organization{} - org.Guid = "my-org-guid" - org.Name = "my-org" - user := cf.UserFields{} - user.Guid = "my-user-guid" - user.Username = "my-user" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - UserFields: user, - Organization: org, - } - userRepo := &testapi.FakeUserRepository{} - - ui := callSetOrgRole(t, []string{"some-user", "some-org", "OrgManager"}, reqFactory, userRepo) - - assert.Contains(t, ui.Outputs[0], "Assigning role ") - assert.Contains(t, ui.Outputs[0], "OrgManager") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.SetOrgRoleUserGuid, "my-user-guid") - assert.Equal(t, userRepo.SetOrgRoleOrganizationGuid, "my-org-guid") - assert.Equal(t, userRepo.SetOrgRoleRole, cf.ORG_MANAGER) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callSetOrgRole(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("set-org-role", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewSetOrgRole(ui, config, userRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/set_space_role.go b/src/cf/commands/user/set_space_role.go deleted file mode 100644 index a20093a5f51..00000000000 --- a/src/cf/commands/user/set_space_role.go +++ /dev/null @@ -1,76 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type SetSpaceRole struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository - userRepo api.UserRepository - userReq requirements.UserRequirement - orgReq requirements.OrganizationRequirement -} - -func NewSetSpaceRole(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository, userRepo api.UserRepository) (cmd *SetSpaceRole) { - cmd = new(SetSpaceRole) - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - cmd.userRepo = userRepo - return -} - -func (cmd *SetSpaceRole) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 4 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "set-space-role") - return - } - - cmd.userReq = reqFactory.NewUserRequirement(c.Args()[0]) - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[1]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.userReq, - cmd.orgReq, - } - return -} - -func (cmd *SetSpaceRole) Run(c *cli.Context) { - spaceName := c.Args()[2] - role := cf.UserInputToSpaceRole[c.Args()[3]] - - user := cmd.userReq.GetUser() - org := cmd.orgReq.GetOrganization() - space, apiResponse := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Say("Assigning role %s to user %s in org %s / space %s as %s...", - terminal.EntityNameColor(role), - terminal.EntityNameColor(user.Username), - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse = cmd.userRepo.SetSpaceRole(user.Guid, space.Guid, space.Organization.Guid, role) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/user/set_space_role_test.go b/src/cf/commands/user/set_space_role_test.go deleted file mode 100644 index 56790cbacf2..00000000000 --- a/src/cf/commands/user/set_space_role_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSetSpaceRoleFailsWithUsage(t *testing.T) { - reqFactory, spaceRepo, userRepo := getSetSpaceRoleDeps() - - ui := callSetSpaceRole(t, []string{}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetSpaceRole(t, []string{"my-user"}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetSpaceRole(t, []string{"my-user", "my-org"}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetSpaceRole(t, []string{"my-user", "my-org", "my-space"}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSetSpaceRole(t, []string{"my-user", "my-org", "my-space", "my-role"}, reqFactory, spaceRepo, userRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestSetSpaceRoleRequirements(t *testing.T) { - args := []string{"username", "org", "space", "role"} - reqFactory, spaceRepo, userRepo := getSetSpaceRoleDeps() - - reqFactory.LoginSuccess = false - callSetSpaceRole(t, args, reqFactory, spaceRepo, userRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callSetSpaceRole(t, args, reqFactory, spaceRepo, userRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.UserUsername, "username") - assert.Equal(t, reqFactory.OrganizationName, "org") -} - -func TestSetSpaceRole(t *testing.T) { - args := []string{"some-user", "some-org", "some-space", "SpaceManager"} - reqFactory, spaceRepo, userRepo := getSetSpaceRoleDeps() - - reqFactory.LoginSuccess = true - - reqFactory.UserFields = cf.UserFields{} - reqFactory.UserFields.Guid = "my-user-guid" - reqFactory.UserFields.Username = "my-user" - reqFactory.Organization = cf.Organization{} - reqFactory.Organization.Guid = "my-org-guid" - reqFactory.Organization.Name = "my-org" - spaceRepo.FindByNameInOrgSpace = cf.Space{} - spaceRepo.FindByNameInOrgSpace.Guid = "my-space-guid" - spaceRepo.FindByNameInOrgSpace.Name = "my-space" - - ui := callSetSpaceRole(t, args, reqFactory, spaceRepo, userRepo) - - assert.Equal(t, spaceRepo.FindByNameInOrgName, "some-space") - assert.Equal(t, spaceRepo.FindByNameInOrgOrgGuid, "my-org-guid") - - assert.Contains(t, ui.Outputs[0], "Assigning role ") - assert.Contains(t, ui.Outputs[0], "SpaceManager") - assert.Contains(t, ui.Outputs[0], "my-user") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-space") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.SetSpaceRoleUserGuid, "my-user-guid") - assert.Equal(t, userRepo.SetSpaceRoleSpaceGuid, "my-space-guid") - assert.Equal(t, userRepo.SetSpaceRoleRole, cf.SPACE_MANAGER) - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func getSetSpaceRoleDeps() (reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository) { - reqFactory = &testreq.FakeReqFactory{} - spaceRepo = &testapi.FakeSpaceRepository{} - userRepo = &testapi.FakeUserRepository{} - return -} - -func callSetSpaceRole(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - ctxt := testcmd.NewContext("set-space-role", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - space2 := cf.SpaceFields{} - space2.Name = "my-space" - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewSetSpaceRole(ui, config, spaceRepo, userRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/space_users.go b/src/cf/commands/user/space_users.go deleted file mode 100644 index 2d3bce59919..00000000000 --- a/src/cf/commands/user/space_users.go +++ /dev/null @@ -1,90 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -var spaceRoles = []string{cf.SPACE_MANAGER, cf.SPACE_DEVELOPER, cf.SPACE_AUDITOR} - -var spaceRoleToDisplayName = map[string]string{ - cf.SPACE_MANAGER: "SPACE MANAGER", - cf.SPACE_DEVELOPER: "SPACE DEVELOPER", - cf.SPACE_AUDITOR: "SPACE AUDITOR", -} - -type SpaceUsers struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository - userRepo api.UserRepository - orgReq requirements.OrganizationRequirement -} - -func NewSpaceUsers(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository, userRepo api.UserRepository) (cmd *SpaceUsers) { - cmd = new(SpaceUsers) - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - cmd.userRepo = userRepo - return -} - -func (cmd *SpaceUsers) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 2 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "space-users") - return - } - - orgName := c.Args()[0] - cmd.orgReq = reqFactory.NewOrganizationRequirement(orgName) - reqs = append(reqs, reqFactory.NewLoginRequirement(), cmd.orgReq) - - return -} - -func (cmd *SpaceUsers) Run(c *cli.Context) { - spaceName := c.Args()[1] - org := cmd.orgReq.GetOrganization() - - space, apiResponse := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - } - - cmd.ui.Say("Getting users in org %s / space %s as %s", - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - for _, role := range spaceRoles { - stopChan := make(chan bool) - defer close(stopChan) - - displayName := spaceRoleToDisplayName[role] - - usersChan, statusChan := cmd.userRepo.ListUsersInSpaceForRole(space.Guid, role, stopChan) - - cmd.ui.Say("") - cmd.ui.Say("%s", terminal.HeaderColor(displayName)) - - for users := range usersChan { - for _, user := range users { - cmd.ui.Say(" %s", user.Username) - } - } - - apiStatus := <-statusChan - if apiStatus.IsNotSuccessful() { - cmd.ui.Failed("Failed fetching space-users for role %s.\n%s", apiStatus.Message, displayName) - return - } - } -} diff --git a/src/cf/commands/user/space_users_test.go b/src/cf/commands/user/space_users_test.go deleted file mode 100644 index c8cdd953ae7..00000000000 --- a/src/cf/commands/user/space_users_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testassert "testhelpers/assert" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestSpaceUsersFailsWithUsage(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - spaceRepo := &testapi.FakeSpaceRepository{} - userRepo := &testapi.FakeUserRepository{} - - ui := callSpaceUsers(t, []string{}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSpaceUsers(t, []string{"my-org"}, reqFactory, spaceRepo, userRepo) - assert.True(t, ui.FailedWithUsage) - - ui = callSpaceUsers(t, []string{"my-org", "my-space"}, reqFactory, spaceRepo, userRepo) - assert.False(t, ui.FailedWithUsage) -} - -func TestSpaceUsersRequirements(t *testing.T) { - reqFactory := &testreq.FakeReqFactory{} - spaceRepo := &testapi.FakeSpaceRepository{} - userRepo := &testapi.FakeUserRepository{} - args := []string{"my-org", "my-space"} - - reqFactory.LoginSuccess = false - callSpaceUsers(t, args, reqFactory, spaceRepo, userRepo) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callSpaceUsers(t, args, reqFactory, spaceRepo, userRepo) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, "my-org", reqFactory.OrganizationName) -} - -func TestSpaceUsers(t *testing.T) { - org := cf.Organization{} - org.Name = "Org1" - org.Guid = "org1-guid" - space := cf.Space{} - space.Name = "Space1" - space.Guid = "space1-guid" - - reqFactory := &testreq.FakeReqFactory{LoginSuccess: true, Organization: org} - spaceRepo := &testapi.FakeSpaceRepository{FindByNameInOrgSpace: space} - userRepo := &testapi.FakeUserRepository{} - - user := cf.UserFields{} - user.Username = "user1" - user2 := cf.UserFields{} - user2.Username = "user2" - user3 := cf.UserFields{} - user3.Username = "user3" - user4 := cf.UserFields{} - user4.Username = "user4" - userRepo.ListUsersByRole = map[string][]cf.UserFields{ - cf.SPACE_MANAGER: []cf.UserFields{user, user2}, - cf.SPACE_DEVELOPER: []cf.UserFields{user4}, - cf.SPACE_AUDITOR: []cf.UserFields{user3}, - } - - ui := callSpaceUsers(t, []string{"my-org", "my-space"}, reqFactory, spaceRepo, userRepo) - - assert.Equal(t, spaceRepo.FindByNameInOrgName, "my-space") - assert.Equal(t, spaceRepo.FindByNameInOrgOrgGuid, "org1-guid") - - assert.Contains(t, ui.Outputs[0], "Getting users in org") - assert.Contains(t, ui.Outputs[0], "Org1") - assert.Contains(t, ui.Outputs[0], "Space1") - assert.Contains(t, ui.Outputs[0], "my-user") - - assert.Equal(t, userRepo.ListUsersSpaceGuid, "space1-guid") - - testassert.SliceContains(t, ui.Outputs, []string{ - "SPACE MANAGER", - "user1", - "user2", - "SPACE DEVELOPER", - "user4", - "SPACE AUDITOR", - "user3", - }) -} - -func callSpaceUsers(t *testing.T, args []string, reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository) (ui *testterm.FakeUI) { - ui = new(testterm.FakeUI) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "my-user", - }) - assert.NoError(t, err) - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - space2 := cf.SpaceFields{} - space2.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewSpaceUsers(ui, config, spaceRepo, userRepo) - ctxt := testcmd.NewContext("space-users", args) - - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/unset_org_role.go b/src/cf/commands/user/unset_org_role.go deleted file mode 100644 index 0e5ec74896c..00000000000 --- a/src/cf/commands/user/unset_org_role.go +++ /dev/null @@ -1,69 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UnsetOrgRole struct { - ui terminal.UI - config *configuration.Configuration - userRepo api.UserRepository - userReq requirements.UserRequirement - orgReq requirements.OrganizationRequirement -} - -func NewUnsetOrgRole(ui terminal.UI, config *configuration.Configuration, userRepo api.UserRepository) (cmd *UnsetOrgRole) { - cmd = new(UnsetOrgRole) - cmd.ui = ui - cmd.config = config - cmd.userRepo = userRepo - - return -} - -func (cmd *UnsetOrgRole) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 3 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "unset-org-role") - return - } - - cmd.userReq = reqFactory.NewUserRequirement(c.Args()[0]) - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[1]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.userReq, - cmd.orgReq, - } - - return -} - -func (cmd *UnsetOrgRole) Run(c *cli.Context) { - role := cf.UserInputToOrgRole[c.Args()[2]] - user := cmd.userReq.GetUser() - org := cmd.orgReq.GetOrganization() - - cmd.ui.Say("Removing role %s from user %s in org %s as %s...", - terminal.EntityNameColor(role), - terminal.EntityNameColor(c.Args()[0]), - terminal.EntityNameColor(c.Args()[1]), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse := cmd.userRepo.UnsetOrgRole(user.Guid, org.Guid, role) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/user/unset_org_role_test.go b/src/cf/commands/user/unset_org_role_test.go deleted file mode 100644 index b81e154d613..00000000000 --- a/src/cf/commands/user/unset_org_role_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUnsetOrgRoleFailsWithUsage(t *testing.T) { - userRepo := &testapi.FakeUserRepository{} - reqFactory := &testreq.FakeReqFactory{} - - ui := callUnsetOrgRole(t, []string{}, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetOrgRole(t, []string{"username"}, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetOrgRole(t, []string{"username", "org"}, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetOrgRole(t, []string{"username", "org", "role"}, userRepo, reqFactory) - assert.False(t, ui.FailedWithUsage) -} - -func TestUnsetOrgRoleRequirements(t *testing.T) { - userRepo := &testapi.FakeUserRepository{} - reqFactory := &testreq.FakeReqFactory{} - args := []string{"username", "org", "role"} - - reqFactory.LoginSuccess = false - callUnsetOrgRole(t, args, userRepo, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callUnsetOrgRole(t, args, userRepo, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.UserUsername, "username") - assert.Equal(t, reqFactory.OrganizationName, "org") -} - -func TestUnsetOrgRole(t *testing.T) { - userRepo := &testapi.FakeUserRepository{} - user := cf.UserFields{} - user.Username = "some-user" - user.Guid = "some-user-guid" - org := cf.Organization{} - org.Name = "some-org" - org.Guid = "some-org-guid" - reqFactory := &testreq.FakeReqFactory{ - LoginSuccess: true, - UserFields: user, - Organization: org, - } - args := []string{"my-username", "my-org", "OrgManager"} - - ui := callUnsetOrgRole(t, args, userRepo, reqFactory) - - assert.Contains(t, ui.Outputs[0], "Removing role ") - assert.Contains(t, ui.Outputs[0], "my-org") - assert.Contains(t, ui.Outputs[0], "my-username") - assert.Contains(t, ui.Outputs[0], "OrgManager") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.UnsetOrgRoleRole, cf.ORG_MANAGER) - assert.Equal(t, userRepo.UnsetOrgRoleUserGuid, "some-user-guid") - assert.Equal(t, userRepo.UnsetOrgRoleOrganizationGuid, "some-org-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func callUnsetOrgRole(t *testing.T, args []string, userRepo *testapi.FakeUserRepository, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("unset-org-role", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - config := &configuration.Configuration{ - SpaceFields: space, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewUnsetOrgRole(ui, config, userRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/commands/user/unset_space_role.go b/src/cf/commands/user/unset_space_role.go deleted file mode 100644 index fc4a7e02064..00000000000 --- a/src/cf/commands/user/unset_space_role.go +++ /dev/null @@ -1,78 +0,0 @@ -package user - -import ( - "cf" - "cf/api" - "cf/configuration" - "cf/requirements" - "cf/terminal" - "errors" - "github.com/codegangsta/cli" -) - -type UnsetSpaceRole struct { - ui terminal.UI - config *configuration.Configuration - spaceRepo api.SpaceRepository - userRepo api.UserRepository - userReq requirements.UserRequirement - orgReq requirements.OrganizationRequirement -} - -func NewUnsetSpaceRole(ui terminal.UI, config *configuration.Configuration, spaceRepo api.SpaceRepository, userRepo api.UserRepository) (cmd *UnsetSpaceRole) { - cmd = new(UnsetSpaceRole) - cmd.ui = ui - cmd.config = config - cmd.spaceRepo = spaceRepo - cmd.userRepo = userRepo - return -} - -func (cmd *UnsetSpaceRole) GetRequirements(reqFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { - if len(c.Args()) != 4 { - err = errors.New("Incorrect Usage") - cmd.ui.FailWithUsage(c, "unset-space-role") - return - } - - cmd.userReq = reqFactory.NewUserRequirement(c.Args()[0]) - cmd.orgReq = reqFactory.NewOrganizationRequirement(c.Args()[1]) - - reqs = []requirements.Requirement{ - reqFactory.NewLoginRequirement(), - cmd.userReq, - cmd.orgReq, - } - - return -} - -func (cmd *UnsetSpaceRole) Run(c *cli.Context) { - spaceName := c.Args()[2] - role := cf.UserInputToSpaceRole[c.Args()[3]] - - user := cmd.userReq.GetUser() - org := cmd.orgReq.GetOrganization() - space, apiResponse := cmd.spaceRepo.FindByNameInOrg(spaceName, org.Guid) - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Say("Removing role %s from user %s in org %s / space %s as %s...", - terminal.EntityNameColor(role), - terminal.EntityNameColor(user.Username), - terminal.EntityNameColor(org.Name), - terminal.EntityNameColor(space.Name), - terminal.EntityNameColor(cmd.config.Username()), - ) - - apiResponse = cmd.userRepo.UnsetSpaceRole(user.Guid, space.Guid, role) - - if apiResponse.IsNotSuccessful() { - cmd.ui.Failed(apiResponse.Message) - return - } - - cmd.ui.Ok() -} diff --git a/src/cf/commands/user/unset_space_role_test.go b/src/cf/commands/user/unset_space_role_test.go deleted file mode 100644 index f5f2b855ae0..00000000000 --- a/src/cf/commands/user/unset_space_role_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package user_test - -import ( - "cf" - . "cf/commands/user" - "cf/configuration" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testcmd "testhelpers/commands" - testconfig "testhelpers/configuration" - testreq "testhelpers/requirements" - testterm "testhelpers/terminal" - "testing" -) - -func TestUnsetSpaceRoleFailsWithUsage(t *testing.T) { - reqFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() - - ui := callUnsetSpaceRole(t, []string{}, spaceRepo, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetSpaceRole(t, []string{"username"}, spaceRepo, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetSpaceRole(t, []string{"username", "org"}, spaceRepo, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetSpaceRole(t, []string{"username", "org", "space"}, spaceRepo, userRepo, reqFactory) - assert.True(t, ui.FailedWithUsage) - - ui = callUnsetSpaceRole(t, []string{"username", "org", "space", "role"}, spaceRepo, userRepo, reqFactory) - assert.False(t, ui.FailedWithUsage) -} - -func TestUnsetSpaceRoleRequirements(t *testing.T) { - reqFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() - args := []string{"username", "org", "space", "role"} - - reqFactory.LoginSuccess = false - callUnsetSpaceRole(t, args, spaceRepo, userRepo, reqFactory) - assert.False(t, testcmd.CommandDidPassRequirements) - - reqFactory.LoginSuccess = true - callUnsetSpaceRole(t, args, spaceRepo, userRepo, reqFactory) - assert.True(t, testcmd.CommandDidPassRequirements) - - assert.Equal(t, reqFactory.UserUsername, "username") - assert.Equal(t, reqFactory.OrganizationName, "org") -} - -func TestUnsetSpaceRole(t *testing.T) { - user := cf.UserFields{} - user.Username = "some-user" - user.Guid = "some-user-guid" - org := cf.Organization{} - org.Name = "some-org" - org.Guid = "some-org-guid" - - reqFactory, spaceRepo, userRepo := getUnsetSpaceRoleDeps() - reqFactory.LoginSuccess = true - reqFactory.UserFields = user - reqFactory.Organization = org - spaceRepo.FindByNameInOrgSpace = cf.Space{} - spaceRepo.FindByNameInOrgSpace.Name = "some-space" - spaceRepo.FindByNameInOrgSpace.Guid = "some-space-guid" - - args := []string{"my-username", "my-org", "my-space", "SpaceManager"} - - ui := callUnsetSpaceRole(t, args, spaceRepo, userRepo, reqFactory) - - assert.Equal(t, spaceRepo.FindByNameInOrgName, "my-space") - assert.Equal(t, spaceRepo.FindByNameInOrgOrgGuid, "some-org-guid") - - assert.Contains(t, ui.Outputs[0], "Removing role ") - assert.Contains(t, ui.Outputs[0], "SpaceManager") - assert.Contains(t, ui.Outputs[0], "some-user") - assert.Contains(t, ui.Outputs[0], "some-org") - assert.Contains(t, ui.Outputs[0], "some-space") - assert.Contains(t, ui.Outputs[0], "current-user") - - assert.Equal(t, userRepo.UnsetSpaceRoleRole, cf.SPACE_MANAGER) - assert.Equal(t, userRepo.UnsetSpaceRoleUserGuid, "some-user-guid") - assert.Equal(t, userRepo.UnsetSpaceRoleSpaceGuid, "some-space-guid") - - assert.Contains(t, ui.Outputs[1], "OK") -} - -func getUnsetSpaceRoleDeps() (reqFactory *testreq.FakeReqFactory, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository) { - reqFactory = &testreq.FakeReqFactory{} - spaceRepo = &testapi.FakeSpaceRepository{} - userRepo = &testapi.FakeUserRepository{} - return -} - -func callUnsetSpaceRole(t *testing.T, args []string, spaceRepo *testapi.FakeSpaceRepository, userRepo *testapi.FakeUserRepository, reqFactory *testreq.FakeReqFactory) (ui *testterm.FakeUI) { - ui = &testterm.FakeUI{} - ctxt := testcmd.NewContext("unset-space-role", args) - - token, err := testconfig.CreateAccessTokenWithTokenInfo(configuration.TokenInfo{ - Username: "current-user", - }) - assert.NoError(t, err) - space2 := cf.SpaceFields{} - space2.Name = "my-space" - org2 := cf.OrganizationFields{} - org2.Name = "my-org" - config := &configuration.Configuration{ - SpaceFields: space2, - OrganizationFields: org2, - AccessToken: token, - } - - cmd := NewUnsetSpaceRole(ui, config, spaceRepo, userRepo) - testcmd.RunCommand(cmd, ctxt, reqFactory) - return -} diff --git a/src/cf/configuration/configuration.go b/src/cf/configuration/configuration.go deleted file mode 100644 index f975dafaeeb..00000000000 --- a/src/cf/configuration/configuration.go +++ /dev/null @@ -1,59 +0,0 @@ -package configuration - -import ( - "cf" - "encoding/json" - "time" -) - -type Configuration struct { - Target string - ApiVersion string - AuthorizationEndpoint string - AccessToken string - RefreshToken string - OrganizationFields cf.OrganizationFields - SpaceFields cf.SpaceFields - ApplicationStartTimeout time.Duration // will be used as seconds -} - -func (c Configuration) UserEmail() (email string) { - return c.getTokenInfo().Email -} - -func (c Configuration) UserGuid() (guid string) { - return c.getTokenInfo().UserGuid -} - -func (c Configuration) Username() (guid string) { - return c.getTokenInfo().Username -} - -func (c Configuration) IsLoggedIn() bool { - return c.AccessToken != "" -} - -func (c Configuration) HasOrganization() bool { - return c.OrganizationFields.Guid != "" && c.OrganizationFields.Name != "" -} - -func (c Configuration) HasSpace() bool { - return c.SpaceFields.Guid != "" && c.SpaceFields.Name != "" -} - -type TokenInfo struct { - Username string `json:"user_name"` - Email string `json:"email"` - UserGuid string `json:"user_id"` -} - -func (c Configuration) getTokenInfo() (info TokenInfo) { - clearInfo, err := DecodeTokenInfo(c.AccessToken) - - if err != nil { - return - } - info = TokenInfo{} - err = json.Unmarshal(clearInfo, &info) - return -} diff --git a/src/cf/configuration/configuration_test.go b/src/cf/configuration/configuration_test.go deleted file mode 100644 index 443a1097682..00000000000 --- a/src/cf/configuration/configuration_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package configuration - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestUserEmailWithAValidAccessToken(t *testing.T) { - config := Configuration{ - AccessToken: "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E", - } - - assert.Equal(t, config.UserEmail(), "user1@example.com") -} - -func TestUserEmailWithInvalidAccessToken(t *testing.T) { - config := Configuration{} - - config.AccessToken = "bearer" - assert.Empty(t, config.UserEmail()) - - config.AccessToken = "bearer eyJhbGciOiJSUzI1NiJ9" - assert.Empty(t, config.UserEmail()) -} - -func TestUserGuidWithAValidAccessToken(t *testing.T) { - config := Configuration{ - AccessToken: "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E", - } - - assert.Equal(t, config.UserGuid(), "772dda3f-669f-4276-b2bd-90486abe1f6f") -} - -func TestUserGuidWithInvalidAccessToken(t *testing.T) { - config := Configuration{} - - config.AccessToken = "bearer" - assert.Empty(t, config.UserGuid()) - - config.AccessToken = "bearer eyJhbGciOiJSUzI1NiJ9" - assert.Empty(t, config.UserGuid()) -} diff --git a/src/cf/configuration/repository.go b/src/cf/configuration/repository.go deleted file mode 100644 index 90ce505ed3d..00000000000 --- a/src/cf/configuration/repository.go +++ /dev/null @@ -1,190 +0,0 @@ -package configuration - -import ( - "cf" - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - "runtime" -) - -const ( - filePermissions = 0644 - dirPermissions = 0700 -) - -var singleton *Configuration - -type ConfigurationRepository interface { - Get() (config *Configuration, err error) - Delete() - Save() (err error) - ClearTokens() (err error) - ClearSession() (err error) - SetOrganization(org cf.OrganizationFields) (err error) - SetSpace(space cf.SpaceFields) (err error) -} - -type ConfigurationDiskRepository struct { -} - -func NewConfigurationDiskRepository() (repo ConfigurationDiskRepository) { - return ConfigurationDiskRepository{} -} - -func (repo ConfigurationDiskRepository) SetOrganization(org cf.OrganizationFields) (err error) { - config, err := repo.Get() - if err != nil { - return - } - - config.OrganizationFields = org - config.SpaceFields = cf.SpaceFields{} - - return saveConfiguration(config) -} - -func (repo ConfigurationDiskRepository) SetSpace(space cf.SpaceFields) (err error) { - config, err := repo.Get() - if err != nil { - return - } - - config.SpaceFields = space - - return saveConfiguration(config) -} - -func (repo ConfigurationDiskRepository) Get() (c *Configuration, err error) { - if singleton == nil { - singleton, err = load() - - if err != nil { - return - } - } - - return singleton, nil -} - -func (repo ConfigurationDiskRepository) Delete() { - file, err := ConfigFile() - - if err != nil { - return - } - - os.Remove(file) - singleton = nil -} - -func (repo ConfigurationDiskRepository) Save() (err error) { - c, err := repo.Get() - if err != nil { - return - } - return saveConfiguration(c) -} - -func (repo ConfigurationDiskRepository) ClearTokens() (err error) { - c, err := repo.Get() - if err != nil { - return - } - c.AccessToken = "" - c.RefreshToken = "" - return -} - -func (repo ConfigurationDiskRepository) ClearSession() (err error) { - err = repo.ClearTokens() - if err != nil { - return - } - - c, err := repo.Get() - if err != nil { - return - } - c.OrganizationFields = cf.OrganizationFields{} - c.SpaceFields = cf.SpaceFields{} - - return saveConfiguration(c) -} - -// Keep this one public for configtest/configuration.go -func ConfigFile() (file string, err error) { - - configDir := filepath.Join(userHomeDir(), ".cf") - - err = os.MkdirAll(configDir, dirPermissions) - - if err != nil { - return - } - - file = filepath.Join(configDir, "config.json") - return -} - -// See: http://stackoverflow.com/questions/7922270/obtain-users-home-directory -// we can't cross compile using cgo and use user.Current() -func userHomeDir() string { - if runtime.GOOS == "windows" { - home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if home == "" { - home = os.Getenv("USERPROFILE") - } - return home - } - - return os.Getenv("HOME") -} - -func defaultConfig() (c *Configuration) { - c = new(Configuration) - c.Target = "" - c.ApiVersion = "" - c.AuthorizationEndpoint = "" - c.ApplicationStartTimeout = 30 // seconds - - return -} - -func load() (c *Configuration, parseError error) { - file, readError := ConfigFile() - c = new(Configuration) - - if readError != nil { - c := defaultConfig() - return c, saveConfiguration(c) - } - - data, readError := ioutil.ReadFile(file) - - if readError != nil { - c := defaultConfig() - return c, saveConfiguration(c) - } - - parseError = json.Unmarshal(data, c) - - return -} - -func saveConfiguration(config *Configuration) (err error) { - bytes, err := json.Marshal(config) - if err != nil { - return - } - - file, err := ConfigFile() - - if err != nil { - return - } - err = ioutil.WriteFile(file, bytes, filePermissions) - - return -} diff --git a/src/cf/configuration/repository_test.go b/src/cf/configuration/repository_test.go deleted file mode 100644 index 151da639961..00000000000 --- a/src/cf/configuration/repository_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package configuration - -import ( - "cf" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestLoadingWithNoConfigFile(t *testing.T) { - repo := NewConfigurationDiskRepository() - config := repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - - assert.Equal(t, config.Target, "") - assert.Equal(t, config.ApiVersion, "") - assert.Equal(t, config.AuthorizationEndpoint, "") - assert.Equal(t, config.AccessToken, "") -} - -func TestSavingAndLoading(t *testing.T) { - repo := NewConfigurationDiskRepository() - configToSave := repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - - configToSave.ApiVersion = "3.1.0" - configToSave.Target = "https://api.target.example.com" - configToSave.AuthorizationEndpoint = "https://login.target.example.com" - configToSave.AccessToken = "bearer my_access_token" - - repo.Save() - - singleton = nil - savedConfig, err := repo.Get() - assert.NoError(t, err) - assert.Equal(t, savedConfig, configToSave) -} - -func TestSetOrganization(t *testing.T) { - repo := NewConfigurationDiskRepository() - config := repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - - config.OrganizationFields = cf.OrganizationFields{} - - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - err := repo.SetOrganization(org) - assert.NoError(t, err) - - repo.Save() - - savedConfig, err := repo.Get() - assert.NoError(t, err) - assert.Equal(t, savedConfig.OrganizationFields, org) - assert.Equal(t, savedConfig.SpaceFields, cf.SpaceFields{}) -} - -func TestSetSpace(t *testing.T) { - repo := NewConfigurationDiskRepository() - repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - err := repo.SetSpace(space) - assert.NoError(t, err) - - repo.Save() - - savedConfig, err := repo.Get() - assert.NoError(t, err) - assert.Equal(t, savedConfig.SpaceFields, space) -} - -func TestClearTokens(t *testing.T) { - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - - repo := NewConfigurationDiskRepository() - config := repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - - config.Target = "http://api.example.com" - config.RefreshToken = "some old refresh token" - config.AccessToken = "some old access token" - config.OrganizationFields = org - config.SpaceFields = space - repo.Save() - - err := repo.ClearTokens() - assert.NoError(t, err) - - repo.Save() - - savedConfig, err := repo.Get() - assert.NoError(t, err) - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Empty(t, savedConfig.AccessToken) - assert.Empty(t, savedConfig.RefreshToken) - assert.Equal(t, savedConfig.OrganizationFields, org) - assert.Equal(t, savedConfig.SpaceFields, space) -} - -func TestClearSession(t *testing.T) { - repo := NewConfigurationDiskRepository() - config := repo.loadDefaultConfig(t) - defer repo.restoreConfig(t) - - config.Target = "http://api.example.com" - config.RefreshToken = "some old refresh token" - config.AccessToken = "some old access token" - org := cf.OrganizationFields{} - org.Name = "my-org" - space := cf.SpaceFields{} - space.Name = "my-space" - repo.Save() - - err := repo.ClearSession() - assert.NoError(t, err) - - repo.Save() - - savedConfig, err := repo.Get() - assert.NoError(t, err) - assert.Equal(t, savedConfig.Target, "http://api.example.com") - assert.Empty(t, savedConfig.AccessToken) - assert.Empty(t, savedConfig.RefreshToken) - assert.Equal(t, savedConfig.OrganizationFields, cf.OrganizationFields{}) - assert.Equal(t, savedConfig.SpaceFields, cf.SpaceFields{}) -} - -func (repo ConfigurationDiskRepository) loadDefaultConfig(t *testing.T) (config *Configuration) { - file, err := ConfigFile() - assert.NoError(t, err) - - _, err = os.Stat(file) - if !os.IsNotExist(err) { - err = os.Rename(file, file+"test-backup") - assert.NoError(t, err) - } - - config, err = repo.Get() - assert.NoError(t, err) - - return -} - -func (repo ConfigurationDiskRepository) restoreConfig(t *testing.T) { - file, err := ConfigFile() - assert.NoError(t, err) - - err = os.Remove(file) - assert.NoError(t, err) - - _, err = os.Stat(file + "test-backup") - if !os.IsNotExist(err) { - err = os.Rename(file+"test-backup", file) - assert.NoError(t, err) - } - - return -} diff --git a/src/cf/configuration/token.go b/src/cf/configuration/token.go deleted file mode 100644 index 586a63a7110..00000000000 --- a/src/cf/configuration/token.go +++ /dev/null @@ -1,38 +0,0 @@ -package configuration - -import ( - "encoding/base64" - "strings" -) - -func DecodeTokenInfo(accessToken string) (clearTokenInfo []byte, err error) { - tokenParts := strings.Split(accessToken, " ") - - if len(tokenParts) < 2 { - return - } - - token := tokenParts[1] - encodedInfoParts := strings.Split(token, ".") - - if len(encodedInfoParts) < 3 { - return - } - - encodedInfo := encodedInfoParts[1] - return base64Decode(encodedInfo) -} - -func base64Decode(encodedInfo string) ([]byte, error) { - return base64.StdEncoding.DecodeString(restorePadding(encodedInfo)) -} - -func restorePadding(seg string) string { - switch len(seg) % 4 { - case 2: - seg = seg + "==" - case 3: - seg = seg + "===" - } - return seg -} diff --git a/src/cf/configuration/token_test.go b/src/cf/configuration/token_test.go deleted file mode 100644 index eeb23fcaf93..00000000000 --- a/src/cf/configuration/token_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package configuration - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestDecodeTokenInfoWithoutRestoringPadding(t *testing.T) { - accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E" - decodedInfo, err := DecodeTokenInfo(accessToken) - - assert.NoError(t, err) - assert.Contains(t, string(decodedInfo), "user1@example.com") -} - -func TestDecodeTokenInfoWhenRestoringPadding(t *testing.T) { - accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwNTg2MjlkNC04NjEwLTQ3NTEtOTg3Ny0yOGMwNzE3YTE5ZTciLCJzdWIiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJ1c2VyX25hbWUiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiZW1haWwiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiaWF0IjoxMzc3MDk1ODM5LCJleHAiOjEzNzcxMzkwMzksImlzcyI6Imh0dHBzOi8vdWFhLnJ1bi5waXZvdGFsLmlvL29hdXRoL3Rva2VuIiwiYXVkIjpbIm9wZW5pZCIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCJdfQ.dcgrGjPvTjYvg8dTSZY5ecZZTNt59IYd442VaEXXvLNB_WQCAdbVOxiJ14ogzQkkzDDw60Q2lbw4z6HrqM1a-BNpYfRmvaIP_79GpIZC6OzQy_PgA1whL27pO7_ABkSJT1CEgJQJMTQlYOiZNHvFTWen3G4O6ey680cxIN5VvbFjmmQHCuwANE9_GqnYYvoI9tS1nERku8DX2H9KH5NAgDa52-p0NhLnZRqYjGss6EyPYkwYN5w2OizfYUmEYVWo8K1Q45_TGMoE-LgZe2mGWwv0euLYBoFTkYhtBMj91dQagLrL1aGcmDKPc6ivkXtfpN4Zv7FJ9OXJ2DPQyHKRpw" - decodedInfo, err := DecodeTokenInfo(accessToken) - - assert.NoError(t, err) - assert.Contains(t, string(decodedInfo), "tlang@gopivotal.com") -} diff --git a/src/cf/domain.go b/src/cf/domain.go deleted file mode 100644 index 6178555d118..00000000000 --- a/src/cf/domain.go +++ /dev/null @@ -1,216 +0,0 @@ -package cf - -import ( - "fmt" - "time" -) - -type InstanceState string - -const ( - InstanceStarting InstanceState = "starting" - InstanceRunning = "running" - InstanceFlapping = "flapping" - InstanceDown = "down" -) - -type BasicFields struct { - Guid string - Name string -} - -func (model BasicFields) String() string { - return model.Name -} - -type OrganizationFields struct { - BasicFields -} - -type Organization struct { - OrganizationFields - Spaces []SpaceFields - Domains []DomainFields -} - -type SpaceFields struct { - BasicFields -} - -type Space struct { - SpaceFields - Organization OrganizationFields - Applications []ApplicationFields - ServiceInstances []ServiceInstanceFields - Domains []DomainFields -} - -type ApplicationFields struct { - BasicFields - State string - Command string - BuildpackUrl string - InstanceCount int - RunningInstances int - Memory uint64 // in Megabytes - DiskQuota uint64 // in Megabytes - EnvironmentVars map[string]string -} - -type Application struct { - ApplicationFields - Stack Stack - Routes []RouteSummary -} - -type AppSummary struct { - ApplicationFields - RouteSummaries []RouteSummary -} - -type AppFileFields struct { - Path string - Sha1 string - Size int64 -} - -type DomainFields struct { - BasicFields - OwningOrganizationGuid string - Shared bool -} - -func (model DomainFields) UrlForHost(host string) string { - if host == "" { - return model.Name - } - return fmt.Sprintf("%s.%s", host, model.Name) -} - -type Domain struct { - DomainFields - Spaces []SpaceFields -} - -type EventFields struct { - InstanceIndex int - Timestamp time.Time - ExitDescription string - ExitStatus int -} - -type RouteFields struct { - Guid string - Host string -} - -type Route struct { - RouteSummary - Space SpaceFields - Apps []ApplicationFields -} - -type RouteSummary struct { - RouteFields - Domain DomainFields -} - -func (model RouteSummary) URL() string { - if model.Host == "" { - return model.Domain.Name - } - return fmt.Sprintf("%s.%s", model.Host, model.Domain.Name) -} - -type Stack struct { - BasicFields - Description string -} - -type AppInstanceFields struct { - State InstanceState - Since time.Time - CpuUsage float64 // percentage - DiskQuota uint64 // in bytes - DiskUsage uint64 - MemQuota uint64 - MemUsage uint64 -} - -type ServicePlanFields struct { - BasicFields -} - -type ServicePlan struct { - ServicePlanFields - ServiceOffering ServiceOfferingFields -} - -type ServiceOfferingFields struct { - Guid string - Label string - Provider string - Version string - Description string - DocumentationUrl string -} - -type ServiceOffering struct { - ServiceOfferingFields - Plans []ServicePlanFields -} - -type ServiceInstanceFields struct { - BasicFields - SysLogDrainUrl string - ApplicationNames []string - Params map[string]string -} - -type ServiceInstance struct { - ServiceInstanceFields - ServiceBindings []ServiceBindingFields - ServicePlan ServicePlanFields - ServiceOffering ServiceOfferingFields -} - -func (inst ServiceInstance) IsUserProvided() bool { - return inst.ServicePlan.Guid == "" -} - -type ServiceBindingFields struct { - Guid string - Url string - AppGuid string -} - -type QuotaFields struct { - BasicFields - MemoryLimit uint64 // in Megabytes -} - -type ServiceAuthTokenFields struct { - Guid string - Label string - Provider string - Token string -} - -type ServiceBroker struct { - BasicFields - Username string - Password string - Url string -} - -type UserFields struct { - Guid string - Username string - Password string - IsAdmin bool -} - -type Buildpack struct { - BasicFields - Position *int -} diff --git a/src/cf/domain_test.go b/src/cf/domain_test.go deleted file mode 100644 index 82a61841b18..00000000000 --- a/src/cf/domain_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package cf - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestRouteURL(t *testing.T) { - route := Route{} - route.Host = "foo" - - domain := DomainFields{} - domain.Name = "example.com" - route.Domain = domain - - assert.Equal(t, route.URL(), "foo.example.com") -} - -func TestRouteURLWithoutHost(t *testing.T) { - route := Route{} - route.Host = "" - - domain := DomainFields{} - domain.Name = "example.com" - route.Domain = domain - - assert.Equal(t, route.URL(), "example.com") -} diff --git a/src/cf/formatters/bytes.go b/src/cf/formatters/bytes.go deleted file mode 100644 index 3bb85b066f2..00000000000 --- a/src/cf/formatters/bytes.go +++ /dev/null @@ -1,69 +0,0 @@ -package formatters - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -const ( - BYTE = 1.0 - KILOBYTE = 1024 * BYTE - MEGABYTE = 1024 * KILOBYTE - GIGABYTE = 1024 * MEGABYTE - TERABYTE = 1024 * GIGABYTE -) - -func ByteSize(bytes uint64) string { - unit := "" - value := float32(bytes) - - switch { - case bytes >= TERABYTE: - unit = "T" - value = value / TERABYTE - case bytes >= GIGABYTE: - unit = "G" - value = value / GIGABYTE - case bytes >= MEGABYTE: - unit = "M" - value = value / MEGABYTE - case bytes >= KILOBYTE: - unit = "K" - value = value / KILOBYTE - case bytes == 0: - return "0" - } - - stringValue := fmt.Sprintf("%.1f", value) - stringValue = strings.TrimSuffix(stringValue, ".0") - return fmt.Sprintf("%s%s", stringValue, unit) -} - -func BytesFromString(s string) (bytes uint64, err error) { - unit := string(s[len(s)-1]) - stringValue := s[0 : len(s)-1] - - value, err := strconv.ParseUint(stringValue, 10, 0) - if err != nil { - return - } - - switch unit { - case "T": - bytes = value * TERABYTE - case "G": - bytes = value * GIGABYTE - case "M": - bytes = value * MEGABYTE - case "K": - bytes = value * KILOBYTE - } - - if bytes == 0 { - err = errors.New("Could not parse byte string") - } - - return -} diff --git a/src/cf/formatters/bytes_test.go b/src/cf/formatters/bytes_test.go deleted file mode 100644 index 7f52af63fb7..00000000000 --- a/src/cf/formatters/bytes_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package formatters - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestByteSize(t *testing.T) { - assert.Equal(t, ByteSize(100*MEGABYTE), "100M") - assert.Equal(t, ByteSize(uint64(100.5*MEGABYTE)), "100.5M") -} diff --git a/src/cf/known_error_codes.go b/src/cf/known_error_codes.go deleted file mode 100644 index c2e501520f4..00000000000 --- a/src/cf/known_error_codes.go +++ /dev/null @@ -1,12 +0,0 @@ -package cf - -const ( - USER_EXISTS = "20002" - USER_NOT_FOUND = "20003" - ORG_EXISTS = "30002" - SPACE_EXISTS = "40002" - SERVICE_INSTANCE_NAME_TAKEN = "60002" - APP_NOT_STAGED = "170002" - APP_STOPPED = "220001" - BUILDPACK_EXISTS = "290001" -) diff --git a/src/cf/net/api_response.go b/src/cf/net/api_response.go deleted file mode 100644 index fcab7430f3f..00000000000 --- a/src/cf/net/api_response.go +++ /dev/null @@ -1,64 +0,0 @@ -package net - -import ( - "fmt" -) - -type ApiResponse struct { - Message string - ErrorCode string - StatusCode int - - isError bool - isNotFound bool -} - -func NewApiResponse(message string, errorCode string, statusCode int) (apiResponse ApiResponse) { - return ApiResponse{ - Message: message, - ErrorCode: errorCode, - StatusCode: statusCode, - isError: true, - } -} - -func NewApiResponseWithMessage(message string, a ...interface{}) (apiResponse ApiResponse) { - return ApiResponse{ - Message: fmt.Sprintf(message, a...), - isError: true, - } -} - -func NewApiResponseWithError(message string, err error) (apiResponse ApiResponse) { - return ApiResponse{ - Message: fmt.Sprintf("%s: %s", message, err.Error()), - isError: true, - } -} - -func NewNotFoundApiResponse(message string, a ...interface{}) (apiResponse ApiResponse) { - return ApiResponse{ - Message: fmt.Sprintf(message, a...), - isNotFound: true, - } -} - -func NewSuccessfulApiResponse() (apiResponse ApiResponse) { - return ApiResponse{} -} - -func (apiResponse ApiResponse) IsError() bool { - return apiResponse.isError -} - -func (apiResponse ApiResponse) IsNotFound() bool { - return apiResponse.isNotFound -} - -func (apiResponse ApiResponse) IsSuccessful() bool { - return !apiResponse.IsNotSuccessful() -} - -func (apiResponse ApiResponse) IsNotSuccessful() bool { - return apiResponse.IsError() || apiResponse.IsNotFound() -} diff --git a/src/cf/net/cloud_controller_gateway.go b/src/cf/net/cloud_controller_gateway.go deleted file mode 100644 index 1346aba97e4..00000000000 --- a/src/cf/net/cloud_controller_gateway.go +++ /dev/null @@ -1,34 +0,0 @@ -package net - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "strconv" -) - -func NewCloudControllerGateway() Gateway { - invalidTokenCode := "1000" - - type ccErrorResponse struct { - Code int - Description string - } - - errorHandler := func(response *http.Response) errorResponse { - jsonBytes, _ := ioutil.ReadAll(response.Body) - response.Body.Close() - - ccResp := ccErrorResponse{} - json.Unmarshal(jsonBytes, &ccResp) - - code := strconv.Itoa(ccResp.Code) - if code == invalidTokenCode { - code = INVALID_TOKEN_CODE - } - - return errorResponse{Code: code, Description: ccResp.Description} - } - - return newGateway(errorHandler) -} diff --git a/src/cf/net/cloud_controller_gateway_test.go b/src/cf/net/cloud_controller_gateway_test.go deleted file mode 100644 index 229e420a770..00000000000 --- a/src/cf/net/cloud_controller_gateway_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package net_test - -import ( - . "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -var failingCloudControllerRequest = func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusBadRequest) - jsonResponse := `{ "code": 210003, "description": "The host is taken: test1" }` - fmt.Fprintln(writer, jsonResponse) -} - -func TestCloudControllerGatewayErrorHandling(t *testing.T) { - gateway := NewCloudControllerGateway() - - ts := httptest.NewTLSServer(http.HandlerFunc(failingCloudControllerRequest)) - defer ts.Close() - - request, apiResponse := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) - assert.False(t, apiResponse.IsNotSuccessful()) - - apiResponse = gateway.PerformRequest(request) - - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Contains(t, apiResponse.Message, "The host is taken: test1") - assert.Contains(t, apiResponse.ErrorCode, "210003") -} - -var invalidTokenCloudControllerRequest = func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusBadRequest) - jsonResponse := `{ "code": 1000, "description": "The token is invalid" }` - fmt.Fprintln(writer, jsonResponse) -} - -func TestCloudControllerGatewayInvalidTokenHandling(t *testing.T) { - gateway := NewCloudControllerGateway() - - ts := httptest.NewTLSServer(http.HandlerFunc(invalidTokenCloudControllerRequest)) - defer ts.Close() - - request, apiResponse := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) - assert.False(t, apiResponse.IsNotSuccessful()) - - apiResponse = gateway.PerformRequest(request) - - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Contains(t, apiResponse.Message, "The token is invalid") - assert.Contains(t, apiResponse.ErrorCode, INVALID_TOKEN_CODE) -} diff --git a/src/cf/net/gateway.go b/src/cf/net/gateway.go deleted file mode 100644 index 36949f06584..00000000000 --- a/src/cf/net/gateway.go +++ /dev/null @@ -1,212 +0,0 @@ -package net - -import ( - "cf" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "runtime" -) - -const INVALID_TOKEN_CODE = "GATEWAY INVALID TOKEN CODE" - -type errorResponse struct { - Code string - Description string -} - -type errorHandler func(*http.Response) errorResponse - -type tokenRefresher interface { - RefreshAuthToken() (string, ApiResponse) -} - -type Request struct { - HttpReq *http.Request - SeekableBody io.ReadSeeker -} - -type Gateway struct { - authenticator tokenRefresher - errHandler errorHandler -} - -func newGateway(errHandler errorHandler) (gateway Gateway) { - gateway.errHandler = errHandler - return -} - -func (gateway *Gateway) SetTokenRefresher(auth tokenRefresher) { - gateway.authenticator = auth -} - -func (gateway Gateway) GetResource(url, accessToken string, resource interface{}) (apiResponse ApiResponse) { - request, apiResponse := gateway.NewRequest("GET", url, accessToken, nil) - if apiResponse.IsNotSuccessful() { - return - } - - _, apiResponse = gateway.PerformRequestForJSONResponse(request, resource) - return -} - -func (gateway Gateway) CreateResource(url, accessToken string, body io.ReadSeeker) (apiResponse ApiResponse) { - return gateway.createUpdateOrDeleteResource("POST", url, accessToken, body, nil) -} - -func (gateway Gateway) CreateResourceForResponse(url, accessToken string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) { - return gateway.createUpdateOrDeleteResource("POST", url, accessToken, body, resource) -} - -func (gateway Gateway) UpdateResource(url, accessToken string, body io.ReadSeeker) (apiResponse ApiResponse) { - return gateway.createUpdateOrDeleteResource("PUT", url, accessToken, body, nil) -} - -func (gateway Gateway) UpdateResourceForResponse(url, accessToken string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) { - return gateway.createUpdateOrDeleteResource("PUT", url, accessToken, body, resource) -} - -func (gateway Gateway) DeleteResource(url, accessToken string) (apiResponse ApiResponse) { - return gateway.createUpdateOrDeleteResource("DELETE", url, accessToken, nil, nil) -} - -func (gateway Gateway) createUpdateOrDeleteResource(verb, url, accessToken string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) { - request, apiResponse := gateway.NewRequest(verb, url, accessToken, body) - if apiResponse.IsNotSuccessful() { - return - } - - if resource != nil { - _, apiResponse = gateway.PerformRequestForJSONResponse(request, resource) - return - } - - return gateway.PerformRequest(request) -} - -func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (req *Request, apiResponse ApiResponse) { - if body != nil { - body.Seek(0, 0) - } - - request, err := http.NewRequest(method, path, body) - if err != nil { - apiResponse = NewApiResponseWithError("Error building request", err) - return - } - - if accessToken != "" { - request.Header.Set("Authorization", accessToken) - } - - request.Header.Set("accept", "application/json") - request.Header.Set("content-type", "application/json") - request.Header.Set("UserFields-Agent", "go-cli "+cf.Version+" / "+runtime.GOOS) - - if body != nil { - switch v := body.(type) { - case *os.File: - fileStats, err := v.Stat() - if err != nil { - break - } - request.ContentLength = fileStats.Size() - } - } - - req = &Request{HttpReq: request, SeekableBody: body} - return -} - -func (gateway Gateway) PerformRequest(request *Request) (apiResponse ApiResponse) { - _, apiResponse = gateway.doRequestHandlingAuth(request) - return -} - -func (gateway Gateway) PerformRequestForResponseBytes(request *Request) (bytes []byte, headers http.Header, apiResponse ApiResponse) { - rawResponse, apiResponse := gateway.doRequestHandlingAuth(request) - if apiResponse.IsNotSuccessful() { - return - } - - bytes, err := ioutil.ReadAll(rawResponse.Body) - if err != nil { - apiResponse = NewApiResponseWithError("Error reading response", err) - } - - headers = rawResponse.Header - return -} - -func (gateway Gateway) PerformRequestForTextResponse(request *Request) (response string, headers http.Header, apiResponse ApiResponse) { - bytes, headers, apiResponse := gateway.PerformRequestForResponseBytes(request) - response = string(bytes) - return -} - -func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (headers http.Header, apiResponse ApiResponse) { - bytes, headers, apiResponse := gateway.PerformRequestForResponseBytes(request) - if apiResponse.IsNotSuccessful() { - return - } - - err := json.Unmarshal(bytes, &response) - if err != nil { - apiResponse = NewApiResponseWithError("Invalid JSON response from server", err) - } - return -} - -func (gateway Gateway) doRequestHandlingAuth(request *Request) (rawResponse *http.Response, apiResponse ApiResponse) { - httpReq := request.HttpReq - - // perform request - rawResponse, apiResponse = gateway.doRequestAndHandlerError(request) - if apiResponse.IsSuccessful() || gateway.authenticator == nil { - return - } - - if apiResponse.ErrorCode != INVALID_TOKEN_CODE { - return - } - - // refresh the auth token - newToken, apiResponse := gateway.authenticator.RefreshAuthToken() - if apiResponse.IsNotSuccessful() { - return - } - - // reset the auth token and request body - httpReq.Header.Set("Authorization", newToken) - if request.SeekableBody != nil { - request.SeekableBody.Seek(0, 0) - httpReq.Body = ioutil.NopCloser(request.SeekableBody) - } - - // make the request again - rawResponse, apiResponse = gateway.doRequestAndHandlerError(request) - return -} - -func (gateway Gateway) doRequestAndHandlerError(request *Request) (rawResponse *http.Response, apiResponse ApiResponse) { - rawResponse, err := doRequest(request.HttpReq) - if err != nil { - apiResponse = NewApiResponseWithError("Error performing request", err) - return - } - - if rawResponse.StatusCode > 299 { - errorResponse := gateway.errHandler(rawResponse) - message := fmt.Sprintf( - "Server error, status code: %d, error code: %s, message: %s", - rawResponse.StatusCode, - errorResponse.Code, - errorResponse.Description, - ) - apiResponse = NewApiResponse(message, errorResponse.Code, rawResponse.StatusCode) - } - return -} diff --git a/src/cf/net/gateway_test.go b/src/cf/net/gateway_test.go deleted file mode 100644 index 5fa5bbb94f7..00000000000 --- a/src/cf/net/gateway_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package net_test - -import ( - "cf" - "cf/api" - "cf/configuration" - . "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "runtime" - "strings" - testconfig "testhelpers/configuration" - testnet "testhelpers/net" - "testing" -) - -func TestNewRequest(t *testing.T) { - - gateway := NewCloudControllerGateway() - - request, apiResponse := gateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, request.HttpReq.Header.Get("Authorization"), "BEARER my-access-token") - assert.Equal(t, request.HttpReq.Header.Get("accept"), "application/json") - assert.Equal(t, request.HttpReq.Header.Get("UserFields-Agent"), "go-cli "+cf.Version+" / "+runtime.GOOS) -} - -func TestNewRequestWithAFileBody(t *testing.T) { - - gateway := NewCloudControllerGateway() - - body, err := os.Open("../../fixtures/hello_world.txt") - assert.NoError(t, err) - request, apiResponse := gateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", body) - - assert.True(t, apiResponse.IsSuccessful()) - assert.Equal(t, request.HttpReq.ContentLength, 12) -} - -func TestRefreshingTheTokenWithUAARequest(t *testing.T) { - gateway := NewUAAGateway() - endpoint := refreshTokenApiEndPoint( - `{ "error": "invalid_token", "error_description": "Auth token is invalid" }`, - testnet.TestResponse{Status: http.StatusOK}, - ) - - testRefreshTokenWithSuccess(t, gateway, endpoint) -} - -func TestRefreshingTheTokenWithUAARequestAndReturningError(t *testing.T) { - gateway := NewUAAGateway() - endpoint := refreshTokenApiEndPoint( - `{ "error": "invalid_token", "error_description": "Auth token is invalid" }`, - testnet.TestResponse{Status: http.StatusBadRequest, Body: `{ - "error": "333", "error_description": "bad request" - }`}, - ) - - testRefreshTokenWithError(t, gateway, endpoint) -} - -func TestRefreshingTheTokenWithCloudControllerRequest(t *testing.T) { - gateway := NewCloudControllerGateway() - endpoint := refreshTokenApiEndPoint( - `{ "code": 1000, "description": "Auth token is invalid" }`, - testnet.TestResponse{Status: http.StatusOK}, - ) - - testRefreshTokenWithSuccess(t, gateway, endpoint) -} - -func TestRefreshingTheTokenWithCloudControllerRequestAndReturningError(t *testing.T) { - gateway := NewCloudControllerGateway() - endpoint := refreshTokenApiEndPoint( - `{ "code": 1000, "description": "Auth token is invalid" }`, - testnet.TestResponse{Status: http.StatusBadRequest, Body: `{ - "code": 333, "description": "bad request" - }`}, - ) - - testRefreshTokenWithError(t, gateway, endpoint) -} - -func testRefreshTokenWithSuccess(t *testing.T, gateway Gateway, endpoint http.HandlerFunc) { - apiResponse := testRefreshToken(t, gateway, endpoint) - assert.True(t, apiResponse.IsSuccessful()) - - savedConfig := testconfig.SavedConfiguration - assert.Equal(t, savedConfig.AccessToken, "bearer new-access-token") - assert.Equal(t, savedConfig.RefreshToken, "new-refresh-token") -} - -func testRefreshTokenWithError(t *testing.T, gateway Gateway, endpoint http.HandlerFunc) { - apiResponse := testRefreshToken(t, gateway, endpoint) - assert.False(t, apiResponse.IsSuccessful()) - assert.Equal(t, apiResponse.ErrorCode, "333") -} - -var refreshTokenApiEndPoint = func(unauthorizedBody string, secondReqResp testnet.TestResponse) http.HandlerFunc { - return func(writer http.ResponseWriter, request *http.Request) { - var jsonResponse string - - bodyBytes, err := ioutil.ReadAll(request.Body) - if err != nil || string(bodyBytes) != "expected body" { - writer.WriteHeader(http.StatusInternalServerError) - return - } - - switch request.Header.Get("Authorization") { - case "bearer initial-access-token": - writer.WriteHeader(http.StatusUnauthorized) - jsonResponse = unauthorizedBody - case "bearer new-access-token": - writer.WriteHeader(secondReqResp.Status) - jsonResponse = secondReqResp.Body - default: - writer.WriteHeader(http.StatusInternalServerError) - } - - fmt.Fprintln(writer, jsonResponse) - } -} - -func testRefreshToken(t *testing.T, gateway Gateway, endpoint http.HandlerFunc) (apiResponse ApiResponse) { - authEndpoint := func(writer http.ResponseWriter, request *http.Request) { - fmt.Fprintln( - writer, - `{ "access_token": "new-access-token", "token_type": "bearer", "refresh_token": "new-refresh-token"}`, - ) - } - - apiServer := httptest.NewTLSServer(endpoint) - defer apiServer.Close() - - authServer := httptest.NewTLSServer(http.HandlerFunc(authEndpoint)) - defer authServer.Close() - - config, auth := createAuthenticationRepository(t, apiServer, authServer) - gateway.SetTokenRefresher(auth) - - request, apiResponse := gateway.NewRequest("POST", config.Target+"/v2/foo", config.AccessToken, strings.NewReader("expected body")) - assert.False(t, apiResponse.IsNotSuccessful()) - - apiResponse = gateway.PerformRequest(request) - return -} - -func createAuthenticationRepository(t *testing.T, apiServer *httptest.Server, authServer *httptest.Server) (*configuration.Configuration, api.AuthenticationRepository) { - configRepo := testconfig.FakeConfigRepository{} - configRepo.Delete() - config, err := configRepo.Get() - assert.NoError(t, err) - - config.AuthorizationEndpoint = authServer.URL - config.Target = apiServer.URL - config.AccessToken = "bearer initial-access-token" - config.RefreshToken = "initial-refresh-token" - - authGateway := NewUAAGateway() - authenticator := api.NewUAAAuthenticationRepository(authGateway, configRepo) - - return config, authenticator -} diff --git a/src/cf/net/http_client.go b/src/cf/net/http_client.go deleted file mode 100644 index b2762307cce..00000000000 --- a/src/cf/net/http_client.go +++ /dev/null @@ -1,96 +0,0 @@ -package net - -import ( - "cf/terminal" - "cf/trace" - "crypto/tls" - "errors" - "fmt" - "net/http" - "net/http/httputil" - "regexp" - "strings" -) - -const ( - PRIVATE_DATA_PLACEHOLDER = "[PRIVATE DATA HIDDEN]" -) - -func newHttpClient() *http.Client { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - Proxy: http.ProxyFromEnvironment, - } - return &http.Client{ - Transport: tr, - CheckRedirect: PrepareRedirect, - } -} - -func PrepareRedirect(req *http.Request, via []*http.Request) error { - if len(via) > 1 { - return errors.New("stopped after 1 redirect") - } - - prevReq := via[len(via)-1] - - req.Header.Set("Authorization", prevReq.Header.Get("Authorization")) - - dumpRequest(req) - - return nil -} - -func Sanitize(input string) (sanitized string) { - var sanitizeJson = func(propertyName string, json string) string { - re := regexp.MustCompile(fmt.Sprintf(`"%s":"[^"]*"`, propertyName)) - return re.ReplaceAllString(json, fmt.Sprintf(`"%s":"`+PRIVATE_DATA_PLACEHOLDER+`"`, propertyName)) - } - - re := regexp.MustCompile(`(?m)^Authorization: .*`) - sanitized = re.ReplaceAllString(input, "Authorization: "+PRIVATE_DATA_PLACEHOLDER) - re = regexp.MustCompile(`password=[^&]*&`) - sanitized = re.ReplaceAllString(sanitized, "password="+PRIVATE_DATA_PLACEHOLDER+"&") - - sanitized = sanitizeJson("access_token", sanitized) - sanitized = sanitizeJson("refresh_token", sanitized) - sanitized = sanitizeJson("token", sanitized) - - return -} - -func doRequest(request *http.Request) (response *http.Response, err error) { - httpClient := newHttpClient() - - dumpRequest(request) - - response, err = httpClient.Do(request) - if err != nil { - return - } - - dumpResponse(response) - return -} - -func dumpRequest(req *http.Request) { - shouldDisplayBody := !strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") - dumpedRequest, err := httputil.DumpRequest(req, shouldDisplayBody) - if err != nil { - trace.Logger.Printf("Error dumping request\n%s\n", err) - } else { - trace.Logger.Printf("\n%s\n%s\n", terminal.HeaderColor("REQUEST:"), Sanitize(string(dumpedRequest))) - if !shouldDisplayBody { - trace.Logger.Println("[MULTIPART/FORM-DATA CONTENT HIDDEN]") - } - } -} - -func dumpResponse(res *http.Response) { - dumpedResponse, err := httputil.DumpResponse(res, true) - if err != nil { - trace.Logger.Printf("Error dumping response\n%s\n", err) - } else { - trace.Logger.Printf("\n%s\n%s\n", terminal.HeaderColor("RESPONSE:"), Sanitize(string(dumpedResponse))) - } -} diff --git a/src/cf/net/http_client_test.go b/src/cf/net/http_client_test.go deleted file mode 100644 index f57ef466339..00000000000 --- a/src/cf/net/http_client_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package net_test - -import ( - . "cf/net" - "github.com/stretchr/testify/assert" - "net/http" - "testing" -) - -func TestSanitizingRemovesAuthorizationToken(t *testing.T) { - request := ` -REQUEST: -GET /v2/organizations HTTP/1.1 -Host: api.run.pivotal.io -Accept: application/json -Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI3NDRkNWQ1My0xODkxLTQzZjktYjNiMy1mMTQxNDZkYzQ4ZmUiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJlbWFpbCI6Im1nZWhhcmQrY2xpQHBpdm90YWxsYWJzLmNvbSIsImlhdCI6MTM3ODI0NzgxNiwiZXhwIjoxMzc4MjkxMDE2LCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.LL_QLO0SztGRENmU-9KA2WouOyPkKVENGQoUtjqrGR-UIekXMClH6fmKELzHtB69z3n9x7_jYJbvv32D-dX1J7p1CMWIDLOzXUnIUDK7cU5Q2yuYszf4v5anKiJtrKWU0_Pg87cQTZ_lWXAhdsi-bhLVR_pITxehfz7DKChjC8gh-FiuDvH5qHxxPqYHUl9jPso5OQ0y0fqZpLt8Yq23DKWaFAZehLnrhFltdQ_jSLy1QAYYZVD_HpQDf9NozKXruIvXhyIuwGj99QmUs3LSyNWecy822VqOoBtPYS6CLegMuWWlO64TJNrnZuh5YsOuW8SudJONx2wwEqARysJIHw -This is the body. Please don't get rid of me even though I contain Authorization: and some other text - ` - - expected := ` -REQUEST: -GET /v2/organizations HTTP/1.1 -Host: api.run.pivotal.io -Accept: application/json -Authorization: [PRIVATE DATA HIDDEN] -This is the body. Please don't get rid of me even though I contain Authorization: and some other text - ` - - assert.Equal(t, Sanitize(request), expected) -} - -func TestSanitizeRemovesPassword(t *testing.T) { - request := ` -POST /oauth/token HTTP/1.1 -Host: login.run.pivotal.io -Accept: application/json -Authorization: [PRIVATE DATA HIDDEN] -Content-Type: application/x-www-form-urlencoded - -grant_type=password&password=password&scope=&username=mgehard%2Bcli%40pivotallabs.com -` - - expected := ` -POST /oauth/token HTTP/1.1 -Host: login.run.pivotal.io -Accept: application/json -Authorization: [PRIVATE DATA HIDDEN] -Content-Type: application/x-www-form-urlencoded - -grant_type=password&password=[PRIVATE DATA HIDDEN]&scope=&username=mgehard%2Bcli%40pivotallabs.com -` - assert.Equal(t, Sanitize(request), expected) -} - -func TestSanitizeRemovesOauthTokensFromBody(t *testing.T) { - response := ` -HTTP/1.1 200 OK -Content-Length: 2132 -Cache-Control: no-cache -Cache-Control: no-store -Cache-Control: no-store -Connection: keep-alive -Content-Type: application/json;charset=UTF-8 -Date: Thu, 05 Sep 2013 16:31:43 GMT -Expires: Thu, 01 Jan 1970 00:00:00 GMT -Pragma: no-cache -Pragma: no-cache -Server: Apache-Coyote/1.1 - -{"access_token":"eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNmE3YzEzNi02NDk3LTRmYWYtODc5OS00YzQyZTFmM2M2ZjUiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJlbWFpbCI6Im1nZWhhcmQrY2xpQHBpdm90YWxsYWJzLmNvbSIsImlhdCI6MTM3ODM5ODcwMywiZXhwIjoxMzc4NDQxOTAzLCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.VZErs4AnXgAzEirSY1A0yV0xQItXiPqaMfpO__MBwCihEpMEtMKemvlUPn3HEKyOGINk9YzhPV30ILrBb0oPt9plCD42BLEtyr_cbeo-1zap6QuhN8YjAAKQgjNYKORSvgi9x13JrXtCGByviHVEBP39Zeum2ZoehZfClWS7YP9lUfqaIBWUDLLBQtT6AZRlbzLwH-MJ5GkH1DOkIXzuWBk0OXp4VNm38kxzLQMnOJ3aJTcWv3YBxJeIgasoQLadTPaEPLxDGeC7V6SqhGJdyyZVnGTOKLt5ict-fxDoX6CxFnT_ZuMvseSocPfS2Or0HR_FICHAv2_C_6yv_4aI7w","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjMjM2M2E3Yi04M2MwLTRiN2ItYjg0Zi1mNTM3MTA4ZGExZmEiLCJzdWIiOiIzM2U3ZmVkNy1iMWMyLTRjMjAtOTU0My0yMTBiMjc2ODM1MDgiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiaWF0IjoxMzc4Mzk4NzAzLCJleHAiOjEzODA5OTA3MDMsImNpZCI6ImNmIiwiaXNzIjoiaHR0cHM6Ly91YWEucnVuLnBpdm90YWwuaW8vb2F1dGgvdG9rZW4iLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX25hbWUiOiJtZ2VoYXJkK2NsaUBwaXZvdGFsbGFicy5jb20iLCJhdWQiOlsiY2xvdWRfY29udHJvbGxlci5yZWFkIiwiY2xvdWRfY29udHJvbGxlci53cml0ZSIsIm9wZW5pZCIsInBhc3N3b3JkLndyaXRlIl19.G8K9hVy2TGvxWEHMmVT86iQ5szMjnN0pWog2ASawpDiV8A4QODn9lJQq0G08LjjElV6wKQywAxM6eU8p32byW6RU9Tu-0iz9lW96aWSppTjsb4itbPLxsdMXLSRKOow0vuuGhwaTYx9OZIMpzNbXJVwbRRyWlhty6LVrEZp3hG37HO-N7g2oJdFZwxATaE63iL5ZnikcvKrPkBTKUGZ8OIAvsAlHQiEnbB8mfaw6Bh74ciTjOl0DYbHlZoEMQazXkLnY3INgCyErRcjtNkjRQGe6fOV4v1Wx3PAZ05gaBsAOaThgifz4Rmaf--hnrhtYI5F3g17tDmht6udZv1_C6A","expires_in":43199,"scope":"cloud_controller.read cloud_controller.write openid password.write","jti":"c6a7c136-6497-4faf-8799-4c42e1f3c6f5"} -` - - expected := ` -HTTP/1.1 200 OK -Content-Length: 2132 -Cache-Control: no-cache -Cache-Control: no-store -Cache-Control: no-store -Connection: keep-alive -Content-Type: application/json;charset=UTF-8 -Date: Thu, 05 Sep 2013 16:31:43 GMT -Expires: Thu, 01 Jan 1970 00:00:00 GMT -Pragma: no-cache -Pragma: no-cache -Server: Apache-Coyote/1.1 - -{"access_token":"[PRIVATE DATA HIDDEN]","token_type":"bearer","refresh_token":"[PRIVATE DATA HIDDEN]","expires_in":43199,"scope":"cloud_controller.read cloud_controller.write openid password.write","jti":"c6a7c136-6497-4faf-8799-4c42e1f3c6f5"} -` - - assert.Equal(t, Sanitize(response), expected) -} - -func TestSanitizeRemovesServiceAuthTokensFromBody(t *testing.T) { - response := ` -HTTP/1.1 200 OK -Content-Length: 2132 -Cache-Control: no-cache -Cache-Control: no-store -Cache-Control: no-store -Connection: keep-alive -Content-Type: application/json;charset=UTF-8 -Date: Thu, 05 Sep 2013 16:31:43 GMT -Expires: Thu, 01 Jan 1970 00:00:00 GMT -Pragma: no-cache -Pragma: no-cache -Server: Apache-Coyote/1.1 - -{"label":"some label","provider":"some provider","token":"some-token-with-stuff-in-it"} -` - - expected := ` -HTTP/1.1 200 OK -Content-Length: 2132 -Cache-Control: no-cache -Cache-Control: no-store -Cache-Control: no-store -Connection: keep-alive -Content-Type: application/json;charset=UTF-8 -Date: Thu, 05 Sep 2013 16:31:43 GMT -Expires: Thu, 01 Jan 1970 00:00:00 GMT -Pragma: no-cache -Pragma: no-cache -Server: Apache-Coyote/1.1 - -{"label":"some label","provider":"some provider","token":"[PRIVATE DATA HIDDEN]"} -` - - assert.Equal(t, Sanitize(response), expected) -} - -func TestPrepareRedirectTransfersAuthorizationHeader(t *testing.T) { - originalReq, err := http.NewRequest("GET", "/foo", nil) - assert.NoError(t, err) - originalReq.Header.Set("Authorization", "my-auth-token") - - redirectReq, err := http.NewRequest("GET", "/bar", nil) - assert.NoError(t, err) - - via := []*http.Request{originalReq} - - err = PrepareRedirect(redirectReq, via) - - assert.NoError(t, err) - assert.Equal(t, redirectReq.Header.Get("Authorization"), "my-auth-token") -} - -func TestPrepareRedirectFailsAfterOneRedirect(t *testing.T) { - firstReq, err := http.NewRequest("GET", "/foo", nil) - assert.NoError(t, err) - - secondReq, err := http.NewRequest("GET", "/manchu", nil) - assert.NoError(t, err) - - redirectReq, err := http.NewRequest("GET", "/bar", nil) - assert.NoError(t, err) - - via := []*http.Request{firstReq, secondReq} - - err = PrepareRedirect(redirectReq, via) - - assert.Error(t, err) -} diff --git a/src/cf/net/uaa_gateway.go b/src/cf/net/uaa_gateway.go deleted file mode 100644 index 36b17a165ef..00000000000 --- a/src/cf/net/uaa_gateway.go +++ /dev/null @@ -1,33 +0,0 @@ -package net - -import ( - "encoding/json" - "io/ioutil" - "net/http" -) - -type uaaErrorResponse struct { - Code string `json:"error"` - Description string `json:"error_description"` -} - -var uaaErrorHandler = func(response *http.Response) errorResponse { - invalidTokenCode := "invalid_token" - - jsonBytes, _ := ioutil.ReadAll(response.Body) - response.Body.Close() - - uaaResp := uaaErrorResponse{} - json.Unmarshal(jsonBytes, &uaaResp) - - code := uaaResp.Code - if code == invalidTokenCode { - code = INVALID_TOKEN_CODE - } - - return errorResponse{Code: code, Description: uaaResp.Description} -} - -func NewUAAGateway() Gateway { - return newGateway(uaaErrorHandler) -} diff --git a/src/cf/net/uaa_gateway_test.go b/src/cf/net/uaa_gateway_test.go deleted file mode 100644 index 3dfb84512a0..00000000000 --- a/src/cf/net/uaa_gateway_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package net_test - -import ( - . "cf/net" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -var failingUAARequest = func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusBadRequest) - jsonResponse := `{ "error": "foo", "error_description": "The foo is wrong..." }` - fmt.Fprintln(writer, jsonResponse) -} - -func TestUAAGatewayErrorHandling(t *testing.T) { - gateway := NewUAAGateway() - - ts := httptest.NewTLSServer(http.HandlerFunc(failingUAARequest)) - defer ts.Close() - - request, apiResponse := gateway.NewRequest("GET", ts.URL, "TOKEN", nil) - assert.False(t, apiResponse.IsNotSuccessful()) - - apiResponse = gateway.PerformRequest(request) - - assert.True(t, apiResponse.IsNotSuccessful()) - assert.Contains(t, apiResponse.Message, "The foo is wrong") - assert.Contains(t, apiResponse.ErrorCode, "foo") -} diff --git a/src/cf/requirements/application.go b/src/cf/requirements/application.go deleted file mode 100644 index c239c0d1df9..00000000000 --- a/src/cf/requirements/application.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type ApplicationRequirement interface { - Requirement - GetApplication() cf.Application -} - -type applicationApiRequirement struct { - name string - ui terminal.UI - appRepo api.ApplicationRepository - application cf.Application -} - -func newApplicationRequirement(name string, ui terminal.UI, aR api.ApplicationRepository) (req *applicationApiRequirement) { - req = new(applicationApiRequirement) - req.name = name - req.ui = ui - req.appRepo = aR - return -} - -func (req *applicationApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.application, apiResponse = req.appRepo.FindByName(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *applicationApiRequirement) GetApplication() cf.Application { - return req.application -} diff --git a/src/cf/requirements/application_test.go b/src/cf/requirements/application_test.go deleted file mode 100644 index e77d1f06895..00000000000 --- a/src/cf/requirements/application_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestApplicationReqExecute(t *testing.T) { - app := cf.Application{} - app.Name = "my-app" - app.Guid = "my-app-guid" - appRepo := &testapi.FakeApplicationRepository{FindByNameApp: app} - ui := new(testterm.FakeUI) - - appReq := newApplicationRequirement("foo", ui, appRepo) - success := appReq.Execute() - - assert.True(t, success) - assert.Equal(t, appRepo.FindByNameName, "foo") - assert.Equal(t, appReq.GetApplication(), app) -} - -func TestApplicationReqExecuteWhenApplicationNotFound(t *testing.T) { - appRepo := &testapi.FakeApplicationRepository{FindByNameNotFound: true} - ui := new(testterm.FakeUI) - - appReq := newApplicationRequirement("foo", ui, appRepo) - success := appReq.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/buildpack.go b/src/cf/requirements/buildpack.go deleted file mode 100644 index e1980e0a36b..00000000000 --- a/src/cf/requirements/buildpack.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type BuildpackRequirement interface { - Requirement - GetBuildpack() cf.Buildpack -} - -type buildpackApiRequirement struct { - name string - ui terminal.UI - buildpackRepo api.BuildpackRepository - buildpack cf.Buildpack -} - -func newBuildpackRequirement(name string, ui terminal.UI, bR api.BuildpackRepository) (req *buildpackApiRequirement) { - req = new(buildpackApiRequirement) - req.name = name - req.ui = ui - req.buildpackRepo = bR - return -} - -func (req *buildpackApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.buildpack, apiResponse = req.buildpackRepo.FindByName(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *buildpackApiRequirement) GetBuildpack() cf.Buildpack { - return req.buildpack -} diff --git a/src/cf/requirements/buildpack_test.go b/src/cf/requirements/buildpack_test.go deleted file mode 100644 index bc284557e4f..00000000000 --- a/src/cf/requirements/buildpack_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestBuildpackReqExecute(t *testing.T) { - buildpack := cf.Buildpack{} - buildpack.Name = "my-buildpack" - buildpack.Guid = "my-buildpack-guid" - buildpackRepo := &testapi.FakeBuildpackRepository{FindByNameBuildpack: buildpack} - ui := new(testterm.FakeUI) - - buildpackReq := newBuildpackRequirement("foo", ui, buildpackRepo) - success := buildpackReq.Execute() - - assert.True(t, success) - assert.Equal(t, buildpackRepo.FindByNameName, "foo") - assert.Equal(t, buildpackReq.GetBuildpack(), buildpack) -} - -func TestBuildpackReqExecuteWhenBuildpackNotFound(t *testing.T) { - buildpackRepo := &testapi.FakeBuildpackRepository{FindByNameNotFound: true} - ui := new(testterm.FakeUI) - - buildpackReq := newBuildpackRequirement("foo", ui, buildpackRepo) - success := buildpackReq.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/domain.go b/src/cf/requirements/domain.go deleted file mode 100644 index 5c271018813..00000000000 --- a/src/cf/requirements/domain.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type DomainRequirement interface { - Requirement - GetDomain() cf.Domain -} - -type domainApiRequirement struct { - name string - ui terminal.UI - domainRepo api.DomainRepository - domain cf.Domain -} - -func newDomainRequirement(name string, ui terminal.UI, domainRepo api.DomainRepository) (req *domainApiRequirement) { - req = new(domainApiRequirement) - req.name = name - req.ui = ui - req.domainRepo = domainRepo - return -} - -func (req *domainApiRequirement) Execute() bool { - var apiResponse net.ApiResponse - req.domain, apiResponse = req.domainRepo.FindByNameInCurrentSpace(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *domainApiRequirement) GetDomain() cf.Domain { - return req.domain -} diff --git a/src/cf/requirements/domain_test.go b/src/cf/requirements/domain_test.go deleted file mode 100644 index 3dc682eb0b5..00000000000 --- a/src/cf/requirements/domain_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestDomainReqExecute(t *testing.T) { - domain := cf.Domain{} - domain.Name = "example.com" - domain.Guid = "domain-guid" - domainRepo := &testapi.FakeDomainRepository{FindByNameDomain: domain} - ui := new(testterm.FakeUI) - - domainReq := newDomainRequirement("example.com", ui, domainRepo) - success := domainReq.Execute() - - assert.True(t, success) - assert.Equal(t, domainRepo.FindByNameInCurrentSpaceName, "example.com") - assert.Equal(t, domainReq.GetDomain(), domain) -} - -func TestDomainReqWhenDomainDoesNotExist(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{FindByNameNotFound: true} - ui := new(testterm.FakeUI) - - domainReq := newDomainRequirement("example.com", ui, domainRepo) - success := domainReq.Execute() - - assert.False(t, success) -} - -func TestDomainReqOnError(t *testing.T) { - domainRepo := &testapi.FakeDomainRepository{FindByNameErr: true} - ui := new(testterm.FakeUI) - - domainReq := newDomainRequirement("example.com", ui, domainRepo) - success := domainReq.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/factory.go b/src/cf/requirements/factory.go deleted file mode 100644 index c2fd8d454e3..00000000000 --- a/src/cf/requirements/factory.go +++ /dev/null @@ -1,118 +0,0 @@ -package requirements - -import ( - "cf/api" - "cf/configuration" - "cf/terminal" -) - -type Requirement interface { - Execute() (success bool) -} - -type Factory interface { - NewApplicationRequirement(name string) ApplicationRequirement - NewServiceInstanceRequirement(name string) ServiceInstanceRequirement - NewLoginRequirement() Requirement - NewValidAccessTokenRequirement() Requirement - NewSpaceRequirement(name string) SpaceRequirement - NewTargetedSpaceRequirement() Requirement - NewTargetedOrgRequirement() TargetedOrgRequirement - NewOrganizationRequirement(name string) OrganizationRequirement - NewDomainRequirement(name string) DomainRequirement - NewUserRequirement(username string) UserRequirement - NewBuildpackRequirement(buildpack string) BuildpackRequirement -} - -type apiRequirementFactory struct { - ui terminal.UI - config *configuration.Configuration - repoLocator api.RepositoryLocator -} - -func NewFactory(ui terminal.UI, config *configuration.Configuration, repoLocator api.RepositoryLocator) (factory apiRequirementFactory) { - return apiRequirementFactory{ui, config, repoLocator} -} - -func (f apiRequirementFactory) NewApplicationRequirement(name string) ApplicationRequirement { - return newApplicationRequirement( - name, - f.ui, - f.repoLocator.GetApplicationRepository(), - ) -} - -func (f apiRequirementFactory) NewServiceInstanceRequirement(name string) ServiceInstanceRequirement { - return newServiceInstanceRequirement( - name, - f.ui, - f.repoLocator.GetServiceRepository(), - ) -} - -func (f apiRequirementFactory) NewLoginRequirement() Requirement { - return newLoginRequirement( - f.ui, - f.config, - ) -} -func (f apiRequirementFactory) NewValidAccessTokenRequirement() Requirement { - return newValidAccessTokenRequirement( - f.ui, - f.repoLocator.GetApplicationRepository(), - ) -} - -func (f apiRequirementFactory) NewSpaceRequirement(name string) SpaceRequirement { - return newSpaceRequirement( - name, - f.ui, - f.repoLocator.GetSpaceRepository(), - ) -} - -func (f apiRequirementFactory) NewTargetedSpaceRequirement() Requirement { - return newTargetedSpaceRequirement( - f.ui, - f.config, - ) -} - -func (f apiRequirementFactory) NewTargetedOrgRequirement() TargetedOrgRequirement { - return newTargetedOrgRequirement( - f.ui, - f.config, - ) -} - -func (f apiRequirementFactory) NewOrganizationRequirement(name string) OrganizationRequirement { - return newOrganizationRequirement( - name, - f.ui, - f.repoLocator.GetOrganizationRepository(), - ) -} - -func (f apiRequirementFactory) NewDomainRequirement(name string) DomainRequirement { - return newDomainRequirement( - name, - f.ui, - f.repoLocator.GetDomainRepository(), - ) -} - -func (f apiRequirementFactory) NewUserRequirement(username string) UserRequirement { - return newUserRequirement( - username, - f.ui, - f.repoLocator.GetUserRepository(), - ) -} - -func (f apiRequirementFactory) NewBuildpackRequirement(buildpack string) BuildpackRequirement { - return newBuildpackRequirement( - buildpack, - f.ui, - f.repoLocator.GetBuildpackRepository(), - ) -} diff --git a/src/cf/requirements/login.go b/src/cf/requirements/login.go deleted file mode 100644 index 3fce3b97bf3..00000000000 --- a/src/cf/requirements/login.go +++ /dev/null @@ -1,23 +0,0 @@ -package requirements - -import ( - "cf/configuration" - "cf/terminal" -) - -type LoginRequirement struct { - ui terminal.UI - config *configuration.Configuration -} - -func newLoginRequirement(ui terminal.UI, config *configuration.Configuration) LoginRequirement { - return LoginRequirement{ui, config} -} - -func (req LoginRequirement) Execute() (success bool) { - if !req.config.IsLoggedIn() { - req.ui.Say(terminal.NotLoggedInText()) - return false - } - return true -} diff --git a/src/cf/requirements/login_test.go b/src/cf/requirements/login_test.go deleted file mode 100644 index aa78eb2f962..00000000000 --- a/src/cf/requirements/login_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package requirements - -import ( - "cf/configuration" - "github.com/stretchr/testify/assert" - testterm "testhelpers/terminal" - "testing" -) - -func TestLoginRequirement(t *testing.T) { - ui := new(testterm.FakeUI) - config := &configuration.Configuration{ - AccessToken: "foo bar token", - } - - req := newLoginRequirement(ui, config) - success := req.Execute() - assert.True(t, success) - - config = &configuration.Configuration{ - AccessToken: "", - } - - req = newLoginRequirement(ui, config) - success = req.Execute() - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "Not logged in.") -} diff --git a/src/cf/requirements/organization.go b/src/cf/requirements/organization.go deleted file mode 100644 index 8d223511566..00000000000 --- a/src/cf/requirements/organization.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type OrganizationRequirement interface { - Requirement - GetOrganization() cf.Organization -} - -type organizationApiRequirement struct { - name string - ui terminal.UI - orgRepo api.OrganizationRepository - org cf.Organization -} - -func newOrganizationRequirement(name string, ui terminal.UI, sR api.OrganizationRepository) (req *organizationApiRequirement) { - req = new(organizationApiRequirement) - req.name = name - req.ui = ui - req.orgRepo = sR - return -} - -func (req *organizationApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.org, apiResponse = req.orgRepo.FindByName(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *organizationApiRequirement) GetOrganization() cf.Organization { - return req.org -} diff --git a/src/cf/requirements/organization_test.go b/src/cf/requirements/organization_test.go deleted file mode 100644 index 7aa083d2312..00000000000 --- a/src/cf/requirements/organization_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestOrgReqExecute(t *testing.T) { - org := cf.Organization{} - org.Name = "my-org" - org.Guid = "my-org-guid" - orgRepo := &testapi.FakeOrgRepository{FindByNameOrganization: org} - ui := new(testterm.FakeUI) - - orgReq := newOrganizationRequirement("foo", ui, orgRepo) - success := orgReq.Execute() - - assert.True(t, success) - assert.Equal(t, orgRepo.FindByNameName, "foo") - assert.Equal(t, orgReq.GetOrganization(), org) -} - -func TestOrgReqWhenOrgDoesNotExist(t *testing.T) { - orgRepo := &testapi.FakeOrgRepository{FindByNameNotFound: true} - ui := new(testterm.FakeUI) - - orgReq := newOrganizationRequirement("foo", ui, orgRepo) - success := orgReq.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/service_instance.go b/src/cf/requirements/service_instance.go deleted file mode 100644 index 309c7f66183..00000000000 --- a/src/cf/requirements/service_instance.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type ServiceInstanceRequirement interface { - Requirement - GetServiceInstance() cf.ServiceInstance -} - -type serviceInstanceApiRequirement struct { - name string - ui terminal.UI - serviceRepo api.ServiceRepository - serviceInstance cf.ServiceInstance -} - -func newServiceInstanceRequirement(name string, ui terminal.UI, sR api.ServiceRepository) (req *serviceInstanceApiRequirement) { - req = new(serviceInstanceApiRequirement) - req.name = name - req.ui = ui - req.serviceRepo = sR - return -} - -func (req *serviceInstanceApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.serviceInstance, apiResponse = req.serviceRepo.FindInstanceByName(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *serviceInstanceApiRequirement) GetServiceInstance() cf.ServiceInstance { - return req.serviceInstance -} diff --git a/src/cf/requirements/service_instance_test.go b/src/cf/requirements/service_instance_test.go deleted file mode 100644 index d8f0564b6e1..00000000000 --- a/src/cf/requirements/service_instance_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestServiceInstanceReqExecute(t *testing.T) { - instance := cf.ServiceInstance{} - instance.Name = "my-service" - instance.Guid = "my-service-guid" - repo := &testapi.FakeServiceRepo{FindInstanceByNameServiceInstance: instance} - ui := new(testterm.FakeUI) - - req := newServiceInstanceRequirement("foo", ui, repo) - success := req.Execute() - - assert.True(t, success) - assert.Equal(t, repo.FindInstanceByNameName, "foo") - assert.Equal(t, req.GetServiceInstance(), instance) -} - -func TestServiceInstanceReqExecuteWhenServiceInstanceNotFound(t *testing.T) { - repo := &testapi.FakeServiceRepo{FindInstanceByNameNotFound: true} - ui := new(testterm.FakeUI) - - req := newServiceInstanceRequirement("foo", ui, repo) - success := req.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/space.go b/src/cf/requirements/space.go deleted file mode 100644 index 67dfeba1375..00000000000 --- a/src/cf/requirements/space.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type SpaceRequirement interface { - Requirement - GetSpace() cf.Space -} - -type spaceApiRequirement struct { - name string - ui terminal.UI - spaceRepo api.SpaceRepository - space cf.Space -} - -func newSpaceRequirement(name string, ui terminal.UI, sR api.SpaceRepository) (req *spaceApiRequirement) { - req = new(spaceApiRequirement) - req.name = name - req.ui = ui - req.spaceRepo = sR - return -} - -func (req *spaceApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.space, apiResponse = req.spaceRepo.FindByName(req.name) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *spaceApiRequirement) GetSpace() cf.Space { - return req.space -} diff --git a/src/cf/requirements/space_test.go b/src/cf/requirements/space_test.go deleted file mode 100644 index e736a84d6a8..00000000000 --- a/src/cf/requirements/space_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestSpaceReqExecute(t *testing.T) { - space := cf.Space{} - space.Name = "my-space" - space.Guid = "my-space-guid" - spaceRepo := &testapi.FakeSpaceRepository{FindByNameSpace: space} - ui := new(testterm.FakeUI) - - spaceReq := newSpaceRequirement("foo", ui, spaceRepo) - success := spaceReq.Execute() - - assert.True(t, success) - assert.Equal(t, spaceRepo.FindByNameName, "foo") - assert.Equal(t, spaceReq.GetSpace(), space) -} - -func TestSpaceReqExecuteWhenSpaceNotFound(t *testing.T) { - spaceRepo := &testapi.FakeSpaceRepository{FindByNameNotFound: true} - ui := new(testterm.FakeUI) - - spaceReq := newSpaceRequirement("foo", ui, spaceRepo) - success := spaceReq.Execute() - - assert.False(t, success) -} diff --git a/src/cf/requirements/targeted_organization.go b/src/cf/requirements/targeted_organization.go deleted file mode 100644 index 98b8a542e39..00000000000 --- a/src/cf/requirements/targeted_organization.go +++ /dev/null @@ -1,37 +0,0 @@ -package requirements - -import ( - "cf" - "cf/configuration" - "cf/terminal" - "fmt" -) - -type TargetedOrgRequirement interface { - Requirement - GetOrganizationFields() cf.OrganizationFields -} - -type targetedOrgApiRequirement struct { - ui terminal.UI - config *configuration.Configuration -} - -func newTargetedOrgRequirement(ui terminal.UI, config *configuration.Configuration) TargetedOrgRequirement { - return targetedOrgApiRequirement{ui, config} -} - -func (req targetedOrgApiRequirement) Execute() (success bool) { - if !req.config.HasOrganization() { - message := fmt.Sprintf("No org targeted, use '%s' to target an org.", - terminal.CommandColor(cf.Name()+" target -o ORG")) - req.ui.Failed(message) - return false - } - - return true -} - -func (req targetedOrgApiRequirement) GetOrganizationFields() (org cf.OrganizationFields) { - return req.config.OrganizationFields -} diff --git a/src/cf/requirements/targeted_organization_test.go b/src/cf/requirements/targeted_organization_test.go deleted file mode 100644 index 4497a63bffd..00000000000 --- a/src/cf/requirements/targeted_organization_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package requirements - -import ( - "cf" - "cf/configuration" - "github.com/stretchr/testify/assert" - testterm "testhelpers/terminal" - "testing" -) - -func TestTargetedOrgRequirement(t *testing.T) { - ui := new(testterm.FakeUI) - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - config := &configuration.Configuration{ - OrganizationFields: org, - } - - req := newTargetedOrgRequirement(ui, config) - success := req.Execute() - assert.True(t, success) - - config.OrganizationFields = cf.OrganizationFields{} - - req = newTargetedOrgRequirement(ui, config) - success = req.Execute() - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "No org targeted") -} diff --git a/src/cf/requirements/targeted_space.go b/src/cf/requirements/targeted_space.go deleted file mode 100644 index febd009d38b..00000000000 --- a/src/cf/requirements/targeted_space.go +++ /dev/null @@ -1,34 +0,0 @@ -package requirements - -import ( - "cf" - "cf/configuration" - "cf/terminal" - "fmt" -) - -type TargetedSpaceRequirement struct { - ui terminal.UI - config *configuration.Configuration -} - -func newTargetedSpaceRequirement(ui terminal.UI, config *configuration.Configuration) TargetedSpaceRequirement { - return TargetedSpaceRequirement{ui, config} -} - -func (req TargetedSpaceRequirement) Execute() (success bool) { - if !req.config.HasOrganization() { - message := fmt.Sprintf("No org and space targeted, use '%s' to target an org and space", - terminal.CommandColor(cf.Name()+" target -o ORG -s SPACE")) - req.ui.Failed(message) - return false - } - - if !req.config.HasSpace() { - message := fmt.Sprintf("No space targeted, use '%s' to target a space", terminal.CommandColor("cf target -s")) - req.ui.Failed(message) - return false - } - - return true -} diff --git a/src/cf/requirements/targeted_space_test.go b/src/cf/requirements/targeted_space_test.go deleted file mode 100644 index 01a707d7fb7..00000000000 --- a/src/cf/requirements/targeted_space_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package requirements - -import ( - "cf" - "cf/configuration" - "github.com/stretchr/testify/assert" - testterm "testhelpers/terminal" - "testing" -) - -func TestSpaceRequirement(t *testing.T) { - ui := new(testterm.FakeUI) - org := cf.OrganizationFields{} - org.Name = "my-org" - org.Guid = "my-org-guid" - space := cf.SpaceFields{} - space.Name = "my-space" - space.Guid = "my-space-guid" - config := &configuration.Configuration{ - OrganizationFields: org, - - SpaceFields: space, - } - - req := newTargetedSpaceRequirement(ui, config) - success := req.Execute() - assert.True(t, success) - - config.SpaceFields = cf.SpaceFields{} - - req = newTargetedSpaceRequirement(ui, config) - success = req.Execute() - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "No space targeted") - - ui.ClearOutputs() - config.OrganizationFields = cf.OrganizationFields{} - - req = newTargetedSpaceRequirement(ui, config) - success = req.Execute() - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "FAILED") - assert.Contains(t, ui.Outputs[1], "No org and space targeted") -} diff --git a/src/cf/requirements/user.go b/src/cf/requirements/user.go deleted file mode 100644 index fd8d984a3af..00000000000 --- a/src/cf/requirements/user.go +++ /dev/null @@ -1,44 +0,0 @@ -package requirements - -import ( - "cf" - "cf/api" - "cf/net" - "cf/terminal" -) - -type UserRequirement interface { - Requirement - GetUser() cf.UserFields -} - -type userApiRequirement struct { - username string - ui terminal.UI - userRepo api.UserRepository - user cf.UserFields -} - -func newUserRequirement(username string, ui terminal.UI, userRepo api.UserRepository) (req *userApiRequirement) { - req = new(userApiRequirement) - req.username = username - req.ui = ui - req.userRepo = userRepo - return -} - -func (req *userApiRequirement) Execute() (success bool) { - var apiResponse net.ApiResponse - req.user, apiResponse = req.userRepo.FindByUsername(req.username) - - if apiResponse.IsNotSuccessful() { - req.ui.Failed(apiResponse.Message) - return false - } - - return true -} - -func (req *userApiRequirement) GetUser() cf.UserFields { - return req.user -} diff --git a/src/cf/requirements/user_test.go b/src/cf/requirements/user_test.go deleted file mode 100644 index bc27a89f5ae..00000000000 --- a/src/cf/requirements/user_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package requirements - -import ( - "cf" - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestUserReqExecute(t *testing.T) { - user := cf.UserFields{} - user.Username = "my-user" - user.Guid = "my-user-guid" - - userRepo := &testapi.FakeUserRepository{FindByUsernameUserFields: user} - ui := new(testterm.FakeUI) - - userReq := newUserRequirement("foo", ui, userRepo) - success := userReq.Execute() - - assert.True(t, success) - assert.Equal(t, userRepo.FindByUsernameUsername, "foo") - assert.Equal(t, userReq.GetUser(), user) -} - -func TestUserReqWhenUserDoesNotExist(t *testing.T) { - userRepo := &testapi.FakeUserRepository{FindByUsernameNotFound: true} - ui := new(testterm.FakeUI) - - userReq := newUserRequirement("foo", ui, userRepo) - success := userReq.Execute() - - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "FAILED") -} diff --git a/src/cf/requirements/valid_access_token.go b/src/cf/requirements/valid_access_token.go deleted file mode 100644 index d2e4c1afc8b..00000000000 --- a/src/cf/requirements/valid_access_token.go +++ /dev/null @@ -1,26 +0,0 @@ -package requirements - -import ( - "cf/api" - "cf/terminal" -) - -type ValidAccessTokenRequirement struct { - ui terminal.UI - appRepo api.ApplicationRepository -} - -func newValidAccessTokenRequirement(ui terminal.UI, appRepo api.ApplicationRepository) ValidAccessTokenRequirement { - return ValidAccessTokenRequirement{ui, appRepo} -} - -func (req ValidAccessTokenRequirement) Execute() (success bool) { - _, apiResponse := req.appRepo.FindByName("checking_for_valid_access_token") - - if apiResponse.IsNotSuccessful() && apiResponse.StatusCode == 401 { - req.ui.Say(terminal.NotLoggedInText()) - return false - } - - return true -} diff --git a/src/cf/requirements/valid_access_token_test.go b/src/cf/requirements/valid_access_token_test.go deleted file mode 100644 index aff85726733..00000000000 --- a/src/cf/requirements/valid_access_token_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package requirements - -import ( - "github.com/stretchr/testify/assert" - testapi "testhelpers/api" - testterm "testhelpers/terminal" - "testing" -) - -func TestValidAccessRequirement(t *testing.T) { - ui := new(testterm.FakeUI) - appRepo := &testapi.FakeApplicationRepository{ - FindByNameAuthErr: true, - } - - req := newValidAccessTokenRequirement(ui, appRepo) - success := req.Execute() - assert.False(t, success) - assert.Contains(t, ui.Outputs[0], "Not logged in.") - - appRepo.FindByNameAuthErr = false - - req = newValidAccessTokenRequirement(ui, appRepo) - success = req.Execute() - assert.True(t, success) -} diff --git a/src/cf/terminal/color.go b/src/cf/terminal/color.go deleted file mode 100644 index ca2fbe5d538..00000000000 --- a/src/cf/terminal/color.go +++ /dev/null @@ -1,110 +0,0 @@ -package terminal - -import ( - "fmt" - "os" - "regexp" - "runtime" -) - -type Color uint - -const ( - red Color = 31 - green = 32 - yellow = 33 - // blue = 34 - magenta = 35 - cyan = 36 - grey = 37 - white = 38 -) - -func colorize(message string, color Color, bold bool) string { - if runtime.GOOS == "windows" || os.Getenv("CF_COLOR") != "true" { - return message - } - - attr := 0 - if bold { - attr = 1 - } - - return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message) -} - -func decolorize(message string) string { - reg, err := regexp.Compile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`) - if err != nil { - panic(err) - } - return string(reg.ReplaceAll([]byte(message), []byte(""))) -} - -func HeaderColor(message string) string { - return colorize(message, white, true) -} - -func TableContentColor(message string) string { - return colorize(message, grey, false) -} - -func CommandColor(message string) string { - return colorize(message, yellow, true) -} - -func StartedColor(message string) string { - return colorize(message, grey, true) -} - -func StoppedColor(message string) string { - return colorize(message, grey, true) -} - -func AdvisoryColor(message string) string { - return colorize(message, yellow, true) -} - -func CrashedColor(message string) string { - return colorize(message, red, true) -} - -func FailureColor(message string) string { - return colorize(message, red, true) -} - -func SuccessColor(message string) string { - return colorize(message, green, true) -} - -func EntityNameColor(message string) string { - return colorize(message, cyan, true) -} - -func PromptColor(message string) string { - return colorize(message, cyan, true) -} - -func TableContentHeaderColor(message string) string { - return colorize(message, cyan, true) -} - -func WarningColor(message string) string { - return colorize(message, magenta, true) -} - -func LogStdoutColor(message string) string { - return colorize(message, white, false) -} - -func LogStderrColor(message string) string { - return colorize(message, red, false) -} - -func LogAppHeaderColor(message string) string { - return colorize(message, yellow, true) -} - -func LogSysHeaderColor(message string) string { - return colorize(message, cyan, true) -} diff --git a/src/cf/terminal/color_test.go b/src/cf/terminal/color_test.go deleted file mode 100644 index efb42d0b383..00000000000 --- a/src/cf/terminal/color_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package terminal - -import ( - "github.com/stretchr/testify/assert" - "os" - "runtime" - "testing" -) - -func TestColorize(t *testing.T) { - os.Setenv("CF_COLOR", "true") - text := "Hello World" - colorizedText := colorize(text, red, true) - - if runtime.GOOS == "windows" { - assert.Equal(t, colorizedText, "Hello World") - } else { - assert.Equal(t, colorizedText, "\033[1;31mHello World\033[0m") - } -} diff --git a/src/cf/terminal/table.go b/src/cf/terminal/table.go deleted file mode 100644 index 93c7701ade3..00000000000 --- a/src/cf/terminal/table.go +++ /dev/null @@ -1,79 +0,0 @@ -package terminal - -import ( - "fmt" - "strings" -) - -type Table interface { - Print(rows [][]string) -} - -type PrintableTable struct { - ui UI - header []string - headerPrinted bool - maxSizes []int -} - -func NewTable(ui UI, header []string) Table { - return &PrintableTable{ - ui: ui, - header: header, - maxSizes: make([]int, len(header)), - } -} - -func (t *PrintableTable) Print(rows [][]string) { - for _, row := range append(rows, t.header) { - t.calculateMaxSize(row) - } - - if t.headerPrinted == false { - t.printHeader() - t.headerPrinted = true - } - - for _, line := range rows { - t.printRow(line) - } -} - -func (t *PrintableTable) calculateMaxSize(row []string) { - for index, value := range row { - cellLength := len(decolorize(value)) - if t.maxSizes[index] < cellLength { - t.maxSizes[index] = cellLength - } - } -} - -func (t *PrintableTable) printHeader() { - output := "" - for col, value := range t.header { - output = output + t.cellValue(col, HeaderColor(value)) - } - t.ui.Say(output) -} - -func (t *PrintableTable) printRow(row []string) { - output := "" - for col, value := range row { - if col == 0 { - value = TableContentHeaderColor(value) - } else { - value = TableContentColor(value) - } - - output = output + t.cellValue(col, value) - } - t.ui.Say(output) -} - -func (t *PrintableTable) cellValue(col int, value string) string { - padding := "" - if col < len(t.header)-1 { - padding = strings.Repeat(" ", t.maxSizes[col]-len(decolorize(value))) - } - return fmt.Sprintf("%s%s ", value, padding) -} diff --git a/src/cf/terminal/ui.go b/src/cf/terminal/ui.go deleted file mode 100644 index 1990b3e2318..00000000000 --- a/src/cf/terminal/ui.go +++ /dev/null @@ -1,177 +0,0 @@ -package terminal - -import ( - "cf" - "cf/configuration" - "cf/trace" - "fmt" - "github.com/codegangsta/cli" - "io" - "os" - "strings" - "time" -) - -type ColoringFunction func(value string, row int, col int) string - -func NotLoggedInText() string { - return fmt.Sprintf("Not logged in. Use '%s' to log in.", CommandColor(cf.Name()+" login")) -} - -type UI interface { - PrintPaginator(rows []string, err error) - Say(message string, args ...interface{}) - Warn(message string, args ...interface{}) - Ask(prompt string, args ...interface{}) (answer string) - AskForPassword(prompt string, args ...interface{}) (answer string) - Confirm(message string, args ...interface{}) bool - Ok() - Failed(message string, args ...interface{}) - FailWithUsage(ctxt *cli.Context, cmdName string) - ConfigFailure(err error) - ShowConfiguration(*configuration.Configuration) - LoadingIndication() - Wait(duration time.Duration) - DisplayTable(table [][]string) - Table(headers []string) Table -} - -type terminalUI struct { -} - -var stdin io.Reader = os.Stdin - -func NewUI() UI { - return terminalUI{} -} - -func (c terminalUI) PrintPaginator(rows []string, err error) { - if err != nil { - c.Failed(err.Error()) - return - } - - for _, row := range rows { - c.Say(row) - } -} - -func (c terminalUI) Say(message string, args ...interface{}) { - fmt.Printf(message+"\n", args...) - return -} - -func (c terminalUI) Warn(message string, args ...interface{}) { - message = fmt.Sprintf(message, args...) - c.Say(WarningColor(message)) - return -} - -func (c terminalUI) Confirm(message string, args ...interface{}) bool { - response := c.Ask(message, args...) - switch strings.ToLower(response) { - case "y", "yes": - return true - } - return false -} - -func (c terminalUI) Ask(prompt string, args ...interface{}) (answer string) { - fmt.Println("") - fmt.Printf(prompt+" ", args...) - fmt.Fscanln(stdin, &answer) - return -} - -func (c terminalUI) Ok() { - c.Say(SuccessColor("OK")) -} - -func (c terminalUI) Failed(message string, args ...interface{}) { - message = fmt.Sprintf(message, args...) - c.Say(FailureColor("FAILED")) - c.Say(message) - - trace.Logger.Print("FAILED") - trace.Logger.Print(message) - os.Exit(1) -} - -func (c terminalUI) FailWithUsage(ctxt *cli.Context, cmdName string) { - c.Say(FailureColor("FAILED")) - c.Say("Incorrect Usage.\n") - cli.ShowCommandHelp(ctxt, cmdName) - c.Say("") - os.Exit(1) -} - -func (c terminalUI) ConfigFailure(err error) { - c.Failed("Please use 'cf api' to set an API endpoint and then 'cf login' to login.") -} - -func (ui terminalUI) ShowConfiguration(config *configuration.Configuration) { - ui.Say("API endpoint: %s (API version: %s)", - EntityNameColor(config.Target), - EntityNameColor(config.ApiVersion)) - - if !config.IsLoggedIn() { - ui.Say(NotLoggedInText()) - } else { - ui.Say("User: %s", EntityNameColor(config.UserEmail())) - } - - if config.HasOrganization() { - ui.Say("Org: %s", EntityNameColor(config.OrganizationFields.Name)) - } - - if config.HasSpace() { - ui.Say("Space: %s", EntityNameColor(config.SpaceFields.Name)) - } -} - -func (c terminalUI) LoadingIndication() { - fmt.Print(".") -} - -func (c terminalUI) Wait(duration time.Duration) { - time.Sleep(duration) -} - -func (ui terminalUI) Table(headers []string) Table { - return NewTable(ui, headers) -} - -func (ui terminalUI) DisplayTable(table [][]string) { - - columnCount := len(table[0]) - maxSizes := make([]int, columnCount) - - for _, line := range table { - for index, value := range line { - cellLength := len(decolorize(value)) - if maxSizes[index] < cellLength { - maxSizes[index] = cellLength - } - } - } - - for row, line := range table { - for col, value := range line { - padding := strings.Repeat(" ", maxSizes[col]-len(decolorize(value))) - value = tableColoringFunc(value, row, col) - fmt.Printf("%s%s ", value, padding) - } - fmt.Print("\n") - } -} - -func tableColoringFunc(value string, row int, col int) string { - switch { - case row == 0: - return HeaderColor(value) - case col == 0 && row > 0: - return TableContentHeaderColor(value) - } - - return TableContentColor(value) -} diff --git a/src/cf/terminal/ui_test.go b/src/cf/terminal/ui_test.go deleted file mode 100644 index 30fd6b1188e..00000000000 --- a/src/cf/terminal/ui_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package terminal - -import ( - "bytes" - "github.com/stretchr/testify/assert" - "io" - "os" - "testing" -) - -func TestSayWithStringOnly(t *testing.T) { - ui := new(terminalUI) - out := captureOutput(func() { - ui.Say("Hello") - }) - - assert.Equal(t, "Hello\n", out) -} - -func TestSayWithStringWithFormat(t *testing.T) { - ui := new(terminalUI) - out := captureOutput(func() { - ui.Say("Hello %s", "World!") - }) - - assert.Equal(t, "Hello World!\n", out) -} - -func TestConfirmYes(t *testing.T) { - simulateStdin("y\n", func() { - ui := new(terminalUI) - - var result bool - out := captureOutput(func() { - result = ui.Confirm("Hello %s", "World?") - }) - - assert.True(t, result) - assert.Contains(t, out, "Hello World?") - }) -} - -func TestConfirmNo(t *testing.T) { - simulateStdin("wat\n", func() { - ui := new(terminalUI) - - var result bool - out := captureOutput(func() { - result = ui.Confirm("Hello %s", "World?") - }) - - assert.False(t, result) - assert.Contains(t, out, "Hello World?") - }) -} - -func simulateStdin(input string, block func()) { - defer func() { - stdin = os.Stdin - }() - - stdinReader, stdinWriter := io.Pipe() - stdin = stdinReader - - go func() { - stdinWriter.Write([]byte(input)) - defer stdinWriter.Close() - }() - - block() -} - -func captureOutput(f func()) string { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - f() - - outC := make(chan string) - - go func() { - var buf bytes.Buffer - io.Copy(&buf, r) - outC <- buf.String() - }() - - w.Close() - os.Stdout = old - return <-outC -} diff --git a/src/cf/trace/trace.go b/src/cf/trace/trace.go deleted file mode 100644 index 7cdc7cd7c2d..00000000000 --- a/src/cf/trace/trace.go +++ /dev/null @@ -1,60 +0,0 @@ -package trace - -import ( - "fileutils" - "io" - "log" - "os" -) - -const CF_TRACE = "CF_TRACE" - -type Printer interface { - Print(v ...interface{}) - Printf(format string, v ...interface{}) - Println(v ...interface{}) -} - -type nullLogger struct{} - -func (*nullLogger) Print(v ...interface{}) {} -func (*nullLogger) Printf(format string, v ...interface{}) {} -func (*nullLogger) Println(v ...interface{}) {} - -var stdOut io.Writer = os.Stdout -var Logger Printer - -func init() { - Logger = NewLogger() -} - -func SetStdout(s io.Writer) { - stdOut = s -} - -func NewLogger() Printer { - cf_trace := os.Getenv(CF_TRACE) - switch cf_trace { - case "", "false": - return new(nullLogger) - case "true": - return newStdoutLogger() - default: - return newFileLogger(cf_trace) - } -} - -func newStdoutLogger() Printer { - return log.New(stdOut, "", 0) -} - -func newFileLogger(path string) Printer { - file, err := fileutils.OpenFile(path) - if err != nil { - logger := newStdoutLogger() - logger.Printf("CF_TRACE ERROR CREATING LOG FILE %s:\n%s", path, err) - return logger - } - - return log.New(file, "", 0) -} diff --git a/src/cf/trace/trace_test.go b/src/cf/trace/trace_test.go deleted file mode 100644 index b089edf2588..00000000000 --- a/src/cf/trace/trace_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package trace_test - -import ( - "bytes" - "cf/trace" - "fileutils" - "github.com/stretchr/testify/assert" - "io/ioutil" - "os" - "runtime" - "testing" -) - -func TestTraceSetToFalse(t *testing.T) { - stdOut := bytes.NewBuffer([]byte{}) - trace.SetStdout(stdOut) - - os.Setenv(trace.CF_TRACE, "false") - - logger := trace.NewLogger() - logger.Print("hello world") - - result, _ := ioutil.ReadAll(stdOut) - assert.Equal(t, string(result), "") -} - -func TestTraceSetToTrue(t *testing.T) { - stdOut := bytes.NewBuffer([]byte{}) - trace.SetStdout(stdOut) - - os.Setenv(trace.CF_TRACE, "true") - - logger := trace.NewLogger() - logger.Print("hello world") - - result, _ := ioutil.ReadAll(stdOut) - assert.Contains(t, string(result), "hello world") -} - -func TestTraceSetToFile(t *testing.T) { - stdOut := bytes.NewBuffer([]byte{}) - trace.SetStdout(stdOut) - - fileutils.TempFile("trace_test", func(file *os.File, err error) { - assert.NoError(t, err) - file.Write([]byte("pre-existing content")) - - os.Setenv(trace.CF_TRACE, file.Name()) - - logger := trace.NewLogger() - logger.Print("hello world") - - file.Seek(0, os.SEEK_SET) - result, err := ioutil.ReadAll(file) - assert.NoError(t, err) - - byteString := string(result) - assert.Contains(t, byteString, "pre-existing content") - assert.Contains(t, byteString, "hello world") - - result, _ = ioutil.ReadAll(stdOut) - assert.Equal(t, string(result), "") - }) -} - -func TestTraceSetToInvalidFile(t *testing.T) { - if runtime.GOOS != "windows" { - stdOut := bytes.NewBuffer([]byte{}) - trace.SetStdout(stdOut) - - fileutils.TempFile("trace_test", func(file *os.File, err error) { - assert.NoError(t, err) - - file.Chmod(0000) - - os.Setenv(trace.CF_TRACE, file.Name()) - - logger := trace.NewLogger() - logger.Print("hello world") - - result, _ := ioutil.ReadAll(file) - assert.Equal(t, string(result), "") - - result, _ = ioutil.ReadAll(stdOut) - assert.Contains(t, string(result), "hello world") - }) - } -} diff --git a/src/cf/user_roles.go b/src/cf/user_roles.go deleted file mode 100644 index 8eeb14c5059..00000000000 --- a/src/cf/user_roles.go +++ /dev/null @@ -1,22 +0,0 @@ -package cf - -const ( - ORG_MANAGER = "OrgManager" - BILLING_MANAGER = "BillingManager" - ORG_AUDITOR = "OrgAuditor" - SPACE_MANAGER = "SpaceManager" - SPACE_DEVELOPER = "SpaceDeveloper" - SPACE_AUDITOR = "SpaceAuditor" -) - -var UserInputToOrgRole = map[string]string{ - "OrgManager": ORG_MANAGER, - "BillingManager": BILLING_MANAGER, - "OrgAuditor": ORG_AUDITOR, -} - -var UserInputToSpaceRole = map[string]string{ - "SpaceManager": SPACE_MANAGER, - "SpaceDeveloper": SPACE_DEVELOPER, - "SpaceAuditor": SPACE_AUDITOR, -} diff --git a/src/cf/zipper.go b/src/cf/zipper.go deleted file mode 100644 index b24b1f130ed..00000000000 --- a/src/cf/zipper.go +++ /dev/null @@ -1,69 +0,0 @@ -package cf - -import ( - "archive/zip" - "errors" - "fileutils" - "os" - "path/filepath" -) - -type Zipper interface { - Zip(dirToZip string, targetFile *os.File) (err error) -} - -type ApplicationZipper struct{} - -var doNotZipExtensions = []string{".zip", ".war", ".jar"} - -func (zipper ApplicationZipper) Zip(dirOrZipFile string, targetFile *os.File) (err error) { - if shouldNotZip(filepath.Ext(dirOrZipFile)) { - err = fileutils.CopyPathToWriter(dirOrZipFile, targetFile) - } else { - err = writeZipFile(dirOrZipFile, targetFile) - } - targetFile.Seek(0, os.SEEK_SET) - return -} - -func shouldNotZip(extension string) (result bool) { - for _, ext := range doNotZipExtensions { - if ext == extension { - return true - } - } - return -} - -func writeZipFile(dir string, targetFile *os.File) (err error) { - isEmpty, err := fileutils.IsDirEmpty(dir) - if err != nil { - return - } - if isEmpty { - err = errors.New("Directory is empty") - return - } - - writer := zip.NewWriter(targetFile) - defer writer.Close() - - err = walkAppFiles(dir, func(fileName string, fullPath string) (err error) { - fileInfo, err := os.Stat(fullPath) - if err != nil { - return err - } - - header, err := zip.FileInfoHeader(fileInfo) - header.Name = fileName - if err != nil { - return err - } - - zipFilePart, err := writer.CreateHeader(header) - err = fileutils.CopyPathToWriter(fullPath, zipFilePart) - return - }) - - return -} diff --git a/src/cf/zipper_test.go b/src/cf/zipper_test.go deleted file mode 100644 index 07ba219df71..00000000000 --- a/src/cf/zipper_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package cf - -import ( - "archive/zip" - "bytes" - "fileutils" - "github.com/stretchr/testify/assert" - "io" - "os" - "path/filepath" - "testing" -) - -func TestZipWithDirectory(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - - workingDir, err := os.Getwd() - assert.NoError(t, err) - - dir := filepath.Join(workingDir, "../fixtures/zip/") - err = os.Chmod(filepath.Join(dir, "subDir/bar.txt"), os.ModePerm) - assert.NoError(t, err) - - zipper := ApplicationZipper{} - err = zipper.Zip(dir, zipFile) - assert.NoError(t, err) - - offset, err := zipFile.Seek(0, os.SEEK_CUR) - assert.NoError(t, err) - assert.Equal(t, offset, 0) - - fileStat, err := zipFile.Stat() - assert.NoError(t, err) - - reader, err := zip.NewReader(zipFile, fileStat.Size()) - assert.NoError(t, err) - - readFileInZip := func(index int) (string, string) { - buf := &bytes.Buffer{} - file := reader.File[index] - fReader, err := file.Open() - _, err = io.Copy(buf, fReader) - - assert.NoError(t, err) - - return file.Name, string(buf.Bytes()) - } - - assert.Equal(t, len(reader.File), 2) - - name, contents := readFileInZip(0) - assert.Equal(t, name, "foo.txt") - assert.Equal(t, contents, "This is a simple text file.") - - name, contents = readFileInZip(1) - assert.Equal(t, name, filepath.Clean("subDir/bar.txt")) - assert.Equal(t, contents, "I am in a subdirectory.") - assert.Equal(t, reader.File[1].FileInfo().Mode(), uint32(os.ModePerm)) - }) -} - -func TestZipWithZipFile(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - dir, err := os.Getwd() - assert.NoError(t, err) - - zipper := ApplicationZipper{} - err = zipper.Zip(filepath.Join(dir, "../fixtures/application.zip"), zipFile) - assert.NoError(t, err) - - assert.Equal(t, fileToString(t, zipFile), "This is an application zip file\n") - }) -} - -func TestZipWithWarFile(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - dir, err := os.Getwd() - assert.NoError(t, err) - - zipper := ApplicationZipper{} - err = zipper.Zip(filepath.Join(dir, "../fixtures/application.war"), zipFile) - assert.NoError(t, err) - - assert.Equal(t, fileToString(t, zipFile), "This is an application war file\n") - }) -} - -func TestZipWithJarFile(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - dir, err := os.Getwd() - assert.NoError(t, err) - - zipper := ApplicationZipper{} - err = zipper.Zip(filepath.Join(dir, "../fixtures/application.jar"), zipFile) - assert.NoError(t, err) - - assert.Equal(t, fileToString(t, zipFile), "This is an application jar file\n") - }) -} - -func TestZipWithInvalidFile(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - zipper := ApplicationZipper{} - err = zipper.Zip("/a/bogus/directory", zipFile) - assert.Error(t, err) - assert.Contains(t, err.Error(), "open /a/bogus/directory") - }) -} - -func TestZipWithEmptyDir(t *testing.T) { - fileutils.TempFile("zip_test", func(zipFile *os.File, err error) { - fileutils.TempDir("zip_test", func(emptyDir string, err error) { - zipper := ApplicationZipper{} - err = zipper.Zip(emptyDir, zipFile) - assert.Error(t, err) - assert.Equal(t, err.Error(), "Directory is empty") - }) - }) -} - -func fileToString(t *testing.T, file *os.File) string { - bytesBuf := &bytes.Buffer{} - _, err := io.Copy(bytesBuf, file) - assert.NoError(t, err) - - return string(bytesBuf.Bytes()) -} diff --git a/src/code.google.com/p/go.net/.hg/00changelog.i b/src/code.google.com/p/go.net/.hg/00changelog.i deleted file mode 100644 index d3a8311050e..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/00changelog.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/branch b/src/code.google.com/p/go.net/.hg/branch deleted file mode 100644 index 4ad96d51599..00000000000 --- a/src/code.google.com/p/go.net/.hg/branch +++ /dev/null @@ -1 +0,0 @@ -default diff --git a/src/code.google.com/p/go.net/.hg/cache/branchheads-served b/src/code.google.com/p/go.net/.hg/cache/branchheads-served deleted file mode 100644 index c592cc32fe5..00000000000 --- a/src/code.google.com/p/go.net/.hg/cache/branchheads-served +++ /dev/null @@ -1,2 +0,0 @@ -bc411e2ac33f17d301647c10ebc2c28a1fc5e8c8 80 -bc411e2ac33f17d301647c10ebc2c28a1fc5e8c8 default diff --git a/src/code.google.com/p/go.net/.hg/dirstate b/src/code.google.com/p/go.net/.hg/dirstate deleted file mode 100644 index 13f074f0689..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/dirstate and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/hgrc b/src/code.google.com/p/go.net/.hg/hgrc deleted file mode 100644 index 692ebb4bcd4..00000000000 --- a/src/code.google.com/p/go.net/.hg/hgrc +++ /dev/null @@ -1,2 +0,0 @@ -[paths] -default = https://code.google.com/p/go.net diff --git a/src/code.google.com/p/go.net/.hg/requires b/src/code.google.com/p/go.net/.hg/requires deleted file mode 100644 index f634f664bf3..00000000000 --- a/src/code.google.com/p/go.net/.hg/requires +++ /dev/null @@ -1,4 +0,0 @@ -dotencode -fncache -revlogv1 -store diff --git a/src/code.google.com/p/go.net/.hg/store/00changelog.i b/src/code.google.com/p/go.net/.hg/store/00changelog.i deleted file mode 100644 index e1475a0518f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/00changelog.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/00manifest.i b/src/code.google.com/p/go.net/.hg/store/00manifest.i deleted file mode 100644 index d2e8f2f9eee..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/00manifest.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/_a_u_t_h_o_r_s.i b/src/code.google.com/p/go.net/.hg/store/data/_a_u_t_h_o_r_s.i deleted file mode 100644 index c16b17e1574..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/_a_u_t_h_o_r_s.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/_c_o_n_t_r_i_b_u_t_o_r_s.i b/src/code.google.com/p/go.net/.hg/store/data/_c_o_n_t_r_i_b_u_t_o_r_s.i deleted file mode 100644 index b853cf0278f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/_c_o_n_t_r_i_b_u_t_o_r_s.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/_l_i_c_e_n_s_e.i b/src/code.google.com/p/go.net/.hg/store/data/_l_i_c_e_n_s_e.i deleted file mode 100644 index aa52959a1db..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/_l_i_c_e_n_s_e.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/_p_a_t_e_n_t_s.i b/src/code.google.com/p/go.net/.hg/store/data/_p_a_t_e_n_t_s.i deleted file mode 100644 index a8653416d0f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/_p_a_t_e_n_t_s.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/_r_e_a_d_m_e.i b/src/code.google.com/p/go.net/.hg/store/data/_r_e_a_d_m_e.i deleted file mode 100644 index 75b7cc96268..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/_r_e_a_d_m_e.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/codereview.cfg.i b/src/code.google.com/p/go.net/.hg/store/data/codereview.cfg.i deleted file mode 100644 index 833f42f7846..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/codereview.cfg.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/dict/dict.go.i b/src/code.google.com/p/go.net/.hg/store/data/dict/dict.go.i deleted file mode 100644 index 402e254b69d..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/dict/dict.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom.go.i deleted file mode 100644 index 4cb3291027d..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom__test.go.i deleted file mode 100644 index 694285208fa..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/atom/atom__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/atom/gen.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/atom/gen.go.i deleted file mode 100644 index e286c9c4c7b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/atom/gen.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/atom/table.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/atom/table.go.i deleted file mode 100644 index bf01614bbfb..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/atom/table.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/atom/table__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/atom/table__test.go.i deleted file mode 100644 index 3fb16477428..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/atom/table__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/const.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/const.go.i deleted file mode 100644 index 17aa3af0704..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/const.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/doc.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/doc.go.i deleted file mode 100644 index 61e06c56cfa..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/doc.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/doctype.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/doctype.go.i deleted file mode 100644 index f58dc23e4bc..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/doctype.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/entity.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/entity.go.i deleted file mode 100644 index 5c2f53c5b8c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/entity.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/entity__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/entity__test.go.i deleted file mode 100644 index 9c70d74a763..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/entity__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/escape.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/escape.go.i deleted file mode 100644 index 231034f6167..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/escape.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/escape__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/escape__test.go.i deleted file mode 100644 index c7be7184610..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/escape__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/example__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/example__test.go.i deleted file mode 100644 index b4a59a776b3..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/example__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/foreign.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/foreign.go.i deleted file mode 100644 index 5db5b40f604..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/foreign.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/node.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/node.go.i deleted file mode 100644 index 3da9bccc247..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/node.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/node__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/node__test.go.i deleted file mode 100644 index a8676f77b54..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/node__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/parse.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/parse.go.i deleted file mode 100644 index 978e1432f8c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/parse.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/parse__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/parse__test.go.i deleted file mode 100644 index b21996c2ba6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/parse__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/render.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/render.go.i deleted file mode 100644 index 5d551705f76..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/render.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/render__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/render__test.go.i deleted file mode 100644 index 3f147ca23e9..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/render__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/go1.html.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/go1.html.i deleted file mode 100644 index 78f5db24ec6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/go1.html.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/_r_e_a_d_m_e.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/_r_e_a_d_m_e.i deleted file mode 100644 index d4aeafc7ac5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/_r_e_a_d_m_e.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption01.dat.i deleted file mode 100644 index 2aa72d06932..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption02.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption02.dat.i deleted file mode 100644 index 7392138cd6d..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/adoption02.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/comments01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/comments01.dat.i deleted file mode 100644 index bcd886a7d53..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/comments01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/doctype01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/doctype01.dat.i deleted file mode 100644 index b0972036f32..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/doctype01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities01.dat.i deleted file mode 100644 index f602d420bae..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities02.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities02.dat.i deleted file mode 100644 index 9e7ba8e3ec7..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/entities02.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/html5test-com.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/html5test-com.dat.i deleted file mode 100644 index c9ee99808fc..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/html5test-com.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/inbody01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/inbody01.dat.i deleted file mode 100644 index 926982a7b87..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/inbody01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/isindex.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/isindex.dat.i deleted file mode 100644 index 008a404b4ff..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/isindex.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat.i deleted file mode 100644 index ddf093712aa..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes.dat.i deleted file mode 100644 index 2ece9a1f0d6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/pending-spec-changes.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/plain-text-unsafe.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/plain-text-unsafe.dat.i deleted file mode 100644 index 0186491e0e0..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/plain-text-unsafe.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scriptdata01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scriptdata01.dat.i deleted file mode 100644 index 12839705924..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scriptdata01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/adoption01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/adoption01.dat.i deleted file mode 100644 index 42842bb6552..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/adoption01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/webkit01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/webkit01.dat.i deleted file mode 100644 index 3cbecb9664c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/scripted/webkit01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tables01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tables01.dat.i deleted file mode 100644 index 29f0601fb40..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tables01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests1.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests1.dat.i deleted file mode 100644 index 8369c5f5867..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests1.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests10.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests10.dat.i deleted file mode 100644 index 893d0cd1b24..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests10.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests11.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests11.dat.i deleted file mode 100644 index 2f46628c09f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests11.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests12.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests12.dat.i deleted file mode 100644 index c7b1936afd0..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests12.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests14.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests14.dat.i deleted file mode 100644 index 40b7c391eda..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests14.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests15.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests15.dat.i deleted file mode 100644 index b375d1321eb..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests15.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests16.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests16.dat.i deleted file mode 100644 index 514c3e3d49c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests16.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests17.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests17.dat.i deleted file mode 100644 index f0c0490f6ca..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests17.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests18.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests18.dat.i deleted file mode 100644 index 8a0aaa89045..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests18.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests19.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests19.dat.i deleted file mode 100644 index 51f23f58591..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests19.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests2.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests2.dat.i deleted file mode 100644 index 19d48459f07..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests2.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests20.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests20.dat.i deleted file mode 100644 index aa24f479a40..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests20.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests21.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests21.dat.i deleted file mode 100644 index 0ce6c3880ff..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests21.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests22.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests22.dat.i deleted file mode 100644 index cd6972d0edb..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests22.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests23.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests23.dat.i deleted file mode 100644 index f6b75f92cbe..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests23.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests24.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests24.dat.i deleted file mode 100644 index 8cf058a11ba..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests24.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests25.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests25.dat.i deleted file mode 100644 index 51555587805..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests25.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests26.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests26.dat.i deleted file mode 100644 index 288df4eb225..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests26.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests3.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests3.dat.i deleted file mode 100644 index 8a72d6b3845..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests3.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests4.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests4.dat.i deleted file mode 100644 index 60b8c471259..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests4.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests5.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests5.dat.i deleted file mode 100644 index 640b76d299b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests5.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests6.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests6.dat.i deleted file mode 100644 index 130f6ff4a3c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests6.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests7.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests7.dat.i deleted file mode 100644 index 94f0978a5da..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests7.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests8.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests8.dat.i deleted file mode 100644 index bb5ce1e3e6e..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests8.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests9.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests9.dat.i deleted file mode 100644 index d4d35270bac..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests9.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests__inner_h_t_m_l__1.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests__inner_h_t_m_l__1.dat.i deleted file mode 100644 index 16ae0e87bed..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tests__inner_h_t_m_l__1.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tricky01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tricky01.dat.i deleted file mode 100644 index f58aa534d92..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/tricky01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit01.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit01.dat.i deleted file mode 100644 index 5afdf277829..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit01.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit02.dat.i b/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit02.dat.i deleted file mode 100644 index bdd9e1def70..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/testdata/webkit/webkit02.dat.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/token.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/token.go.i deleted file mode 100644 index 95bb4399533..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/token.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/html/token__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/html/token__test.go.i deleted file mode 100644 index 3ddaf30ec00..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/html/token__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/idna/idna.go.i b/src/code.google.com/p/go.net/.hg/store/data/idna/idna.go.i deleted file mode 100644 index 386ea69baed..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/idna/idna.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/idna/idna__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/idna/idna__test.go.i deleted file mode 100644 index b6649010296..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/idna/idna__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/idna/punycode.go.i b/src/code.google.com/p/go.net/.hg/store/data/idna/punycode.go.i deleted file mode 100644 index 8d9d2569418..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/idna/punycode.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/idna/punycode__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/idna/punycode__test.go.i deleted file mode 100644 index 6b1d9d335e0..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/idna/punycode__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/control.go.i deleted file mode 100644 index 1d89b78eb32..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__bsd.go.i deleted file mode 100644 index d64a22ad72a..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__linux.go.i deleted file mode 100644 index 2b0ee126742..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__plan9.go.i deleted file mode 100644 index 1d8d450790e..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__windows.go.i deleted file mode 100644 index b8a6c075f2c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/control__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__plan9.go.i deleted file mode 100644 index 9d95d7cc9aa..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__posix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__posix.go.i deleted file mode 100644 index e2dd8606125..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/dgramopt__posix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/doc.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/doc.go.i deleted file mode 100644 index dd62a1b43d6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/doc.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/endpoint.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/endpoint.go.i deleted file mode 100644 index 2031b33cd62..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/endpoint.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/example__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/example__test.go.i deleted file mode 100644 index abfe3b60b13..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/example__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/gen.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/gen.go.i deleted file mode 100644 index 4c4158ed9b6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/gen.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__plan9.go.i deleted file mode 100644 index b319ecf560b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__posix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__posix.go.i deleted file mode 100644 index b8ed0945d7a..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/genericopt__posix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/gentest.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/gentest.go.i deleted file mode 100644 index 6cc3b4db8f9..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/gentest.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/header.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/header.go.i deleted file mode 100644 index 654b46e9a54..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/header.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/header__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/header__test.go.i deleted file mode 100644 index 6e070d8f831..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/header__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper.go.i deleted file mode 100644 index 40d71273d0b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__plan9.go.i deleted file mode 100644 index 7f2690df848..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__posix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__posix.go.i deleted file mode 100644 index 45731fff885..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__posix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__unix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__unix.go.i deleted file mode 100644 index f491e5aad56..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__unix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__windows.go.i deleted file mode 100644 index 2cf4f5d4d15..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/helper__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana.go.i deleted file mode 100644 index 3e57d26b071..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana__test.go.i deleted file mode 100644 index eec512cd0cc..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/iana__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/icmp.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/icmp.go.i deleted file mode 100644 index 84de37c0e31..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/icmp.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/mockicmp__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/mockicmp__test.go.i deleted file mode 100644 index 4da0781b971..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/mockicmp__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/mocktransponder__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/mocktransponder__test.go.i deleted file mode 100644 index 28af159eb5f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/mocktransponder__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicast__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicast__test.go.i deleted file mode 100644 index ce8ab8b04a7..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicast__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastlistener__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastlistener__test.go.i deleted file mode 100644 index dd1c76af2f8..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastlistener__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastsockopt__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastsockopt__test.go.i deleted file mode 100644 index 1dceaefee32..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/multicastsockopt__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/packet.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/packet.go.i deleted file mode 100644 index 33cf5ff6ca6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/packet.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/payload.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/payload.go.i deleted file mode 100644 index 184097a6a01..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/payload.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__bsd.go.i deleted file mode 100644 index 0e77255561e..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__freebsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__freebsd.go.i deleted file mode 100644 index b5a0067f7b2..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__freebsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__linux.go.i deleted file mode 100644 index 78282568c39..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__plan9.go.i deleted file mode 100644 index 136154aa6ad..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__unix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__unix.go.i deleted file mode 100644 index faf560446a5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__unix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__windows.go.i deleted file mode 100644 index 7a9ba29929f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/sockopt__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicast__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicast__test.go.i deleted file mode 100644 index fd84f723404..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicast__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicastsockopt__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicastsockopt__test.go.i deleted file mode 100644 index e1c0ee41ca1..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv4/unicastsockopt__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control.go.i deleted file mode 100644 index 0ebcb07437c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc2292__darwin.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc2292__darwin.go.i deleted file mode 100644 index 06d9cb1c803..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc2292__darwin.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__bsd.go.i deleted file mode 100644 index ebf27754ccf..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__linux.go.i deleted file mode 100644 index 9bf1a45d912..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__plan9.go.i deleted file mode 100644 index 4b194d74a75..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__windows.go.i deleted file mode 100644 index 8377fd3bd62..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__rfc3542__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__test.go.i deleted file mode 100644 index cf19463a7c5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/control__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__plan9.go.i deleted file mode 100644 index 2ca4c6e4cbd..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__posix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__posix.go.i deleted file mode 100644 index f1790aa4ee6..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/dgramopt__posix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/doc.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/doc.go.i deleted file mode 100644 index 7247f2a808e..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/doc.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/endpoint.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/endpoint.go.i deleted file mode 100644 index 6306cf11271..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/endpoint.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/gen.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/gen.go.i deleted file mode 100644 index 280dc1c4536..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/gen.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__plan9.go.i deleted file mode 100644 index 8bf9b21ed2b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__posix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__posix.go.i deleted file mode 100644 index 2bb8e71a0d1..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/genericopt__posix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/gentest.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/gentest.go.i deleted file mode 100644 index fa37721694f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/gentest.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper.go.i deleted file mode 100644 index 50e9db0e270..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__plan9.go.i deleted file mode 100644 index 83bb59060af..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__unix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__unix.go.i deleted file mode 100644 index 5673833335c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__unix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__windows.go.i deleted file mode 100644 index eda96bc2078..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/helper__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana.go.i deleted file mode 100644 index bec88801ce2..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana__test.go.i deleted file mode 100644 index 10524de8fd1..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/iana__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp.go.i deleted file mode 100644 index 2ff06c68707..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__bsd.go.i deleted file mode 100644 index 7ff82e53bbe..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__linux.go.i deleted file mode 100644 index d8187e432e4..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__plan9.go.i deleted file mode 100644 index f82473a306c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__test.go.i deleted file mode 100644 index b65f2b49176..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__windows.go.i deleted file mode 100644 index f82473a306c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/icmp__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/mockicmp__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/mockicmp__test.go.i deleted file mode 100644 index 35849d74a10..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/mockicmp__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/mocktransponder__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/mocktransponder__test.go.i deleted file mode 100644 index 559f8126144..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/mocktransponder__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicast__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicast__test.go.i deleted file mode 100644 index 21a88295db1..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicast__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastlistener__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastlistener__test.go.i deleted file mode 100644 index 34bc6d40998..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastlistener__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastsockopt__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastsockopt__test.go.i deleted file mode 100644 index 1c32e226359..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/multicastsockopt__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload.go.i deleted file mode 100644 index db900791b1c..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__cmsg.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__cmsg.go.i deleted file mode 100644 index 05b774bea83..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__cmsg.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__noncmsg.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__noncmsg.go.i deleted file mode 100644 index baa0aa42327..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/payload__noncmsg.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc2292__darwin.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc2292__darwin.go.i deleted file mode 100644 index ff844cb1b2f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc2292__darwin.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__bsd.go.i deleted file mode 100644 index 657f0daf548..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__linux.go.i deleted file mode 100644 index ed8f100b9e5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__unix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__unix.go.i deleted file mode 100644 index 7f7557d44b7..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__unix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__windows.go.i deleted file mode 100644 index 08abaf9c7f9..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3493__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__bsd.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__bsd.go.i deleted file mode 100644 index 5c09d01812a..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__bsd.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__linux.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__linux.go.i deleted file mode 100644 index e9d8f4b0407..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__linux.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__plan9.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__plan9.go.i deleted file mode 100644 index 2637730c2a5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__plan9.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__unix.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__unix.go.i deleted file mode 100644 index 6b8e51d6323..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__unix.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__windows.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__windows.go.i deleted file mode 100644 index 9ca656296fc..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__rfc3542__windows.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__test.go.i deleted file mode 100644 index 4f73c9b776d..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/sockopt__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicast__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicast__test.go.i deleted file mode 100644 index 62e49f4b741..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicast__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicastsockopt__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicastsockopt__test.go.i deleted file mode 100644 index 4352c76170a..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/ipv6/unicastsockopt__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/netutil/listen.go.i b/src/code.google.com/p/go.net/.hg/store/data/netutil/listen.go.i deleted file mode 100644 index d09da3cc485..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/netutil/listen.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/netutil/listen__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/netutil/listen__test.go.i deleted file mode 100644 index 956c9e1364b..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/netutil/listen__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/direct.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/direct.go.i deleted file mode 100644 index e214530e8a4..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/direct.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host.go.i deleted file mode 100644 index 58514b910fb..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host__test.go.i deleted file mode 100644 index 18a90e1a726..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/per__host__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy.go.i deleted file mode 100644 index cfb5785a096..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy__test.go.i deleted file mode 100644 index 674ce21fad4..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/proxy__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/proxy/socks5.go.i b/src/code.google.com/p/go.net/.hg/store/data/proxy/socks5.go.i deleted file mode 100644 index c11eba13220..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/proxy/socks5.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/gen.go.i b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/gen.go.i deleted file mode 100644 index c8fe1787150..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/gen.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list.go.i b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list.go.i deleted file mode 100644 index 3339d57dcdd..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list__test.go.i deleted file mode 100644 index 492b91e6874..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/list__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.d b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.d deleted file mode 100644 index 73f636a6ed3..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.d and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.i b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.i deleted file mode 100644 index cdea3e007c0..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table__test.go.i deleted file mode 100644 index 8f63d608dcb..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/publicsuffix/table__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/spdy/dictionary.go.i b/src/code.google.com/p/go.net/.hg/store/data/spdy/dictionary.go.i deleted file mode 100644 index 27984acb759..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/spdy/dictionary.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/spdy/read.go.i b/src/code.google.com/p/go.net/.hg/store/data/spdy/read.go.i deleted file mode 100644 index 1c2ab2922d5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/spdy/read.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/spdy/spdy__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/spdy/spdy__test.go.i deleted file mode 100644 index dec87c5abd5..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/spdy/spdy__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/spdy/types.go.i b/src/code.google.com/p/go.net/.hg/store/data/spdy/types.go.i deleted file mode 100644 index ca76152d5f3..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/spdy/types.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/spdy/write.go.i b/src/code.google.com/p/go.net/.hg/store/data/spdy/write.go.i deleted file mode 100644 index d28a658f341..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/spdy/write.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/client.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/client.go.i deleted file mode 100644 index dc96a21301f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/client.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/exampledial__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/exampledial__test.go.i deleted file mode 100644 index 8d2cbfc7ee8..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/exampledial__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/examplehandler__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/examplehandler__test.go.i deleted file mode 100644 index c93321b8dbf..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/examplehandler__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie.go.i deleted file mode 100644 index d9d3ed9c538..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie__test.go.i deleted file mode 100644 index baf971046f4..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/hixie__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi.go.i deleted file mode 100644 index f227bdd6303..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi__test.go.i deleted file mode 100644 index b88a1aa6194..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/hybi__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/server.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/server.go.i deleted file mode 100644 index 787ce5e399f..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/server.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket.go.i deleted file mode 100644 index c4ad110b299..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket__test.go.i b/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket__test.go.i deleted file mode 100644 index 80b737cf0e1..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/websocket/websocket__test.go.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/data/~2ehgignore.i b/src/code.google.com/p/go.net/.hg/store/data/~2ehgignore.i deleted file mode 100644 index 365461422dd..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/data/~2ehgignore.i and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/store/fncache b/src/code.google.com/p/go.net/.hg/store/fncache deleted file mode 100644 index bec45e5ce87..00000000000 --- a/src/code.google.com/p/go.net/.hg/store/fncache +++ /dev/null @@ -1,197 +0,0 @@ -data/ipv4/sockopt_plan9.go.i -data/html/const.go.i -data/CONTRIBUTORS.i -data/ipv4/dgramopt_plan9.go.i -data/html/testdata/webkit/tests_innerHTML_1.dat.i -data/html/testdata/webkit/plain-text-unsafe.dat.i -data/publicsuffix/list.go.i -data/websocket/websocket.go.i -data/html/testdata/webkit/adoption02.dat.i -data/spdy/types.go.i -data/ipv6/helper.go.i -data/html/testdata/webkit/tests2.dat.i -data/proxy/per_host_test.go.i -data/html/testdata/webkit/tests9.dat.i -data/proxy/direct.go.i -data/ipv6/sockopt_rfc3493_unix.go.i -data/html/foreign.go.i -data/html/testdata/webkit/tests22.dat.i -data/websocket/websocket_test.go.i -data/ipv4/gen.go.i -data/html/testdata/webkit/webkit01.dat.i -data/html/atom/atom_test.go.i -data/html/testdata/webkit/inbody01.dat.i -data/html/testdata/webkit/scripted/webkit01.dat.i -data/html/parse_test.go.i -data/ipv4/example_test.go.i -data/dict/dict.go.i -data/ipv4/control_bsd.go.i -data/html/testdata/webkit/tests14.dat.i -data/ipv4/sockopt_windows.go.i -data/ipv6/genericopt_posix.go.i -data/proxy/proxy_test.go.i -data/html/testdata/webkit/tests11.dat.i -data/ipv4/header.go.i -data/ipv4/packet.go.i -data/ipv4/iana_test.go.i -data/idna/idna_test.go.i -data/html/testdata/webkit/tests21.dat.i -data/html/testdata/webkit/scripted/adoption01.dat.i -data/ipv6/sockopt_test.go.i -data/ipv4/dgramopt_posix.go.i -data/websocket/examplehandler_test.go.i -data/html/testdata/webkit/tests1.dat.i -data/publicsuffix/list_test.go.i -data/spdy/read.go.i -data/ipv6/payload_noncmsg.go.i -data/ipv6/helper_plan9.go.i -data/ipv6/control_rfc2292_darwin.go.i -data/ipv6/control_rfc3542_plan9.go.i -data/html/testdata/webkit/scriptdata01.dat.i -data/ipv6/dgramopt_posix.go.i -data/ipv6/control_rfc3542_linux.go.i -data/ipv6/icmp_test.go.i -data/idna/punycode_test.go.i -data/ipv4/genericopt_posix.go.i -data/html/testdata/webkit/tests16.dat.i -data/ipv4/helper.go.i -data/ipv6/dgramopt_plan9.go.i -data/websocket/exampledial_test.go.i -data/ipv4/icmp.go.i -data/ipv6/iana_test.go.i -data/codereview.cfg.i -data/ipv6/unicast_test.go.i -data/ipv6/sockopt_rfc3493_linux.go.i -data/html/testdata/go1.html.i -data/html/node.go.i -data/html/node_test.go.i -data/ipv4/mocktransponder_test.go.i -data/ipv6/payload.go.i -data/html/entity.go.i -data/ipv4/control.go.i -data/ipv6/icmp_bsd.go.i -data/html/testdata/webkit/tests3.dat.i -data/websocket/hybi.go.i -data/ipv6/icmp.go.i -data/ipv4/helper_posix.go.i -data/ipv4/doc.go.i -data/html/testdata/webkit/tests12.dat.i -data/html/escape.go.i -data/html/escape_test.go.i -data/README.i -data/ipv6/unicastsockopt_test.go.i -data/html/testdata/webkit/tests26.dat.i -data/html/testdata/webkit/tests6.dat.i -data/ipv4/payload.go.i -data/ipv6/icmp_plan9.go.i -data/websocket/hybi_test.go.i -data/websocket/server.go.i -data/ipv4/sockopt_freebsd.go.i -data/ipv6/icmp_linux.go.i -data/AUTHORS.i -data/ipv4/sockopt_bsd.go.i -data/spdy/dictionary.go.i -data/ipv6/genericopt_plan9.go.i -data/proxy/socks5.go.i -data/ipv4/sockopt_linux.go.i -data/ipv6/control_rfc3542_windows.go.i -data/spdy/spdy_test.go.i -data/ipv4/helper_plan9.go.i -data/ipv4/sockopt_unix.go.i -data/ipv6/sockopt_rfc3542_windows.go.i -data/ipv6/control_test.go.i -data/publicsuffix/gen.go.i -data/ipv6/gentest.go.i -data/html/testdata/webkit/tests15.dat.i -data/ipv4/endpoint.go.i -data/html/render.go.i -data/ipv6/sockopt_rfc3542_bsd.go.i -data/ipv6/sockopt_rfc3542_plan9.go.i -data/html/token_test.go.i -data/ipv6/helper_unix.go.i -data/html/testdata/webkit/pending-spec-changes.dat.i -data/html/testdata/webkit/comments01.dat.i -data/html/testdata/webkit/tests19.dat.i -data/ipv4/control_linux.go.i -data/ipv6/control.go.i -data/netutil/listen.go.i -data/idna/punycode.go.i -data/ipv4/header_test.go.i -data/html/testdata/webkit/tests24.dat.i -data/.hgignore.i -data/html/testdata/webkit/tests10.dat.i -data/proxy/per_host.go.i -data/html/entity_test.go.i -data/ipv4/helper_windows.go.i -data/html/atom/gen.go.i -data/ipv4/multicastlistener_test.go.i -data/ipv4/unicastsockopt_test.go.i -data/ipv6/mocktransponder_test.go.i -data/ipv6/iana.go.i -data/html/doctype.go.i -data/ipv4/mockicmp_test.go.i -data/ipv6/multicastsockopt_test.go.i -data/html/atom/table_test.go.i -data/idna/idna.go.i -data/html/testdata/webkit/tests17.dat.i -data/html/testdata/webkit/README.i -data/netutil/listen_test.go.i -data/ipv6/icmp_windows.go.i -data/ipv6/gen.go.i -data/websocket/hixie_test.go.i -data/html/testdata/webkit/tests20.dat.i -data/html/testdata/webkit/adoption01.dat.i -data/html/token.go.i -data/proxy/proxy.go.i -data/html/testdata/webkit/isindex.dat.i -data/html/testdata/webkit/html5test-com.dat.i -data/ipv6/multicast_test.go.i -data/websocket/client.go.i -data/ipv4/control_plan9.go.i -data/ipv6/sockopt_rfc3493_windows.go.i -data/html/testdata/webkit/entities02.dat.i -data/html/testdata/webkit/tricky01.dat.i -data/ipv6/multicastlistener_test.go.i -data/ipv6/helper_windows.go.i -data/html/testdata/webkit/tables01.dat.i -data/ipv4/helper_unix.go.i -data/ipv6/mockicmp_test.go.i -data/html/testdata/webkit/webkit02.dat.i -data/ipv4/control_windows.go.i -data/html/testdata/webkit/tests25.dat.i -data/html/doc.go.i -data/ipv6/sockopt_rfc3542_linux.go.i -data/ipv4/gentest.go.i -data/html/testdata/webkit/tests5.dat.i -data/publicsuffix/table.go.d -data/html/atom/atom.go.i -data/LICENSE.i -data/publicsuffix/table.go.i -data/html/testdata/webkit/tests8.dat.i -data/ipv4/iana.go.i -data/html/testdata/webkit/entities01.dat.i -data/ipv6/sockopt_rfc3542_unix.go.i -data/ipv4/genericopt_plan9.go.i -data/html/render_test.go.i -data/ipv4/multicastsockopt_test.go.i -data/ipv6/doc.go.i -data/html/testdata/webkit/tests23.dat.i -data/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat.i -data/html/example_test.go.i -data/ipv6/sockopt_rfc3493_bsd.go.i -data/ipv6/sockopt_rfc2292_darwin.go.i -data/html/testdata/webkit/doctype01.dat.i -data/ipv6/endpoint.go.i -data/PATENTS.i -data/html/testdata/webkit/tests7.dat.i -data/spdy/write.go.i -data/html/atom/table.go.i -data/ipv6/control_rfc3542_bsd.go.i -data/websocket/hixie.go.i -data/ipv4/unicast_test.go.i -data/html/testdata/webkit/tests4.dat.i -data/html/parse.go.i -data/html/testdata/webkit/tests18.dat.i -data/publicsuffix/table_test.go.i -data/ipv4/multicast_test.go.i -data/ipv6/payload_cmsg.go.i diff --git a/src/code.google.com/p/go.net/.hg/store/undo b/src/code.google.com/p/go.net/.hg/store/undo deleted file mode 100644 index 67665b029ce..00000000000 Binary files a/src/code.google.com/p/go.net/.hg/store/undo and /dev/null differ diff --git a/src/code.google.com/p/go.net/.hg/undo.branch b/src/code.google.com/p/go.net/.hg/undo.branch deleted file mode 100644 index 331d858ce9b..00000000000 --- a/src/code.google.com/p/go.net/.hg/undo.branch +++ /dev/null @@ -1 +0,0 @@ -default \ No newline at end of file diff --git a/src/code.google.com/p/go.net/.hg/undo.desc b/src/code.google.com/p/go.net/.hg/undo.desc deleted file mode 100644 index 48a1e39b6bb..00000000000 --- a/src/code.google.com/p/go.net/.hg/undo.desc +++ /dev/null @@ -1,3 +0,0 @@ -0 -pull -https://code.google.com/p/go.net diff --git a/src/code.google.com/p/go.net/.hgignore b/src/code.google.com/p/go.net/.hgignore deleted file mode 100644 index 571db5fdad6..00000000000 --- a/src/code.google.com/p/go.net/.hgignore +++ /dev/null @@ -1,2 +0,0 @@ -syntax:glob -last-change diff --git a/src/code.google.com/p/go.net/AUTHORS b/src/code.google.com/p/go.net/AUTHORS deleted file mode 100644 index 15167cd746c..00000000000 --- a/src/code.google.com/p/go.net/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/src/code.google.com/p/go.net/CONTRIBUTORS b/src/code.google.com/p/go.net/CONTRIBUTORS deleted file mode 100644 index 1c4577e9680..00000000000 --- a/src/code.google.com/p/go.net/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/src/code.google.com/p/go.net/LICENSE b/src/code.google.com/p/go.net/LICENSE deleted file mode 100644 index 6a66aea5eaf..00000000000 --- a/src/code.google.com/p/go.net/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/code.google.com/p/go.net/PATENTS b/src/code.google.com/p/go.net/PATENTS deleted file mode 100644 index 733099041f8..00000000000 --- a/src/code.google.com/p/go.net/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the Go project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of Go, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of Go. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of Go or any code incorporated within this -implementation of Go constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of Go -shall terminate as of the date such litigation is filed. diff --git a/src/code.google.com/p/go.net/README b/src/code.google.com/p/go.net/README deleted file mode 100644 index 6b13d8e5050..00000000000 --- a/src/code.google.com/p/go.net/README +++ /dev/null @@ -1,3 +0,0 @@ -This repository holds supplementary Go networking libraries. - -To submit changes to this repository, see http://golang.org/doc/contribute.html. diff --git a/src/code.google.com/p/go.net/codereview.cfg b/src/code.google.com/p/go.net/codereview.cfg deleted file mode 100644 index e3eb47ca0d7..00000000000 --- a/src/code.google.com/p/go.net/codereview.cfg +++ /dev/null @@ -1,2 +0,0 @@ -defaultcc: golang-dev@googlegroups.com -contributors: http://go.googlecode.com/hg/CONTRIBUTORS diff --git a/src/code.google.com/p/go.net/dict/dict.go b/src/code.google.com/p/go.net/dict/dict.go deleted file mode 100644 index e7f5290f552..00000000000 --- a/src/code.google.com/p/go.net/dict/dict.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package dict implements the Dictionary Server Protocol -// as defined in RFC 2229. -package dict - -import ( - "net/textproto" - "strconv" - "strings" -) - -// A Client represents a client connection to a dictionary server. -type Client struct { - text *textproto.Conn -} - -// Dial returns a new client connected to a dictionary server at -// addr on the given network. -func Dial(network, addr string) (*Client, error) { - text, err := textproto.Dial(network, addr) - if err != nil { - return nil, err - } - _, _, err = text.ReadCodeLine(220) - if err != nil { - text.Close() - return nil, err - } - return &Client{text: text}, nil -} - -// Close closes the connection to the dictionary server. -func (c *Client) Close() error { - return c.text.Close() -} - -// A Dict represents a dictionary available on the server. -type Dict struct { - Name string // short name of dictionary - Desc string // long description -} - -// Dicts returns a list of the dictionaries available on the server. -func (c *Client) Dicts() ([]Dict, error) { - id, err := c.text.Cmd("SHOW DB") - if err != nil { - return nil, err - } - - c.text.StartResponse(id) - defer c.text.EndResponse(id) - - _, _, err = c.text.ReadCodeLine(110) - if err != nil { - return nil, err - } - lines, err := c.text.ReadDotLines() - if err != nil { - return nil, err - } - _, _, err = c.text.ReadCodeLine(250) - - dicts := make([]Dict, len(lines)) - for i := range dicts { - d := &dicts[i] - a, _ := fields(lines[i]) - if len(a) < 2 { - return nil, textproto.ProtocolError("invalid dictionary: " + lines[i]) - } - d.Name = a[0] - d.Desc = a[1] - } - return dicts, err -} - -// A Defn represents a definition. -type Defn struct { - Dict Dict // Dict where definition was found - Word string // Word being defined - Text []byte // Definition text, typically multiple lines -} - -// Define requests the definition of the given word. -// The argument dict names the dictionary to use, -// the Name field of a Dict returned by Dicts. -// -// The special dictionary name "*" means to look in all the -// server's dictionaries. -// The special dictionary name "!" means to look in all the -// server's dictionaries in turn, stopping after finding the word -// in one of them. -func (c *Client) Define(dict, word string) ([]*Defn, error) { - id, err := c.text.Cmd("DEFINE %s %q", dict, word) - if err != nil { - return nil, err - } - - c.text.StartResponse(id) - defer c.text.EndResponse(id) - - _, line, err := c.text.ReadCodeLine(150) - if err != nil { - return nil, err - } - a, _ := fields(line) - if len(a) < 1 { - return nil, textproto.ProtocolError("malformed response: " + line) - } - n, err := strconv.Atoi(a[0]) - if err != nil { - return nil, textproto.ProtocolError("invalid definition count: " + a[0]) - } - def := make([]*Defn, n) - for i := 0; i < n; i++ { - _, line, err = c.text.ReadCodeLine(151) - if err != nil { - return nil, err - } - a, _ := fields(line) - if len(a) < 3 { - // skip it, to keep protocol in sync - i-- - n-- - def = def[0:n] - continue - } - d := &Defn{Word: a[0], Dict: Dict{a[1], a[2]}} - d.Text, err = c.text.ReadDotBytes() - if err != nil { - return nil, err - } - def[i] = d - } - _, _, err = c.text.ReadCodeLine(250) - return def, err -} - -// Fields returns the fields in s. -// Fields are space separated unquoted words -// or quoted with single or double quote. -func fields(s string) ([]string, error) { - var v []string - i := 0 - for { - for i < len(s) && (s[i] == ' ' || s[i] == '\t') { - i++ - } - if i >= len(s) { - break - } - if s[i] == '"' || s[i] == '\'' { - q := s[i] - // quoted string - var j int - for j = i + 1; ; j++ { - if j >= len(s) { - return nil, textproto.ProtocolError("malformed quoted string") - } - if s[j] == '\\' { - j++ - continue - } - if s[j] == q { - j++ - break - } - } - v = append(v, unquote(s[i+1:j-1])) - i = j - } else { - // atom - var j int - for j = i; j < len(s); j++ { - if s[j] == ' ' || s[j] == '\t' || s[j] == '\\' || s[j] == '"' || s[j] == '\'' { - break - } - } - v = append(v, s[i:j]) - i = j - } - if i < len(s) { - c := s[i] - if c != ' ' && c != '\t' { - return nil, textproto.ProtocolError("quotes not on word boundaries") - } - } - } - return v, nil -} - -func unquote(s string) string { - if strings.Index(s, "\\") < 0 { - return s - } - b := []byte(s) - w := 0 - for r := 0; r < len(b); r++ { - c := b[r] - if c == '\\' { - r++ - c = b[r] - } - b[w] = c - w++ - } - return string(b[0:w]) -} diff --git a/src/code.google.com/p/go.net/html/atom/atom.go b/src/code.google.com/p/go.net/html/atom/atom.go deleted file mode 100644 index 227404bdafa..00000000000 --- a/src/code.google.com/p/go.net/html/atom/atom.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package atom provides integer codes (also known as atoms) for a fixed set of -// frequently occurring HTML strings: tag names and attribute keys such as "p" -// and "id". -// -// Sharing an atom's name between all elements with the same tag can result in -// fewer string allocations when tokenizing and parsing HTML. Integer -// comparisons are also generally faster than string comparisons. -// -// The value of an atom's particular code is not guaranteed to stay the same -// between versions of this package. Neither is any ordering guaranteed: -// whether atom.H1 < atom.H2 may also change. The codes are not guaranteed to -// be dense. The only guarantees are that e.g. looking up "div" will yield -// atom.Div, calling atom.Div.String will return "div", and atom.Div != 0. -package atom - -// Atom is an integer code for a string. The zero value maps to "". -type Atom uint32 - -// String returns the atom's name. -func (a Atom) String() string { - start := uint32(a >> 8) - n := uint32(a & 0xff) - if start+n > uint32(len(atomText)) { - return "" - } - return atomText[start : start+n] -} - -func (a Atom) string() string { - return atomText[a>>8 : a>>8+a&0xff] -} - -// fnv computes the FNV hash with an arbitrary starting value h. -func fnv(h uint32, s []byte) uint32 { - for i := range s { - h ^= uint32(s[i]) - h *= 16777619 - } - return h -} - -func match(s string, t []byte) bool { - for i, c := range t { - if s[i] != c { - return false - } - } - return true -} - -// Lookup returns the atom whose name is s. It returns zero if there is no -// such atom. The lookup is case sensitive. -func Lookup(s []byte) Atom { - if len(s) == 0 || len(s) > maxAtomLen { - return 0 - } - h := fnv(hash0, s) - if a := table[h&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { - return a - } - if a := table[(h>>16)&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { - return a - } - return 0 -} - -// String returns a string whose contents are equal to s. In that sense, it is -// equivalent to string(s) but may be more efficient. -func String(s []byte) string { - if a := Lookup(s); a != 0 { - return a.String() - } - return string(s) -} diff --git a/src/code.google.com/p/go.net/html/atom/atom_test.go b/src/code.google.com/p/go.net/html/atom/atom_test.go deleted file mode 100644 index 6e33704dd5e..00000000000 --- a/src/code.google.com/p/go.net/html/atom/atom_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package atom - -import ( - "sort" - "testing" -) - -func TestKnown(t *testing.T) { - for _, s := range testAtomList { - if atom := Lookup([]byte(s)); atom.String() != s { - t.Errorf("Lookup(%q) = %#x (%q)", s, uint32(atom), atom.String()) - } - } -} - -func TestHits(t *testing.T) { - for _, a := range table { - if a == 0 { - continue - } - got := Lookup([]byte(a.String())) - if got != a { - t.Errorf("Lookup(%q) = %#x, want %#x", a.String(), uint32(got), uint32(a)) - } - } -} - -func TestMisses(t *testing.T) { - testCases := []string{ - "", - "\x00", - "\xff", - "A", - "DIV", - "Div", - "dIV", - "aa", - "a\x00", - "ab", - "abb", - "abbr0", - "abbr ", - " abbr", - " a", - "acceptcharset", - "acceptCharset", - "accept_charset", - "h0", - "h1h2", - "h7", - "onClick", - "λ", - // The following string has the same hash (0xa1d7fab7) as "onmouseover". - "\x00\x00\x00\x00\x00\x50\x18\xae\x38\xd0\xb7", - } - for _, tc := range testCases { - got := Lookup([]byte(tc)) - if got != 0 { - t.Errorf("Lookup(%q): got %d, want 0", tc, got) - } - } -} - -func TestForeignObject(t *testing.T) { - const ( - afo = Foreignobject - afO = ForeignObject - sfo = "foreignobject" - sfO = "foreignObject" - ) - if got := Lookup([]byte(sfo)); got != afo { - t.Errorf("Lookup(%q): got %#v, want %#v", sfo, got, afo) - } - if got := Lookup([]byte(sfO)); got != afO { - t.Errorf("Lookup(%q): got %#v, want %#v", sfO, got, afO) - } - if got := afo.String(); got != sfo { - t.Errorf("Atom(%#v).String(): got %q, want %q", afo, got, sfo) - } - if got := afO.String(); got != sfO { - t.Errorf("Atom(%#v).String(): got %q, want %q", afO, got, sfO) - } -} - -func BenchmarkLookup(b *testing.B) { - sortedTable := make([]string, 0, len(table)) - for _, a := range table { - if a != 0 { - sortedTable = append(sortedTable, a.String()) - } - } - sort.Strings(sortedTable) - - x := make([][]byte, 1000) - for i := range x { - x[i] = []byte(sortedTable[i%len(sortedTable)]) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, s := range x { - Lookup(s) - } - } -} diff --git a/src/code.google.com/p/go.net/html/atom/gen.go b/src/code.google.com/p/go.net/html/atom/gen.go deleted file mode 100644 index 9958a718842..00000000000 --- a/src/code.google.com/p/go.net/html/atom/gen.go +++ /dev/null @@ -1,636 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build ignore - -package main - -// This program generates table.go and table_test.go. -// Invoke as -// -// go run gen.go |gofmt >table.go -// go run gen.go -test |gofmt >table_test.go - -import ( - "flag" - "fmt" - "math/rand" - "os" - "sort" - "strings" -) - -// identifier converts s to a Go exported identifier. -// It converts "div" to "Div" and "accept-charset" to "AcceptCharset". -func identifier(s string) string { - b := make([]byte, 0, len(s)) - cap := true - for _, c := range s { - if c == '-' { - cap = true - continue - } - if cap && 'a' <= c && c <= 'z' { - c -= 'a' - 'A' - } - cap = false - b = append(b, byte(c)) - } - return string(b) -} - -var test = flag.Bool("test", false, "generate table_test.go") - -func main() { - flag.Parse() - - var all []string - all = append(all, elements...) - all = append(all, attributes...) - all = append(all, eventHandlers...) - all = append(all, extra...) - sort.Strings(all) - - if *test { - fmt.Printf("// generated by go run gen.go -test; DO NOT EDIT\n\n") - fmt.Printf("package atom\n\n") - fmt.Printf("var testAtomList = []string{\n") - for _, s := range all { - fmt.Printf("\t%q,\n", s) - } - fmt.Printf("}\n") - return - } - - // uniq - lists have dups - // compute max len too - maxLen := 0 - w := 0 - for _, s := range all { - if w == 0 || all[w-1] != s { - if maxLen < len(s) { - maxLen = len(s) - } - all[w] = s - w++ - } - } - all = all[:w] - - // Find hash that minimizes table size. - var best *table - for i := 0; i < 1000000; i++ { - if best != nil && 1<<(best.k-1) < len(all) { - break - } - h := rand.Uint32() - for k := uint(0); k <= 16; k++ { - if best != nil && k >= best.k { - break - } - var t table - if t.init(h, k, all) { - best = &t - break - } - } - } - if best == nil { - fmt.Fprintf(os.Stderr, "failed to construct string table\n") - os.Exit(1) - } - - // Lay out strings, using overlaps when possible. - layout := append([]string{}, all...) - - // Remove strings that are substrings of other strings - for changed := true; changed; { - changed = false - for i, s := range layout { - if s == "" { - continue - } - for j, t := range layout { - if i != j && t != "" && strings.Contains(s, t) { - changed = true - layout[j] = "" - } - } - } - } - - // Join strings where one suffix matches another prefix. - for { - // Find best i, j, k such that layout[i][len-k:] == layout[j][:k], - // maximizing overlap length k. - besti := -1 - bestj := -1 - bestk := 0 - for i, s := range layout { - if s == "" { - continue - } - for j, t := range layout { - if i == j { - continue - } - for k := bestk + 1; k <= len(s) && k <= len(t); k++ { - if s[len(s)-k:] == t[:k] { - besti = i - bestj = j - bestk = k - } - } - } - } - if bestk > 0 { - layout[besti] += layout[bestj][bestk:] - layout[bestj] = "" - continue - } - break - } - - text := strings.Join(layout, "") - - atom := map[string]uint32{} - for _, s := range all { - off := strings.Index(text, s) - if off < 0 { - panic("lost string " + s) - } - atom[s] = uint32(off<<8 | len(s)) - } - - // Generate the Go code. - fmt.Printf("// generated by go run gen.go; DO NOT EDIT\n\n") - fmt.Printf("package atom\n\nconst (\n") - for _, s := range all { - fmt.Printf("\t%s Atom = %#x\n", identifier(s), atom[s]) - } - fmt.Printf(")\n\n") - - fmt.Printf("const hash0 = %#x\n\n", best.h0) - fmt.Printf("const maxAtomLen = %d\n\n", maxLen) - - fmt.Printf("var table = [1<<%d]Atom{\n", best.k) - for i, s := range best.tab { - if s == "" { - continue - } - fmt.Printf("\t%#x: %#x, // %s\n", i, atom[s], s) - } - fmt.Printf("}\n") - datasize := (1 << best.k) * 4 - - fmt.Printf("const atomText =\n") - textsize := len(text) - for len(text) > 60 { - fmt.Printf("\t%q +\n", text[:60]) - text = text[60:] - } - fmt.Printf("\t%q\n\n", text) - - fmt.Fprintf(os.Stderr, "%d atoms; %d string bytes + %d tables = %d total data\n", len(all), textsize, datasize, textsize+datasize) -} - -type byLen []string - -func (x byLen) Less(i, j int) bool { return len(x[i]) > len(x[j]) } -func (x byLen) Swap(i, j int) { x[i], x[j] = x[j], x[i] } -func (x byLen) Len() int { return len(x) } - -// fnv computes the FNV hash with an arbitrary starting value h. -func fnv(h uint32, s string) uint32 { - for i := 0; i < len(s); i++ { - h ^= uint32(s[i]) - h *= 16777619 - } - return h -} - -// A table represents an attempt at constructing the lookup table. -// The lookup table uses cuckoo hashing, meaning that each string -// can be found in one of two positions. -type table struct { - h0 uint32 - k uint - mask uint32 - tab []string -} - -// hash returns the two hashes for s. -func (t *table) hash(s string) (h1, h2 uint32) { - h := fnv(t.h0, s) - h1 = h & t.mask - h2 = (h >> 16) & t.mask - return -} - -// init initializes the table with the given parameters. -// h0 is the initial hash value, -// k is the number of bits of hash value to use, and -// x is the list of strings to store in the table. -// init returns false if the table cannot be constructed. -func (t *table) init(h0 uint32, k uint, x []string) bool { - t.h0 = h0 - t.k = k - t.tab = make([]string, 1< len(t.tab) { - return false - } - s := t.tab[i] - h1, h2 := t.hash(s) - j := h1 + h2 - i - if t.tab[j] != "" && !t.push(j, depth+1) { - return false - } - t.tab[j] = s - return true -} - -// The lists of element names and attribute keys were taken from -// http://www.whatwg.org/specs/web-apps/current-work/multipage/section-index.html -// as of the "HTML Living Standard - Last Updated 30 May 2012" version. - -var elements = []string{ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "command", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hgroup", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "map", - "mark", - "menu", - "meta", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "pre", - "progress", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", -} - -var attributes = []string{ - "accept", - "accept-charset", - "accesskey", - "action", - "alt", - "async", - "autocomplete", - "autofocus", - "autoplay", - "border", - "challenge", - "charset", - "checked", - "cite", - "class", - "cols", - "colspan", - "command", - "content", - "contenteditable", - "contextmenu", - "controls", - "coords", - "crossorigin", - "data", - "datetime", - "default", - "defer", - "dir", - "dirname", - "disabled", - "download", - "draggable", - "dropzone", - "enctype", - "for", - "form", - "formaction", - "formenctype", - "formmethod", - "formnovalidate", - "formtarget", - "headers", - "height", - "hidden", - "high", - "href", - "hreflang", - "http-equiv", - "icon", - "id", - "inert", - "ismap", - "itemid", - "itemprop", - "itemref", - "itemscope", - "itemtype", - "keytype", - "kind", - "label", - "lang", - "list", - "loop", - "low", - "manifest", - "max", - "maxlength", - "media", - "mediagroup", - "method", - "min", - "multiple", - "muted", - "name", - "novalidate", - "open", - "optimum", - "pattern", - "ping", - "placeholder", - "poster", - "preload", - "radiogroup", - "readonly", - "rel", - "required", - "reversed", - "rows", - "rowspan", - "sandbox", - "spellcheck", - "scope", - "scoped", - "seamless", - "selected", - "shape", - "size", - "sizes", - "span", - "src", - "srcdoc", - "srclang", - "start", - "step", - "style", - "tabindex", - "target", - "title", - "translate", - "type", - "typemustmatch", - "usemap", - "value", - "width", - "wrap", -} - -var eventHandlers = []string{ - "onabort", - "onafterprint", - "onbeforeprint", - "onbeforeunload", - "onblur", - "oncancel", - "oncanplay", - "oncanplaythrough", - "onchange", - "onclick", - "onclose", - "oncontextmenu", - "oncuechange", - "ondblclick", - "ondrag", - "ondragend", - "ondragenter", - "ondragleave", - "ondragover", - "ondragstart", - "ondrop", - "ondurationchange", - "onemptied", - "onended", - "onerror", - "onfocus", - "onhashchange", - "oninput", - "oninvalid", - "onkeydown", - "onkeypress", - "onkeyup", - "onload", - "onloadeddata", - "onloadedmetadata", - "onloadstart", - "onmessage", - "onmousedown", - "onmousemove", - "onmouseout", - "onmouseover", - "onmouseup", - "onmousewheel", - "onoffline", - "ononline", - "onpagehide", - "onpageshow", - "onpause", - "onplay", - "onplaying", - "onpopstate", - "onprogress", - "onratechange", - "onreset", - "onresize", - "onscroll", - "onseeked", - "onseeking", - "onselect", - "onshow", - "onstalled", - "onstorage", - "onsubmit", - "onsuspend", - "ontimeupdate", - "onunload", - "onvolumechange", - "onwaiting", -} - -// extra are ad-hoc values not covered by any of the lists above. -var extra = []string{ - "align", - "annotation", - "annotation-xml", - "applet", - "basefont", - "bgsound", - "big", - "blink", - "center", - "color", - "desc", - "face", - "font", - "foreignObject", // HTML is case-insensitive, but SVG-embedded-in-HTML is case-sensitive. - "foreignobject", - "frame", - "frameset", - "image", - "isindex", - "listing", - "malignmark", - "marquee", - "math", - "mglyph", - "mi", - "mn", - "mo", - "ms", - "mtext", - "nobr", - "noembed", - "noframes", - "plaintext", - "prompt", - "public", - "spacer", - "strike", - "svg", - "system", - "tt", - "xmp", -} diff --git a/src/code.google.com/p/go.net/html/atom/table.go b/src/code.google.com/p/go.net/html/atom/table.go deleted file mode 100644 index 20b8b8a5903..00000000000 --- a/src/code.google.com/p/go.net/html/atom/table.go +++ /dev/null @@ -1,694 +0,0 @@ -// generated by go run gen.go; DO NOT EDIT - -package atom - -const ( - A Atom = 0x1 - Abbr Atom = 0x4 - Accept Atom = 0x2106 - AcceptCharset Atom = 0x210e - Accesskey Atom = 0x3309 - Action Atom = 0x21b06 - Address Atom = 0x5d507 - Align Atom = 0x1105 - Alt Atom = 0x4503 - Annotation Atom = 0x18d0a - AnnotationXml Atom = 0x18d0e - Applet Atom = 0x2d106 - Area Atom = 0x31804 - Article Atom = 0x39907 - Aside Atom = 0x4f05 - Async Atom = 0x9305 - Audio Atom = 0xaf05 - Autocomplete Atom = 0xd50c - Autofocus Atom = 0xe109 - Autoplay Atom = 0x10c08 - B Atom = 0x101 - Base Atom = 0x11404 - Basefont Atom = 0x11408 - Bdi Atom = 0x1a03 - Bdo Atom = 0x12503 - Bgsound Atom = 0x13807 - Big Atom = 0x14403 - Blink Atom = 0x14705 - Blockquote Atom = 0x14c0a - Body Atom = 0x2f04 - Border Atom = 0x15606 - Br Atom = 0x202 - Button Atom = 0x15c06 - Canvas Atom = 0x4b06 - Caption Atom = 0x1e007 - Center Atom = 0x2df06 - Challenge Atom = 0x23e09 - Charset Atom = 0x2807 - Checked Atom = 0x33f07 - Cite Atom = 0x9704 - Class Atom = 0x3d905 - Code Atom = 0x16f04 - Col Atom = 0x17603 - Colgroup Atom = 0x17608 - Color Atom = 0x18305 - Cols Atom = 0x18804 - Colspan Atom = 0x18807 - Command Atom = 0x19b07 - Content Atom = 0x42c07 - Contenteditable Atom = 0x42c0f - Contextmenu Atom = 0x3480b - Controls Atom = 0x1ae08 - Coords Atom = 0x1ba06 - Crossorigin Atom = 0x1c40b - Data Atom = 0x44304 - Datalist Atom = 0x44308 - Datetime Atom = 0x25b08 - Dd Atom = 0x28802 - Default Atom = 0x5207 - Defer Atom = 0x17105 - Del Atom = 0x4d603 - Desc Atom = 0x4804 - Details Atom = 0x6507 - Dfn Atom = 0x8303 - Dialog Atom = 0x1b06 - Dir Atom = 0x9d03 - Dirname Atom = 0x9d07 - Disabled Atom = 0x10008 - Div Atom = 0x10703 - Dl Atom = 0x13e02 - Download Atom = 0x40908 - Draggable Atom = 0x1a109 - Dropzone Atom = 0x3a208 - Dt Atom = 0x4e402 - Em Atom = 0x7f02 - Embed Atom = 0x7f05 - Enctype Atom = 0x23007 - Face Atom = 0x2dd04 - Fieldset Atom = 0x1d508 - Figcaption Atom = 0x1dd0a - Figure Atom = 0x1f106 - Font Atom = 0x11804 - Footer Atom = 0x5906 - For Atom = 0x1fd03 - ForeignObject Atom = 0x1fd0d - Foreignobject Atom = 0x20a0d - Form Atom = 0x21704 - Formaction Atom = 0x2170a - Formenctype Atom = 0x22c0b - Formmethod Atom = 0x2470a - Formnovalidate Atom = 0x2510e - Formtarget Atom = 0x2660a - Frame Atom = 0x8705 - Frameset Atom = 0x8708 - H1 Atom = 0x13602 - H2 Atom = 0x29602 - H3 Atom = 0x2c502 - H4 Atom = 0x30e02 - H5 Atom = 0x4e602 - H6 Atom = 0x27002 - Head Atom = 0x2fa04 - Header Atom = 0x2fa06 - Headers Atom = 0x2fa07 - Height Atom = 0x27206 - Hgroup Atom = 0x27a06 - Hidden Atom = 0x28606 - High Atom = 0x29304 - Hr Atom = 0x13102 - Href Atom = 0x29804 - Hreflang Atom = 0x29808 - Html Atom = 0x27604 - HttpEquiv Atom = 0x2a00a - I Atom = 0x601 - Icon Atom = 0x42b04 - Id Atom = 0x5102 - Iframe Atom = 0x2b406 - Image Atom = 0x2ba05 - Img Atom = 0x2bf03 - Inert Atom = 0x4c105 - Input Atom = 0x3f605 - Ins Atom = 0x1cd03 - Isindex Atom = 0x2c707 - Ismap Atom = 0x2ce05 - Itemid Atom = 0x9806 - Itemprop Atom = 0x57e08 - Itemref Atom = 0x2d707 - Itemscope Atom = 0x2e509 - Itemtype Atom = 0x2ef08 - Kbd Atom = 0x1903 - Keygen Atom = 0x3906 - Keytype Atom = 0x51207 - Kind Atom = 0xfd04 - Label Atom = 0xba05 - Lang Atom = 0x29c04 - Legend Atom = 0x1a806 - Li Atom = 0x1202 - Link Atom = 0x14804 - List Atom = 0x44704 - Listing Atom = 0x44707 - Loop Atom = 0xbe04 - Low Atom = 0x13f03 - Malignmark Atom = 0x100a - Manifest Atom = 0x5b608 - Map Atom = 0x2d003 - Mark Atom = 0x1604 - Marquee Atom = 0x5f207 - Math Atom = 0x2f704 - Max Atom = 0x30603 - Maxlength Atom = 0x30609 - Media Atom = 0xa205 - Mediagroup Atom = 0xa20a - Menu Atom = 0x34f04 - Meta Atom = 0x45604 - Meter Atom = 0x26105 - Method Atom = 0x24b06 - Mglyph Atom = 0x2c006 - Mi Atom = 0x9b02 - Min Atom = 0x31003 - Mn Atom = 0x25402 - Mo Atom = 0x47a02 - Ms Atom = 0x2e802 - Mtext Atom = 0x31305 - Multiple Atom = 0x32108 - Muted Atom = 0x32905 - Name Atom = 0xa004 - Nav Atom = 0x3e03 - Nobr Atom = 0x7404 - Noembed Atom = 0x7d07 - Noframes Atom = 0x8508 - Noscript Atom = 0x28b08 - Novalidate Atom = 0x2550a - Object Atom = 0x21106 - Ol Atom = 0xcd02 - Onabort Atom = 0x16007 - Onafterprint Atom = 0x1e50c - Onbeforeprint Atom = 0x21f0d - Onbeforeunload Atom = 0x5c90e - Onblur Atom = 0x3e206 - Oncancel Atom = 0xb308 - Oncanplay Atom = 0x12709 - Oncanplaythrough Atom = 0x12710 - Onchange Atom = 0x3b808 - Onclick Atom = 0x2ad07 - Onclose Atom = 0x32e07 - Oncontextmenu Atom = 0x3460d - Oncuechange Atom = 0x3530b - Ondblclick Atom = 0x35e0a - Ondrag Atom = 0x36806 - Ondragend Atom = 0x36809 - Ondragenter Atom = 0x3710b - Ondragleave Atom = 0x37c0b - Ondragover Atom = 0x3870a - Ondragstart Atom = 0x3910b - Ondrop Atom = 0x3a006 - Ondurationchange Atom = 0x3b010 - Onemptied Atom = 0x3a709 - Onended Atom = 0x3c007 - Onerror Atom = 0x3c707 - Onfocus Atom = 0x3ce07 - Onhashchange Atom = 0x3e80c - Oninput Atom = 0x3f407 - Oninvalid Atom = 0x3fb09 - Onkeydown Atom = 0x40409 - Onkeypress Atom = 0x4110a - Onkeyup Atom = 0x42107 - Onload Atom = 0x43b06 - Onloadeddata Atom = 0x43b0c - Onloadedmetadata Atom = 0x44e10 - Onloadstart Atom = 0x4640b - Onmessage Atom = 0x46f09 - Onmousedown Atom = 0x4780b - Onmousemove Atom = 0x4830b - Onmouseout Atom = 0x48e0a - Onmouseover Atom = 0x49b0b - Onmouseup Atom = 0x4a609 - Onmousewheel Atom = 0x4af0c - Onoffline Atom = 0x4bb09 - Ononline Atom = 0x4c608 - Onpagehide Atom = 0x4ce0a - Onpageshow Atom = 0x4d90a - Onpause Atom = 0x4e807 - Onplay Atom = 0x4f206 - Onplaying Atom = 0x4f209 - Onpopstate Atom = 0x4fb0a - Onprogress Atom = 0x5050a - Onratechange Atom = 0x5190c - Onreset Atom = 0x52507 - Onresize Atom = 0x52c08 - Onscroll Atom = 0x53a08 - Onseeked Atom = 0x54208 - Onseeking Atom = 0x54a09 - Onselect Atom = 0x55308 - Onshow Atom = 0x55d06 - Onstalled Atom = 0x56609 - Onstorage Atom = 0x56f09 - Onsubmit Atom = 0x57808 - Onsuspend Atom = 0x58809 - Ontimeupdate Atom = 0x1190c - Onunload Atom = 0x59108 - Onvolumechange Atom = 0x5990e - Onwaiting Atom = 0x5a709 - Open Atom = 0x58404 - Optgroup Atom = 0xc008 - Optimum Atom = 0x5b007 - Option Atom = 0x5c506 - Output Atom = 0x49506 - P Atom = 0xc01 - Param Atom = 0xc05 - Pattern Atom = 0x6e07 - Ping Atom = 0xab04 - Placeholder Atom = 0xc70b - Plaintext Atom = 0xf109 - Poster Atom = 0x17d06 - Pre Atom = 0x27f03 - Preload Atom = 0x27f07 - Progress Atom = 0x50708 - Prompt Atom = 0x5bf06 - Public Atom = 0x42706 - Q Atom = 0x15101 - Radiogroup Atom = 0x30a - Readonly Atom = 0x31908 - Rel Atom = 0x28003 - Required Atom = 0x1f508 - Reversed Atom = 0x5e08 - Rows Atom = 0x7704 - Rowspan Atom = 0x7707 - Rp Atom = 0x1eb02 - Rt Atom = 0x16502 - Ruby Atom = 0xd104 - S Atom = 0x2c01 - Samp Atom = 0x6b04 - Sandbox Atom = 0xe907 - Scope Atom = 0x2e905 - Scoped Atom = 0x2e906 - Script Atom = 0x28d06 - Seamless Atom = 0x33308 - Section Atom = 0x3dd07 - Select Atom = 0x55506 - Selected Atom = 0x55508 - Shape Atom = 0x1b505 - Size Atom = 0x53004 - Sizes Atom = 0x53005 - Small Atom = 0x1bf05 - Source Atom = 0x1cf06 - Spacer Atom = 0x30006 - Span Atom = 0x7a04 - Spellcheck Atom = 0x33a0a - Src Atom = 0x3d403 - Srcdoc Atom = 0x3d406 - Srclang Atom = 0x41a07 - Start Atom = 0x39705 - Step Atom = 0x5bc04 - Strike Atom = 0x50e06 - Strong Atom = 0x53406 - Style Atom = 0x5db05 - Sub Atom = 0x57a03 - Summary Atom = 0x5e007 - Sup Atom = 0x5e703 - Svg Atom = 0x5ea03 - System Atom = 0x5ed06 - Tabindex Atom = 0x45c08 - Table Atom = 0x43605 - Target Atom = 0x26a06 - Tbody Atom = 0x2e05 - Td Atom = 0x4702 - Textarea Atom = 0x31408 - Tfoot Atom = 0x5805 - Th Atom = 0x13002 - Thead Atom = 0x2f905 - Time Atom = 0x11b04 - Title Atom = 0x8e05 - Tr Atom = 0xf902 - Track Atom = 0xf905 - Translate Atom = 0x16609 - Tt Atom = 0x7002 - Type Atom = 0x23304 - Typemustmatch Atom = 0x2330d - U Atom = 0xb01 - Ul Atom = 0x5602 - Usemap Atom = 0x4ec06 - Value Atom = 0x4005 - Var Atom = 0x10903 - Video Atom = 0x2a905 - Wbr Atom = 0x14103 - Width Atom = 0x4e205 - Wrap Atom = 0x56204 - Xmp Atom = 0xef03 -) - -const hash0 = 0xc17da63e - -const maxAtomLen = 16 - -var table = [1 << 9]Atom{ - 0x1: 0x4830b, // onmousemove - 0x2: 0x5a709, // onwaiting - 0x4: 0x5bf06, // prompt - 0x7: 0x5b007, // optimum - 0x8: 0x1604, // mark - 0xa: 0x2d707, // itemref - 0xb: 0x4d90a, // onpageshow - 0xc: 0x55506, // select - 0xd: 0x1a109, // draggable - 0xe: 0x3e03, // nav - 0xf: 0x19b07, // command - 0x11: 0xb01, // u - 0x14: 0x2fa07, // headers - 0x15: 0x44308, // datalist - 0x17: 0x6b04, // samp - 0x1a: 0x40409, // onkeydown - 0x1b: 0x53a08, // onscroll - 0x1c: 0x17603, // col - 0x20: 0x57e08, // itemprop - 0x21: 0x2a00a, // http-equiv - 0x22: 0x5e703, // sup - 0x24: 0x1f508, // required - 0x2b: 0x27f07, // preload - 0x2c: 0x21f0d, // onbeforeprint - 0x2d: 0x3710b, // ondragenter - 0x2e: 0x4e402, // dt - 0x2f: 0x57808, // onsubmit - 0x30: 0x13102, // hr - 0x31: 0x3460d, // oncontextmenu - 0x33: 0x2ba05, // image - 0x34: 0x4e807, // onpause - 0x35: 0x27a06, // hgroup - 0x36: 0xab04, // ping - 0x37: 0x55308, // onselect - 0x3a: 0x10703, // div - 0x40: 0x9b02, // mi - 0x41: 0x33308, // seamless - 0x42: 0x2807, // charset - 0x43: 0x5102, // id - 0x44: 0x4fb0a, // onpopstate - 0x45: 0x4d603, // del - 0x46: 0x5f207, // marquee - 0x47: 0x3309, // accesskey - 0x49: 0x5906, // footer - 0x4a: 0x2d106, // applet - 0x4b: 0x2ce05, // ismap - 0x51: 0x34f04, // menu - 0x52: 0x2f04, // body - 0x55: 0x8708, // frameset - 0x56: 0x52507, // onreset - 0x57: 0x14705, // blink - 0x58: 0x8e05, // title - 0x59: 0x39907, // article - 0x5b: 0x13002, // th - 0x5d: 0x15101, // q - 0x5e: 0x58404, // open - 0x5f: 0x31804, // area - 0x61: 0x43b06, // onload - 0x62: 0x3f605, // input - 0x63: 0x11404, // base - 0x64: 0x18807, // colspan - 0x65: 0x51207, // keytype - 0x66: 0x13e02, // dl - 0x68: 0x1d508, // fieldset - 0x6a: 0x31003, // min - 0x6b: 0x10903, // var - 0x6f: 0x2fa06, // header - 0x70: 0x16502, // rt - 0x71: 0x17608, // colgroup - 0x72: 0x25402, // mn - 0x74: 0x16007, // onabort - 0x75: 0x3906, // keygen - 0x76: 0x4bb09, // onoffline - 0x77: 0x23e09, // challenge - 0x78: 0x2d003, // map - 0x7a: 0x30e02, // h4 - 0x7b: 0x3c707, // onerror - 0x7c: 0x30609, // maxlength - 0x7d: 0x31305, // mtext - 0x7e: 0x5805, // tfoot - 0x7f: 0x11804, // font - 0x80: 0x100a, // malignmark - 0x81: 0x45604, // meta - 0x82: 0x9305, // async - 0x83: 0x2c502, // h3 - 0x84: 0x28802, // dd - 0x85: 0x29804, // href - 0x86: 0xa20a, // mediagroup - 0x87: 0x1ba06, // coords - 0x88: 0x41a07, // srclang - 0x89: 0x35e0a, // ondblclick - 0x8a: 0x4005, // value - 0x8c: 0xb308, // oncancel - 0x8e: 0x33a0a, // spellcheck - 0x8f: 0x8705, // frame - 0x91: 0x14403, // big - 0x94: 0x21b06, // action - 0x95: 0x9d03, // dir - 0x97: 0x31908, // readonly - 0x99: 0x43605, // table - 0x9a: 0x5e007, // summary - 0x9b: 0x14103, // wbr - 0x9c: 0x30a, // radiogroup - 0x9d: 0xa004, // name - 0x9f: 0x5ed06, // system - 0xa1: 0x18305, // color - 0xa2: 0x4b06, // canvas - 0xa3: 0x27604, // html - 0xa5: 0x54a09, // onseeking - 0xac: 0x1b505, // shape - 0xad: 0x28003, // rel - 0xae: 0x12710, // oncanplaythrough - 0xaf: 0x3870a, // ondragover - 0xb1: 0x1fd0d, // foreignObject - 0xb3: 0x7704, // rows - 0xb6: 0x44707, // listing - 0xb7: 0x49506, // output - 0xb9: 0x3480b, // contextmenu - 0xbb: 0x13f03, // low - 0xbc: 0x1eb02, // rp - 0xbd: 0x58809, // onsuspend - 0xbe: 0x15c06, // button - 0xbf: 0x4804, // desc - 0xc1: 0x3dd07, // section - 0xc2: 0x5050a, // onprogress - 0xc3: 0x56f09, // onstorage - 0xc4: 0x2f704, // math - 0xc5: 0x4f206, // onplay - 0xc7: 0x5602, // ul - 0xc8: 0x6e07, // pattern - 0xc9: 0x4af0c, // onmousewheel - 0xca: 0x36809, // ondragend - 0xcb: 0xd104, // ruby - 0xcc: 0xc01, // p - 0xcd: 0x32e07, // onclose - 0xce: 0x26105, // meter - 0xcf: 0x13807, // bgsound - 0xd2: 0x27206, // height - 0xd4: 0x101, // b - 0xd5: 0x2ef08, // itemtype - 0xd8: 0x1e007, // caption - 0xd9: 0x10008, // disabled - 0xdc: 0x5ea03, // svg - 0xdd: 0x1bf05, // small - 0xde: 0x44304, // data - 0xe0: 0x4c608, // ononline - 0xe1: 0x2c006, // mglyph - 0xe3: 0x7f05, // embed - 0xe4: 0xf902, // tr - 0xe5: 0x4640b, // onloadstart - 0xe7: 0x3b010, // ondurationchange - 0xed: 0x12503, // bdo - 0xee: 0x4702, // td - 0xef: 0x4f05, // aside - 0xf0: 0x29602, // h2 - 0xf1: 0x50708, // progress - 0xf2: 0x14c0a, // blockquote - 0xf4: 0xba05, // label - 0xf5: 0x601, // i - 0xf7: 0x7707, // rowspan - 0xfb: 0x4f209, // onplaying - 0xfd: 0x2bf03, // img - 0xfe: 0xc008, // optgroup - 0xff: 0x42c07, // content - 0x101: 0x5190c, // onratechange - 0x103: 0x3e80c, // onhashchange - 0x104: 0x6507, // details - 0x106: 0x40908, // download - 0x109: 0xe907, // sandbox - 0x10b: 0x42c0f, // contenteditable - 0x10d: 0x37c0b, // ondragleave - 0x10e: 0x2106, // accept - 0x10f: 0x55508, // selected - 0x112: 0x2170a, // formaction - 0x113: 0x2df06, // center - 0x115: 0x44e10, // onloadedmetadata - 0x116: 0x14804, // link - 0x117: 0x11b04, // time - 0x118: 0x1c40b, // crossorigin - 0x119: 0x3ce07, // onfocus - 0x11a: 0x56204, // wrap - 0x11b: 0x42b04, // icon - 0x11d: 0x2a905, // video - 0x11e: 0x3d905, // class - 0x121: 0x5990e, // onvolumechange - 0x122: 0x3e206, // onblur - 0x123: 0x2e509, // itemscope - 0x124: 0x5db05, // style - 0x127: 0x42706, // public - 0x129: 0x2510e, // formnovalidate - 0x12a: 0x55d06, // onshow - 0x12c: 0x16609, // translate - 0x12d: 0x9704, // cite - 0x12e: 0x2e802, // ms - 0x12f: 0x1190c, // ontimeupdate - 0x130: 0xfd04, // kind - 0x131: 0x2660a, // formtarget - 0x135: 0x3c007, // onended - 0x136: 0x28606, // hidden - 0x137: 0x2c01, // s - 0x139: 0x2470a, // formmethod - 0x13a: 0x44704, // list - 0x13c: 0x27002, // h6 - 0x13d: 0xcd02, // ol - 0x13e: 0x3530b, // oncuechange - 0x13f: 0x20a0d, // foreignobject - 0x143: 0x5c90e, // onbeforeunload - 0x145: 0x3a709, // onemptied - 0x146: 0x17105, // defer - 0x147: 0xef03, // xmp - 0x148: 0xaf05, // audio - 0x149: 0x1903, // kbd - 0x14c: 0x46f09, // onmessage - 0x14d: 0x5c506, // option - 0x14e: 0x4503, // alt - 0x14f: 0x33f07, // checked - 0x150: 0x10c08, // autoplay - 0x152: 0x202, // br - 0x153: 0x2550a, // novalidate - 0x156: 0x7d07, // noembed - 0x159: 0x2ad07, // onclick - 0x15a: 0x4780b, // onmousedown - 0x15b: 0x3b808, // onchange - 0x15e: 0x3fb09, // oninvalid - 0x15f: 0x2e906, // scoped - 0x160: 0x1ae08, // controls - 0x161: 0x32905, // muted - 0x163: 0x4ec06, // usemap - 0x164: 0x1dd0a, // figcaption - 0x165: 0x36806, // ondrag - 0x166: 0x29304, // high - 0x168: 0x3d403, // src - 0x169: 0x17d06, // poster - 0x16b: 0x18d0e, // annotation-xml - 0x16c: 0x5bc04, // step - 0x16d: 0x4, // abbr - 0x16e: 0x1b06, // dialog - 0x170: 0x1202, // li - 0x172: 0x47a02, // mo - 0x175: 0x1fd03, // for - 0x176: 0x1cd03, // ins - 0x178: 0x53004, // size - 0x17a: 0x5207, // default - 0x17b: 0x1a03, // bdi - 0x17c: 0x4ce0a, // onpagehide - 0x17d: 0x9d07, // dirname - 0x17e: 0x23304, // type - 0x17f: 0x21704, // form - 0x180: 0x4c105, // inert - 0x181: 0x12709, // oncanplay - 0x182: 0x8303, // dfn - 0x183: 0x45c08, // tabindex - 0x186: 0x7f02, // em - 0x187: 0x29c04, // lang - 0x189: 0x3a208, // dropzone - 0x18a: 0x4110a, // onkeypress - 0x18b: 0x25b08, // datetime - 0x18c: 0x18804, // cols - 0x18d: 0x1, // a - 0x18e: 0x43b0c, // onloadeddata - 0x191: 0x15606, // border - 0x192: 0x2e05, // tbody - 0x193: 0x24b06, // method - 0x195: 0xbe04, // loop - 0x196: 0x2b406, // iframe - 0x198: 0x2fa04, // head - 0x19e: 0x5b608, // manifest - 0x19f: 0xe109, // autofocus - 0x1a0: 0x16f04, // code - 0x1a1: 0x53406, // strong - 0x1a2: 0x32108, // multiple - 0x1a3: 0xc05, // param - 0x1a6: 0x23007, // enctype - 0x1a7: 0x2dd04, // face - 0x1a8: 0xf109, // plaintext - 0x1a9: 0x13602, // h1 - 0x1aa: 0x56609, // onstalled - 0x1ad: 0x28d06, // script - 0x1ae: 0x30006, // spacer - 0x1af: 0x52c08, // onresize - 0x1b0: 0x49b0b, // onmouseover - 0x1b1: 0x59108, // onunload - 0x1b2: 0x54208, // onseeked - 0x1b4: 0x2330d, // typemustmatch - 0x1b5: 0x1f106, // figure - 0x1b6: 0x48e0a, // onmouseout - 0x1b7: 0x27f03, // pre - 0x1b8: 0x4e205, // width - 0x1bb: 0x7404, // nobr - 0x1be: 0x7002, // tt - 0x1bf: 0x1105, // align - 0x1c0: 0x3f407, // oninput - 0x1c3: 0x42107, // onkeyup - 0x1c6: 0x1e50c, // onafterprint - 0x1c7: 0x210e, // accept-charset - 0x1c8: 0x9806, // itemid - 0x1cb: 0x50e06, // strike - 0x1cc: 0x57a03, // sub - 0x1cd: 0xf905, // track - 0x1ce: 0x39705, // start - 0x1d0: 0x11408, // basefont - 0x1d6: 0x1cf06, // source - 0x1d7: 0x1a806, // legend - 0x1d8: 0x2f905, // thead - 0x1da: 0x2e905, // scope - 0x1dd: 0x21106, // object - 0x1de: 0xa205, // media - 0x1df: 0x18d0a, // annotation - 0x1e0: 0x22c0b, // formenctype - 0x1e2: 0x28b08, // noscript - 0x1e4: 0x53005, // sizes - 0x1e5: 0xd50c, // autocomplete - 0x1e6: 0x7a04, // span - 0x1e7: 0x8508, // noframes - 0x1e8: 0x26a06, // target - 0x1e9: 0x3a006, // ondrop - 0x1ea: 0x3d406, // srcdoc - 0x1ec: 0x5e08, // reversed - 0x1f0: 0x2c707, // isindex - 0x1f3: 0x29808, // hreflang - 0x1f5: 0x4e602, // h5 - 0x1f6: 0x5d507, // address - 0x1fa: 0x30603, // max - 0x1fb: 0xc70b, // placeholder - 0x1fc: 0x31408, // textarea - 0x1fe: 0x4a609, // onmouseup - 0x1ff: 0x3910b, // ondragstart -} - -const atomText = "abbradiogrouparamalignmarkbdialogaccept-charsetbodyaccesskey" + - "genavaluealtdescanvasidefaultfootereversedetailsampatternobr" + - "owspanoembedfnoframesetitleasyncitemidirnamediagroupingaudio" + - "ncancelabelooptgrouplaceholderubyautocompleteautofocusandbox" + - "mplaintextrackindisabledivarautoplaybasefontimeupdatebdoncan" + - "playthrough1bgsoundlowbrbigblinkblockquoteborderbuttonabortr" + - "anslatecodefercolgroupostercolorcolspannotation-xmlcommandra" + - "ggablegendcontrolshapecoordsmallcrossoriginsourcefieldsetfig" + - "captionafterprintfigurequiredforeignObjectforeignobjectforma" + - "ctionbeforeprintformenctypemustmatchallengeformmethodformnov" + - "alidatetimeterformtargeth6heightmlhgroupreloadhiddenoscripth" + - "igh2hreflanghttp-equivideonclickiframeimageimglyph3isindexis" + - "mappletitemrefacenteritemscopeditemtypematheaderspacermaxlen" + - "gth4minmtextareadonlymultiplemutedoncloseamlesspellcheckedon" + - "contextmenuoncuechangeondblclickondragendondragenterondragle" + - "aveondragoverondragstarticleondropzonemptiedondurationchange" + - "onendedonerroronfocusrcdoclassectionbluronhashchangeoninputo" + - "ninvalidonkeydownloadonkeypressrclangonkeyupublicontentedita" + - "bleonloadeddatalistingonloadedmetadatabindexonloadstartonmes" + - "sageonmousedownonmousemoveonmouseoutputonmouseoveronmouseupo" + - "nmousewheelonofflinertononlineonpagehidelonpageshowidth5onpa" + - "usemaponplayingonpopstateonprogresstrikeytypeonratechangeonr" + - "esetonresizestrongonscrollonseekedonseekingonselectedonshowr" + - "aponstalledonstorageonsubmitempropenonsuspendonunloadonvolum" + - "echangeonwaitingoptimumanifestepromptoptionbeforeunloaddress" + - "tylesummarysupsvgsystemarquee" diff --git a/src/code.google.com/p/go.net/html/atom/table_test.go b/src/code.google.com/p/go.net/html/atom/table_test.go deleted file mode 100644 index db016a1c01c..00000000000 --- a/src/code.google.com/p/go.net/html/atom/table_test.go +++ /dev/null @@ -1,341 +0,0 @@ -// generated by go run gen.go -test; DO NOT EDIT - -package atom - -var testAtomList = []string{ - "a", - "abbr", - "accept", - "accept-charset", - "accesskey", - "action", - "address", - "align", - "alt", - "annotation", - "annotation-xml", - "applet", - "area", - "article", - "aside", - "async", - "audio", - "autocomplete", - "autofocus", - "autoplay", - "b", - "base", - "basefont", - "bdi", - "bdo", - "bgsound", - "big", - "blink", - "blockquote", - "body", - "border", - "br", - "button", - "canvas", - "caption", - "center", - "challenge", - "charset", - "checked", - "cite", - "cite", - "class", - "code", - "col", - "colgroup", - "color", - "cols", - "colspan", - "command", - "command", - "content", - "contenteditable", - "contextmenu", - "controls", - "coords", - "crossorigin", - "data", - "data", - "datalist", - "datetime", - "dd", - "default", - "defer", - "del", - "desc", - "details", - "dfn", - "dialog", - "dir", - "dirname", - "disabled", - "div", - "dl", - "download", - "draggable", - "dropzone", - "dt", - "em", - "embed", - "enctype", - "face", - "fieldset", - "figcaption", - "figure", - "font", - "footer", - "for", - "foreignObject", - "foreignobject", - "form", - "form", - "formaction", - "formenctype", - "formmethod", - "formnovalidate", - "formtarget", - "frame", - "frameset", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "headers", - "height", - "hgroup", - "hidden", - "high", - "hr", - "href", - "hreflang", - "html", - "http-equiv", - "i", - "icon", - "id", - "iframe", - "image", - "img", - "inert", - "input", - "ins", - "isindex", - "ismap", - "itemid", - "itemprop", - "itemref", - "itemscope", - "itemtype", - "kbd", - "keygen", - "keytype", - "kind", - "label", - "label", - "lang", - "legend", - "li", - "link", - "list", - "listing", - "loop", - "low", - "malignmark", - "manifest", - "map", - "mark", - "marquee", - "math", - "max", - "maxlength", - "media", - "mediagroup", - "menu", - "meta", - "meter", - "method", - "mglyph", - "mi", - "min", - "mn", - "mo", - "ms", - "mtext", - "multiple", - "muted", - "name", - "nav", - "nobr", - "noembed", - "noframes", - "noscript", - "novalidate", - "object", - "ol", - "onabort", - "onafterprint", - "onbeforeprint", - "onbeforeunload", - "onblur", - "oncancel", - "oncanplay", - "oncanplaythrough", - "onchange", - "onclick", - "onclose", - "oncontextmenu", - "oncuechange", - "ondblclick", - "ondrag", - "ondragend", - "ondragenter", - "ondragleave", - "ondragover", - "ondragstart", - "ondrop", - "ondurationchange", - "onemptied", - "onended", - "onerror", - "onfocus", - "onhashchange", - "oninput", - "oninvalid", - "onkeydown", - "onkeypress", - "onkeyup", - "onload", - "onloadeddata", - "onloadedmetadata", - "onloadstart", - "onmessage", - "onmousedown", - "onmousemove", - "onmouseout", - "onmouseover", - "onmouseup", - "onmousewheel", - "onoffline", - "ononline", - "onpagehide", - "onpageshow", - "onpause", - "onplay", - "onplaying", - "onpopstate", - "onprogress", - "onratechange", - "onreset", - "onresize", - "onscroll", - "onseeked", - "onseeking", - "onselect", - "onshow", - "onstalled", - "onstorage", - "onsubmit", - "onsuspend", - "ontimeupdate", - "onunload", - "onvolumechange", - "onwaiting", - "open", - "optgroup", - "optimum", - "option", - "output", - "p", - "param", - "pattern", - "ping", - "placeholder", - "plaintext", - "poster", - "pre", - "preload", - "progress", - "prompt", - "public", - "q", - "radiogroup", - "readonly", - "rel", - "required", - "reversed", - "rows", - "rowspan", - "rp", - "rt", - "ruby", - "s", - "samp", - "sandbox", - "scope", - "scoped", - "script", - "seamless", - "section", - "select", - "selected", - "shape", - "size", - "sizes", - "small", - "source", - "spacer", - "span", - "span", - "spellcheck", - "src", - "srcdoc", - "srclang", - "start", - "step", - "strike", - "strong", - "style", - "style", - "sub", - "summary", - "sup", - "svg", - "system", - "tabindex", - "table", - "target", - "tbody", - "td", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "title", - "tr", - "track", - "translate", - "tt", - "type", - "typemustmatch", - "u", - "ul", - "usemap", - "value", - "var", - "video", - "wbr", - "width", - "wrap", - "xmp", -} diff --git a/src/code.google.com/p/go.net/html/const.go b/src/code.google.com/p/go.net/html/const.go deleted file mode 100644 index d7cc8bb9a99..00000000000 --- a/src/code.google.com/p/go.net/html/const.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -// Section 12.2.3.2 of the HTML5 specification says "The following elements -// have varying levels of special parsing rules". -// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#the-stack-of-open-elements -var isSpecialElementMap = map[string]bool{ - "address": true, - "applet": true, - "area": true, - "article": true, - "aside": true, - "base": true, - "basefont": true, - "bgsound": true, - "blockquote": true, - "body": true, - "br": true, - "button": true, - "caption": true, - "center": true, - "col": true, - "colgroup": true, - "command": true, - "dd": true, - "details": true, - "dir": true, - "div": true, - "dl": true, - "dt": true, - "embed": true, - "fieldset": true, - "figcaption": true, - "figure": true, - "footer": true, - "form": true, - "frame": true, - "frameset": true, - "h1": true, - "h2": true, - "h3": true, - "h4": true, - "h5": true, - "h6": true, - "head": true, - "header": true, - "hgroup": true, - "hr": true, - "html": true, - "iframe": true, - "img": true, - "input": true, - "isindex": true, - "li": true, - "link": true, - "listing": true, - "marquee": true, - "menu": true, - "meta": true, - "nav": true, - "noembed": true, - "noframes": true, - "noscript": true, - "object": true, - "ol": true, - "p": true, - "param": true, - "plaintext": true, - "pre": true, - "script": true, - "section": true, - "select": true, - "style": true, - "summary": true, - "table": true, - "tbody": true, - "td": true, - "textarea": true, - "tfoot": true, - "th": true, - "thead": true, - "title": true, - "tr": true, - "ul": true, - "wbr": true, - "xmp": true, -} - -func isSpecialElement(element *Node) bool { - switch element.Namespace { - case "", "html": - return isSpecialElementMap[element.Data] - case "svg": - return element.Data == "foreignObject" - } - return false -} diff --git a/src/code.google.com/p/go.net/html/doc.go b/src/code.google.com/p/go.net/html/doc.go deleted file mode 100644 index fac0f54e78a..00000000000 --- a/src/code.google.com/p/go.net/html/doc.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package html implements an HTML5-compliant tokenizer and parser. - -Tokenization is done by creating a Tokenizer for an io.Reader r. It is the -caller's responsibility to ensure that r provides UTF-8 encoded HTML. - - z := html.NewTokenizer(r) - -Given a Tokenizer z, the HTML is tokenized by repeatedly calling z.Next(), -which parses the next token and returns its type, or an error: - - for { - tt := z.Next() - if tt == html.ErrorToken { - // ... - return ... - } - // Process the current token. - } - -There are two APIs for retrieving the current token. The high-level API is to -call Token; the low-level API is to call Text or TagName / TagAttr. Both APIs -allow optionally calling Raw after Next but before Token, Text, TagName, or -TagAttr. In EBNF notation, the valid call sequence per token is: - - Next {Raw} [ Token | Text | TagName {TagAttr} ] - -Token returns an independent data structure that completely describes a token. -Entities (such as "<") are unescaped, tag names and attribute keys are -lower-cased, and attributes are collected into a []Attribute. For example: - - for { - if z.Next() == html.ErrorToken { - // Returning io.EOF indicates success. - return z.Err() - } - emitToken(z.Token()) - } - -The low-level API performs fewer allocations and copies, but the contents of -the []byte values returned by Text, TagName and TagAttr may change on the next -call to Next. For example, to extract an HTML page's anchor text: - - depth := 0 - for { - tt := z.Next() - switch tt { - case ErrorToken: - return z.Err() - case TextToken: - if depth > 0 { - // emitBytes should copy the []byte it receives, - // if it doesn't process it immediately. - emitBytes(z.Text()) - } - case StartTagToken, EndTagToken: - tn, _ := z.TagName() - if len(tn) == 1 && tn[0] == 'a' { - if tt == StartTagToken { - depth++ - } else { - depth-- - } - } - } - } - -Parsing is done by calling Parse with an io.Reader, which returns the root of -the parse tree (the document element) as a *Node. It is the caller's -responsibility to ensure that the Reader provides UTF-8 encoded HTML. For -example, to process each anchor node in depth-first order: - - doc, err := html.Parse(r) - if err != nil { - // ... - } - var f func(*html.Node) - f = func(n *html.Node) { - if n.Type == html.ElementNode && n.Data == "a" { - // Do something with n... - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - f(c) - } - } - f(doc) - -The relevant specifications include: -http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html and -http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html -*/ -package html - -// The tokenization algorithm implemented by this package is not a line-by-line -// transliteration of the relatively verbose state-machine in the WHATWG -// specification. A more direct approach is used instead, where the program -// counter implies the state, such as whether it is tokenizing a tag or a text -// node. Specification compliance is verified by checking expected and actual -// outputs over a test suite rather than aiming for algorithmic fidelity. - -// TODO(nigeltao): Does a DOM API belong in this package or a separate one? -// TODO(nigeltao): How does parsing interact with a JavaScript engine? diff --git a/src/code.google.com/p/go.net/html/doctype.go b/src/code.google.com/p/go.net/html/doctype.go deleted file mode 100644 index c484e5a94fb..00000000000 --- a/src/code.google.com/p/go.net/html/doctype.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "strings" -) - -// parseDoctype parses the data from a DoctypeToken into a name, -// public identifier, and system identifier. It returns a Node whose Type -// is DoctypeNode, whose Data is the name, and which has attributes -// named "system" and "public" for the two identifiers if they were present. -// quirks is whether the document should be parsed in "quirks mode". -func parseDoctype(s string) (n *Node, quirks bool) { - n = &Node{Type: DoctypeNode} - - // Find the name. - space := strings.IndexAny(s, whitespace) - if space == -1 { - space = len(s) - } - n.Data = s[:space] - // The comparison to "html" is case-sensitive. - if n.Data != "html" { - quirks = true - } - n.Data = strings.ToLower(n.Data) - s = strings.TrimLeft(s[space:], whitespace) - - if len(s) < 6 { - // It can't start with "PUBLIC" or "SYSTEM". - // Ignore the rest of the string. - return n, quirks || s != "" - } - - key := strings.ToLower(s[:6]) - s = s[6:] - for key == "public" || key == "system" { - s = strings.TrimLeft(s, whitespace) - if s == "" { - break - } - quote := s[0] - if quote != '"' && quote != '\'' { - break - } - s = s[1:] - q := strings.IndexRune(s, rune(quote)) - var id string - if q == -1 { - id = s - s = "" - } else { - id = s[:q] - s = s[q+1:] - } - n.Attr = append(n.Attr, Attribute{Key: key, Val: id}) - if key == "public" { - key = "system" - } else { - key = "" - } - } - - if key != "" || s != "" { - quirks = true - } else if len(n.Attr) > 0 { - if n.Attr[0].Key == "public" { - public := strings.ToLower(n.Attr[0].Val) - switch public { - case "-//w3o//dtd w3 html strict 3.0//en//", "-/w3d/dtd html 4.0 transitional/en", "html": - quirks = true - default: - for _, q := range quirkyIDs { - if strings.HasPrefix(public, q) { - quirks = true - break - } - } - } - // The following two public IDs only cause quirks mode if there is no system ID. - if len(n.Attr) == 1 && (strings.HasPrefix(public, "-//w3c//dtd html 4.01 frameset//") || - strings.HasPrefix(public, "-//w3c//dtd html 4.01 transitional//")) { - quirks = true - } - } - if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" && - strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" { - quirks = true - } - } - - return n, quirks -} - -// quirkyIDs is a list of public doctype identifiers that cause a document -// to be interpreted in quirks mode. The identifiers should be in lower case. -var quirkyIDs = []string{ - "+//silmaril//dtd html pro v0r11 19970101//", - "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", - "-//as//dtd html 3.0 aswedit + extensions//", - "-//ietf//dtd html 2.0 level 1//", - "-//ietf//dtd html 2.0 level 2//", - "-//ietf//dtd html 2.0 strict level 1//", - "-//ietf//dtd html 2.0 strict level 2//", - "-//ietf//dtd html 2.0 strict//", - "-//ietf//dtd html 2.0//", - "-//ietf//dtd html 2.1e//", - "-//ietf//dtd html 3.0//", - "-//ietf//dtd html 3.2 final//", - "-//ietf//dtd html 3.2//", - "-//ietf//dtd html 3//", - "-//ietf//dtd html level 0//", - "-//ietf//dtd html level 1//", - "-//ietf//dtd html level 2//", - "-//ietf//dtd html level 3//", - "-//ietf//dtd html strict level 0//", - "-//ietf//dtd html strict level 1//", - "-//ietf//dtd html strict level 2//", - "-//ietf//dtd html strict level 3//", - "-//ietf//dtd html strict//", - "-//ietf//dtd html//", - "-//metrius//dtd metrius presentational//", - "-//microsoft//dtd internet explorer 2.0 html strict//", - "-//microsoft//dtd internet explorer 2.0 html//", - "-//microsoft//dtd internet explorer 2.0 tables//", - "-//microsoft//dtd internet explorer 3.0 html strict//", - "-//microsoft//dtd internet explorer 3.0 html//", - "-//microsoft//dtd internet explorer 3.0 tables//", - "-//netscape comm. corp.//dtd html//", - "-//netscape comm. corp.//dtd strict html//", - "-//o'reilly and associates//dtd html 2.0//", - "-//o'reilly and associates//dtd html extended 1.0//", - "-//o'reilly and associates//dtd html extended relaxed 1.0//", - "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", - "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", - "-//spyglass//dtd html 2.0 extended//", - "-//sq//dtd html 2.0 hotmetal + extensions//", - "-//sun microsystems corp.//dtd hotjava html//", - "-//sun microsystems corp.//dtd hotjava strict html//", - "-//w3c//dtd html 3 1995-03-24//", - "-//w3c//dtd html 3.2 draft//", - "-//w3c//dtd html 3.2 final//", - "-//w3c//dtd html 3.2//", - "-//w3c//dtd html 3.2s draft//", - "-//w3c//dtd html 4.0 frameset//", - "-//w3c//dtd html 4.0 transitional//", - "-//w3c//dtd html experimental 19960712//", - "-//w3c//dtd html experimental 970421//", - "-//w3c//dtd w3 html//", - "-//w3o//dtd w3 html 3.0//", - "-//webtechs//dtd mozilla html 2.0//", - "-//webtechs//dtd mozilla html//", -} diff --git a/src/code.google.com/p/go.net/html/entity.go b/src/code.google.com/p/go.net/html/entity.go deleted file mode 100644 index af8a007ed04..00000000000 --- a/src/code.google.com/p/go.net/html/entity.go +++ /dev/null @@ -1,2253 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -// All entities that do not end with ';' are 6 or fewer bytes long. -const longestEntityWithoutSemicolon = 6 - -// entity is a map from HTML entity names to their values. The semicolon matters: -// http://www.whatwg.org/specs/web-apps/current-work/multipage/named-character-references.html -// lists both "amp" and "amp;" as two separate entries. -// -// Note that the HTML5 list is larger than the HTML4 list at -// http://www.w3.org/TR/html4/sgml/entities.html -var entity = map[string]rune{ - "AElig;": '\U000000C6', - "AMP;": '\U00000026', - "Aacute;": '\U000000C1', - "Abreve;": '\U00000102', - "Acirc;": '\U000000C2', - "Acy;": '\U00000410', - "Afr;": '\U0001D504', - "Agrave;": '\U000000C0', - "Alpha;": '\U00000391', - "Amacr;": '\U00000100', - "And;": '\U00002A53', - "Aogon;": '\U00000104', - "Aopf;": '\U0001D538', - "ApplyFunction;": '\U00002061', - "Aring;": '\U000000C5', - "Ascr;": '\U0001D49C', - "Assign;": '\U00002254', - "Atilde;": '\U000000C3', - "Auml;": '\U000000C4', - "Backslash;": '\U00002216', - "Barv;": '\U00002AE7', - "Barwed;": '\U00002306', - "Bcy;": '\U00000411', - "Because;": '\U00002235', - "Bernoullis;": '\U0000212C', - "Beta;": '\U00000392', - "Bfr;": '\U0001D505', - "Bopf;": '\U0001D539', - "Breve;": '\U000002D8', - "Bscr;": '\U0000212C', - "Bumpeq;": '\U0000224E', - "CHcy;": '\U00000427', - "COPY;": '\U000000A9', - "Cacute;": '\U00000106', - "Cap;": '\U000022D2', - "CapitalDifferentialD;": '\U00002145', - "Cayleys;": '\U0000212D', - "Ccaron;": '\U0000010C', - "Ccedil;": '\U000000C7', - "Ccirc;": '\U00000108', - "Cconint;": '\U00002230', - "Cdot;": '\U0000010A', - "Cedilla;": '\U000000B8', - "CenterDot;": '\U000000B7', - "Cfr;": '\U0000212D', - "Chi;": '\U000003A7', - "CircleDot;": '\U00002299', - "CircleMinus;": '\U00002296', - "CirclePlus;": '\U00002295', - "CircleTimes;": '\U00002297', - "ClockwiseContourIntegral;": '\U00002232', - "CloseCurlyDoubleQuote;": '\U0000201D', - "CloseCurlyQuote;": '\U00002019', - "Colon;": '\U00002237', - "Colone;": '\U00002A74', - "Congruent;": '\U00002261', - "Conint;": '\U0000222F', - "ContourIntegral;": '\U0000222E', - "Copf;": '\U00002102', - "Coproduct;": '\U00002210', - "CounterClockwiseContourIntegral;": '\U00002233', - "Cross;": '\U00002A2F', - "Cscr;": '\U0001D49E', - "Cup;": '\U000022D3', - "CupCap;": '\U0000224D', - "DD;": '\U00002145', - "DDotrahd;": '\U00002911', - "DJcy;": '\U00000402', - "DScy;": '\U00000405', - "DZcy;": '\U0000040F', - "Dagger;": '\U00002021', - "Darr;": '\U000021A1', - "Dashv;": '\U00002AE4', - "Dcaron;": '\U0000010E', - "Dcy;": '\U00000414', - "Del;": '\U00002207', - "Delta;": '\U00000394', - "Dfr;": '\U0001D507', - "DiacriticalAcute;": '\U000000B4', - "DiacriticalDot;": '\U000002D9', - "DiacriticalDoubleAcute;": '\U000002DD', - "DiacriticalGrave;": '\U00000060', - "DiacriticalTilde;": '\U000002DC', - "Diamond;": '\U000022C4', - "DifferentialD;": '\U00002146', - "Dopf;": '\U0001D53B', - "Dot;": '\U000000A8', - "DotDot;": '\U000020DC', - "DotEqual;": '\U00002250', - "DoubleContourIntegral;": '\U0000222F', - "DoubleDot;": '\U000000A8', - "DoubleDownArrow;": '\U000021D3', - "DoubleLeftArrow;": '\U000021D0', - "DoubleLeftRightArrow;": '\U000021D4', - "DoubleLeftTee;": '\U00002AE4', - "DoubleLongLeftArrow;": '\U000027F8', - "DoubleLongLeftRightArrow;": '\U000027FA', - "DoubleLongRightArrow;": '\U000027F9', - "DoubleRightArrow;": '\U000021D2', - "DoubleRightTee;": '\U000022A8', - "DoubleUpArrow;": '\U000021D1', - "DoubleUpDownArrow;": '\U000021D5', - "DoubleVerticalBar;": '\U00002225', - "DownArrow;": '\U00002193', - "DownArrowBar;": '\U00002913', - "DownArrowUpArrow;": '\U000021F5', - "DownBreve;": '\U00000311', - "DownLeftRightVector;": '\U00002950', - "DownLeftTeeVector;": '\U0000295E', - "DownLeftVector;": '\U000021BD', - "DownLeftVectorBar;": '\U00002956', - "DownRightTeeVector;": '\U0000295F', - "DownRightVector;": '\U000021C1', - "DownRightVectorBar;": '\U00002957', - "DownTee;": '\U000022A4', - "DownTeeArrow;": '\U000021A7', - "Downarrow;": '\U000021D3', - "Dscr;": '\U0001D49F', - "Dstrok;": '\U00000110', - "ENG;": '\U0000014A', - "ETH;": '\U000000D0', - "Eacute;": '\U000000C9', - "Ecaron;": '\U0000011A', - "Ecirc;": '\U000000CA', - "Ecy;": '\U0000042D', - "Edot;": '\U00000116', - "Efr;": '\U0001D508', - "Egrave;": '\U000000C8', - "Element;": '\U00002208', - "Emacr;": '\U00000112', - "EmptySmallSquare;": '\U000025FB', - "EmptyVerySmallSquare;": '\U000025AB', - "Eogon;": '\U00000118', - "Eopf;": '\U0001D53C', - "Epsilon;": '\U00000395', - "Equal;": '\U00002A75', - "EqualTilde;": '\U00002242', - "Equilibrium;": '\U000021CC', - "Escr;": '\U00002130', - "Esim;": '\U00002A73', - "Eta;": '\U00000397', - "Euml;": '\U000000CB', - "Exists;": '\U00002203', - "ExponentialE;": '\U00002147', - "Fcy;": '\U00000424', - "Ffr;": '\U0001D509', - "FilledSmallSquare;": '\U000025FC', - "FilledVerySmallSquare;": '\U000025AA', - "Fopf;": '\U0001D53D', - "ForAll;": '\U00002200', - "Fouriertrf;": '\U00002131', - "Fscr;": '\U00002131', - "GJcy;": '\U00000403', - "GT;": '\U0000003E', - "Gamma;": '\U00000393', - "Gammad;": '\U000003DC', - "Gbreve;": '\U0000011E', - "Gcedil;": '\U00000122', - "Gcirc;": '\U0000011C', - "Gcy;": '\U00000413', - "Gdot;": '\U00000120', - "Gfr;": '\U0001D50A', - "Gg;": '\U000022D9', - "Gopf;": '\U0001D53E', - "GreaterEqual;": '\U00002265', - "GreaterEqualLess;": '\U000022DB', - "GreaterFullEqual;": '\U00002267', - "GreaterGreater;": '\U00002AA2', - "GreaterLess;": '\U00002277', - "GreaterSlantEqual;": '\U00002A7E', - "GreaterTilde;": '\U00002273', - "Gscr;": '\U0001D4A2', - "Gt;": '\U0000226B', - "HARDcy;": '\U0000042A', - "Hacek;": '\U000002C7', - "Hat;": '\U0000005E', - "Hcirc;": '\U00000124', - "Hfr;": '\U0000210C', - "HilbertSpace;": '\U0000210B', - "Hopf;": '\U0000210D', - "HorizontalLine;": '\U00002500', - "Hscr;": '\U0000210B', - "Hstrok;": '\U00000126', - "HumpDownHump;": '\U0000224E', - "HumpEqual;": '\U0000224F', - "IEcy;": '\U00000415', - "IJlig;": '\U00000132', - "IOcy;": '\U00000401', - "Iacute;": '\U000000CD', - "Icirc;": '\U000000CE', - "Icy;": '\U00000418', - "Idot;": '\U00000130', - "Ifr;": '\U00002111', - "Igrave;": '\U000000CC', - "Im;": '\U00002111', - "Imacr;": '\U0000012A', - "ImaginaryI;": '\U00002148', - "Implies;": '\U000021D2', - "Int;": '\U0000222C', - "Integral;": '\U0000222B', - "Intersection;": '\U000022C2', - "InvisibleComma;": '\U00002063', - "InvisibleTimes;": '\U00002062', - "Iogon;": '\U0000012E', - "Iopf;": '\U0001D540', - "Iota;": '\U00000399', - "Iscr;": '\U00002110', - "Itilde;": '\U00000128', - "Iukcy;": '\U00000406', - "Iuml;": '\U000000CF', - "Jcirc;": '\U00000134', - "Jcy;": '\U00000419', - "Jfr;": '\U0001D50D', - "Jopf;": '\U0001D541', - "Jscr;": '\U0001D4A5', - "Jsercy;": '\U00000408', - "Jukcy;": '\U00000404', - "KHcy;": '\U00000425', - "KJcy;": '\U0000040C', - "Kappa;": '\U0000039A', - "Kcedil;": '\U00000136', - "Kcy;": '\U0000041A', - "Kfr;": '\U0001D50E', - "Kopf;": '\U0001D542', - "Kscr;": '\U0001D4A6', - "LJcy;": '\U00000409', - "LT;": '\U0000003C', - "Lacute;": '\U00000139', - "Lambda;": '\U0000039B', - "Lang;": '\U000027EA', - "Laplacetrf;": '\U00002112', - "Larr;": '\U0000219E', - "Lcaron;": '\U0000013D', - "Lcedil;": '\U0000013B', - "Lcy;": '\U0000041B', - "LeftAngleBracket;": '\U000027E8', - "LeftArrow;": '\U00002190', - "LeftArrowBar;": '\U000021E4', - "LeftArrowRightArrow;": '\U000021C6', - "LeftCeiling;": '\U00002308', - "LeftDoubleBracket;": '\U000027E6', - "LeftDownTeeVector;": '\U00002961', - "LeftDownVector;": '\U000021C3', - "LeftDownVectorBar;": '\U00002959', - "LeftFloor;": '\U0000230A', - "LeftRightArrow;": '\U00002194', - "LeftRightVector;": '\U0000294E', - "LeftTee;": '\U000022A3', - "LeftTeeArrow;": '\U000021A4', - "LeftTeeVector;": '\U0000295A', - "LeftTriangle;": '\U000022B2', - "LeftTriangleBar;": '\U000029CF', - "LeftTriangleEqual;": '\U000022B4', - "LeftUpDownVector;": '\U00002951', - "LeftUpTeeVector;": '\U00002960', - "LeftUpVector;": '\U000021BF', - "LeftUpVectorBar;": '\U00002958', - "LeftVector;": '\U000021BC', - "LeftVectorBar;": '\U00002952', - "Leftarrow;": '\U000021D0', - "Leftrightarrow;": '\U000021D4', - "LessEqualGreater;": '\U000022DA', - "LessFullEqual;": '\U00002266', - "LessGreater;": '\U00002276', - "LessLess;": '\U00002AA1', - "LessSlantEqual;": '\U00002A7D', - "LessTilde;": '\U00002272', - "Lfr;": '\U0001D50F', - "Ll;": '\U000022D8', - "Lleftarrow;": '\U000021DA', - "Lmidot;": '\U0000013F', - "LongLeftArrow;": '\U000027F5', - "LongLeftRightArrow;": '\U000027F7', - "LongRightArrow;": '\U000027F6', - "Longleftarrow;": '\U000027F8', - "Longleftrightarrow;": '\U000027FA', - "Longrightarrow;": '\U000027F9', - "Lopf;": '\U0001D543', - "LowerLeftArrow;": '\U00002199', - "LowerRightArrow;": '\U00002198', - "Lscr;": '\U00002112', - "Lsh;": '\U000021B0', - "Lstrok;": '\U00000141', - "Lt;": '\U0000226A', - "Map;": '\U00002905', - "Mcy;": '\U0000041C', - "MediumSpace;": '\U0000205F', - "Mellintrf;": '\U00002133', - "Mfr;": '\U0001D510', - "MinusPlus;": '\U00002213', - "Mopf;": '\U0001D544', - "Mscr;": '\U00002133', - "Mu;": '\U0000039C', - "NJcy;": '\U0000040A', - "Nacute;": '\U00000143', - "Ncaron;": '\U00000147', - "Ncedil;": '\U00000145', - "Ncy;": '\U0000041D', - "NegativeMediumSpace;": '\U0000200B', - "NegativeThickSpace;": '\U0000200B', - "NegativeThinSpace;": '\U0000200B', - "NegativeVeryThinSpace;": '\U0000200B', - "NestedGreaterGreater;": '\U0000226B', - "NestedLessLess;": '\U0000226A', - "NewLine;": '\U0000000A', - "Nfr;": '\U0001D511', - "NoBreak;": '\U00002060', - "NonBreakingSpace;": '\U000000A0', - "Nopf;": '\U00002115', - "Not;": '\U00002AEC', - "NotCongruent;": '\U00002262', - "NotCupCap;": '\U0000226D', - "NotDoubleVerticalBar;": '\U00002226', - "NotElement;": '\U00002209', - "NotEqual;": '\U00002260', - "NotExists;": '\U00002204', - "NotGreater;": '\U0000226F', - "NotGreaterEqual;": '\U00002271', - "NotGreaterLess;": '\U00002279', - "NotGreaterTilde;": '\U00002275', - "NotLeftTriangle;": '\U000022EA', - "NotLeftTriangleEqual;": '\U000022EC', - "NotLess;": '\U0000226E', - "NotLessEqual;": '\U00002270', - "NotLessGreater;": '\U00002278', - "NotLessTilde;": '\U00002274', - "NotPrecedes;": '\U00002280', - "NotPrecedesSlantEqual;": '\U000022E0', - "NotReverseElement;": '\U0000220C', - "NotRightTriangle;": '\U000022EB', - "NotRightTriangleEqual;": '\U000022ED', - "NotSquareSubsetEqual;": '\U000022E2', - "NotSquareSupersetEqual;": '\U000022E3', - "NotSubsetEqual;": '\U00002288', - "NotSucceeds;": '\U00002281', - "NotSucceedsSlantEqual;": '\U000022E1', - "NotSupersetEqual;": '\U00002289', - "NotTilde;": '\U00002241', - "NotTildeEqual;": '\U00002244', - "NotTildeFullEqual;": '\U00002247', - "NotTildeTilde;": '\U00002249', - "NotVerticalBar;": '\U00002224', - "Nscr;": '\U0001D4A9', - "Ntilde;": '\U000000D1', - "Nu;": '\U0000039D', - "OElig;": '\U00000152', - "Oacute;": '\U000000D3', - "Ocirc;": '\U000000D4', - "Ocy;": '\U0000041E', - "Odblac;": '\U00000150', - "Ofr;": '\U0001D512', - "Ograve;": '\U000000D2', - "Omacr;": '\U0000014C', - "Omega;": '\U000003A9', - "Omicron;": '\U0000039F', - "Oopf;": '\U0001D546', - "OpenCurlyDoubleQuote;": '\U0000201C', - "OpenCurlyQuote;": '\U00002018', - "Or;": '\U00002A54', - "Oscr;": '\U0001D4AA', - "Oslash;": '\U000000D8', - "Otilde;": '\U000000D5', - "Otimes;": '\U00002A37', - "Ouml;": '\U000000D6', - "OverBar;": '\U0000203E', - "OverBrace;": '\U000023DE', - "OverBracket;": '\U000023B4', - "OverParenthesis;": '\U000023DC', - "PartialD;": '\U00002202', - "Pcy;": '\U0000041F', - "Pfr;": '\U0001D513', - "Phi;": '\U000003A6', - "Pi;": '\U000003A0', - "PlusMinus;": '\U000000B1', - "Poincareplane;": '\U0000210C', - "Popf;": '\U00002119', - "Pr;": '\U00002ABB', - "Precedes;": '\U0000227A', - "PrecedesEqual;": '\U00002AAF', - "PrecedesSlantEqual;": '\U0000227C', - "PrecedesTilde;": '\U0000227E', - "Prime;": '\U00002033', - "Product;": '\U0000220F', - "Proportion;": '\U00002237', - "Proportional;": '\U0000221D', - "Pscr;": '\U0001D4AB', - "Psi;": '\U000003A8', - "QUOT;": '\U00000022', - "Qfr;": '\U0001D514', - "Qopf;": '\U0000211A', - "Qscr;": '\U0001D4AC', - "RBarr;": '\U00002910', - "REG;": '\U000000AE', - "Racute;": '\U00000154', - "Rang;": '\U000027EB', - "Rarr;": '\U000021A0', - "Rarrtl;": '\U00002916', - "Rcaron;": '\U00000158', - "Rcedil;": '\U00000156', - "Rcy;": '\U00000420', - "Re;": '\U0000211C', - "ReverseElement;": '\U0000220B', - "ReverseEquilibrium;": '\U000021CB', - "ReverseUpEquilibrium;": '\U0000296F', - "Rfr;": '\U0000211C', - "Rho;": '\U000003A1', - "RightAngleBracket;": '\U000027E9', - "RightArrow;": '\U00002192', - "RightArrowBar;": '\U000021E5', - "RightArrowLeftArrow;": '\U000021C4', - "RightCeiling;": '\U00002309', - "RightDoubleBracket;": '\U000027E7', - "RightDownTeeVector;": '\U0000295D', - "RightDownVector;": '\U000021C2', - "RightDownVectorBar;": '\U00002955', - "RightFloor;": '\U0000230B', - "RightTee;": '\U000022A2', - "RightTeeArrow;": '\U000021A6', - "RightTeeVector;": '\U0000295B', - "RightTriangle;": '\U000022B3', - "RightTriangleBar;": '\U000029D0', - "RightTriangleEqual;": '\U000022B5', - "RightUpDownVector;": '\U0000294F', - "RightUpTeeVector;": '\U0000295C', - "RightUpVector;": '\U000021BE', - "RightUpVectorBar;": '\U00002954', - "RightVector;": '\U000021C0', - "RightVectorBar;": '\U00002953', - "Rightarrow;": '\U000021D2', - "Ropf;": '\U0000211D', - "RoundImplies;": '\U00002970', - "Rrightarrow;": '\U000021DB', - "Rscr;": '\U0000211B', - "Rsh;": '\U000021B1', - "RuleDelayed;": '\U000029F4', - "SHCHcy;": '\U00000429', - "SHcy;": '\U00000428', - "SOFTcy;": '\U0000042C', - "Sacute;": '\U0000015A', - "Sc;": '\U00002ABC', - "Scaron;": '\U00000160', - "Scedil;": '\U0000015E', - "Scirc;": '\U0000015C', - "Scy;": '\U00000421', - "Sfr;": '\U0001D516', - "ShortDownArrow;": '\U00002193', - "ShortLeftArrow;": '\U00002190', - "ShortRightArrow;": '\U00002192', - "ShortUpArrow;": '\U00002191', - "Sigma;": '\U000003A3', - "SmallCircle;": '\U00002218', - "Sopf;": '\U0001D54A', - "Sqrt;": '\U0000221A', - "Square;": '\U000025A1', - "SquareIntersection;": '\U00002293', - "SquareSubset;": '\U0000228F', - "SquareSubsetEqual;": '\U00002291', - "SquareSuperset;": '\U00002290', - "SquareSupersetEqual;": '\U00002292', - "SquareUnion;": '\U00002294', - "Sscr;": '\U0001D4AE', - "Star;": '\U000022C6', - "Sub;": '\U000022D0', - "Subset;": '\U000022D0', - "SubsetEqual;": '\U00002286', - "Succeeds;": '\U0000227B', - "SucceedsEqual;": '\U00002AB0', - "SucceedsSlantEqual;": '\U0000227D', - "SucceedsTilde;": '\U0000227F', - "SuchThat;": '\U0000220B', - "Sum;": '\U00002211', - "Sup;": '\U000022D1', - "Superset;": '\U00002283', - "SupersetEqual;": '\U00002287', - "Supset;": '\U000022D1', - "THORN;": '\U000000DE', - "TRADE;": '\U00002122', - "TSHcy;": '\U0000040B', - "TScy;": '\U00000426', - "Tab;": '\U00000009', - "Tau;": '\U000003A4', - "Tcaron;": '\U00000164', - "Tcedil;": '\U00000162', - "Tcy;": '\U00000422', - "Tfr;": '\U0001D517', - "Therefore;": '\U00002234', - "Theta;": '\U00000398', - "ThinSpace;": '\U00002009', - "Tilde;": '\U0000223C', - "TildeEqual;": '\U00002243', - "TildeFullEqual;": '\U00002245', - "TildeTilde;": '\U00002248', - "Topf;": '\U0001D54B', - "TripleDot;": '\U000020DB', - "Tscr;": '\U0001D4AF', - "Tstrok;": '\U00000166', - "Uacute;": '\U000000DA', - "Uarr;": '\U0000219F', - "Uarrocir;": '\U00002949', - "Ubrcy;": '\U0000040E', - "Ubreve;": '\U0000016C', - "Ucirc;": '\U000000DB', - "Ucy;": '\U00000423', - "Udblac;": '\U00000170', - "Ufr;": '\U0001D518', - "Ugrave;": '\U000000D9', - "Umacr;": '\U0000016A', - "UnderBar;": '\U0000005F', - "UnderBrace;": '\U000023DF', - "UnderBracket;": '\U000023B5', - "UnderParenthesis;": '\U000023DD', - "Union;": '\U000022C3', - "UnionPlus;": '\U0000228E', - "Uogon;": '\U00000172', - "Uopf;": '\U0001D54C', - "UpArrow;": '\U00002191', - "UpArrowBar;": '\U00002912', - "UpArrowDownArrow;": '\U000021C5', - "UpDownArrow;": '\U00002195', - "UpEquilibrium;": '\U0000296E', - "UpTee;": '\U000022A5', - "UpTeeArrow;": '\U000021A5', - "Uparrow;": '\U000021D1', - "Updownarrow;": '\U000021D5', - "UpperLeftArrow;": '\U00002196', - "UpperRightArrow;": '\U00002197', - "Upsi;": '\U000003D2', - "Upsilon;": '\U000003A5', - "Uring;": '\U0000016E', - "Uscr;": '\U0001D4B0', - "Utilde;": '\U00000168', - "Uuml;": '\U000000DC', - "VDash;": '\U000022AB', - "Vbar;": '\U00002AEB', - "Vcy;": '\U00000412', - "Vdash;": '\U000022A9', - "Vdashl;": '\U00002AE6', - "Vee;": '\U000022C1', - "Verbar;": '\U00002016', - "Vert;": '\U00002016', - "VerticalBar;": '\U00002223', - "VerticalLine;": '\U0000007C', - "VerticalSeparator;": '\U00002758', - "VerticalTilde;": '\U00002240', - "VeryThinSpace;": '\U0000200A', - "Vfr;": '\U0001D519', - "Vopf;": '\U0001D54D', - "Vscr;": '\U0001D4B1', - "Vvdash;": '\U000022AA', - "Wcirc;": '\U00000174', - "Wedge;": '\U000022C0', - "Wfr;": '\U0001D51A', - "Wopf;": '\U0001D54E', - "Wscr;": '\U0001D4B2', - "Xfr;": '\U0001D51B', - "Xi;": '\U0000039E', - "Xopf;": '\U0001D54F', - "Xscr;": '\U0001D4B3', - "YAcy;": '\U0000042F', - "YIcy;": '\U00000407', - "YUcy;": '\U0000042E', - "Yacute;": '\U000000DD', - "Ycirc;": '\U00000176', - "Ycy;": '\U0000042B', - "Yfr;": '\U0001D51C', - "Yopf;": '\U0001D550', - "Yscr;": '\U0001D4B4', - "Yuml;": '\U00000178', - "ZHcy;": '\U00000416', - "Zacute;": '\U00000179', - "Zcaron;": '\U0000017D', - "Zcy;": '\U00000417', - "Zdot;": '\U0000017B', - "ZeroWidthSpace;": '\U0000200B', - "Zeta;": '\U00000396', - "Zfr;": '\U00002128', - "Zopf;": '\U00002124', - "Zscr;": '\U0001D4B5', - "aacute;": '\U000000E1', - "abreve;": '\U00000103', - "ac;": '\U0000223E', - "acd;": '\U0000223F', - "acirc;": '\U000000E2', - "acute;": '\U000000B4', - "acy;": '\U00000430', - "aelig;": '\U000000E6', - "af;": '\U00002061', - "afr;": '\U0001D51E', - "agrave;": '\U000000E0', - "alefsym;": '\U00002135', - "aleph;": '\U00002135', - "alpha;": '\U000003B1', - "amacr;": '\U00000101', - "amalg;": '\U00002A3F', - "amp;": '\U00000026', - "and;": '\U00002227', - "andand;": '\U00002A55', - "andd;": '\U00002A5C', - "andslope;": '\U00002A58', - "andv;": '\U00002A5A', - "ang;": '\U00002220', - "ange;": '\U000029A4', - "angle;": '\U00002220', - "angmsd;": '\U00002221', - "angmsdaa;": '\U000029A8', - "angmsdab;": '\U000029A9', - "angmsdac;": '\U000029AA', - "angmsdad;": '\U000029AB', - "angmsdae;": '\U000029AC', - "angmsdaf;": '\U000029AD', - "angmsdag;": '\U000029AE', - "angmsdah;": '\U000029AF', - "angrt;": '\U0000221F', - "angrtvb;": '\U000022BE', - "angrtvbd;": '\U0000299D', - "angsph;": '\U00002222', - "angst;": '\U000000C5', - "angzarr;": '\U0000237C', - "aogon;": '\U00000105', - "aopf;": '\U0001D552', - "ap;": '\U00002248', - "apE;": '\U00002A70', - "apacir;": '\U00002A6F', - "ape;": '\U0000224A', - "apid;": '\U0000224B', - "apos;": '\U00000027', - "approx;": '\U00002248', - "approxeq;": '\U0000224A', - "aring;": '\U000000E5', - "ascr;": '\U0001D4B6', - "ast;": '\U0000002A', - "asymp;": '\U00002248', - "asympeq;": '\U0000224D', - "atilde;": '\U000000E3', - "auml;": '\U000000E4', - "awconint;": '\U00002233', - "awint;": '\U00002A11', - "bNot;": '\U00002AED', - "backcong;": '\U0000224C', - "backepsilon;": '\U000003F6', - "backprime;": '\U00002035', - "backsim;": '\U0000223D', - "backsimeq;": '\U000022CD', - "barvee;": '\U000022BD', - "barwed;": '\U00002305', - "barwedge;": '\U00002305', - "bbrk;": '\U000023B5', - "bbrktbrk;": '\U000023B6', - "bcong;": '\U0000224C', - "bcy;": '\U00000431', - "bdquo;": '\U0000201E', - "becaus;": '\U00002235', - "because;": '\U00002235', - "bemptyv;": '\U000029B0', - "bepsi;": '\U000003F6', - "bernou;": '\U0000212C', - "beta;": '\U000003B2', - "beth;": '\U00002136', - "between;": '\U0000226C', - "bfr;": '\U0001D51F', - "bigcap;": '\U000022C2', - "bigcirc;": '\U000025EF', - "bigcup;": '\U000022C3', - "bigodot;": '\U00002A00', - "bigoplus;": '\U00002A01', - "bigotimes;": '\U00002A02', - "bigsqcup;": '\U00002A06', - "bigstar;": '\U00002605', - "bigtriangledown;": '\U000025BD', - "bigtriangleup;": '\U000025B3', - "biguplus;": '\U00002A04', - "bigvee;": '\U000022C1', - "bigwedge;": '\U000022C0', - "bkarow;": '\U0000290D', - "blacklozenge;": '\U000029EB', - "blacksquare;": '\U000025AA', - "blacktriangle;": '\U000025B4', - "blacktriangledown;": '\U000025BE', - "blacktriangleleft;": '\U000025C2', - "blacktriangleright;": '\U000025B8', - "blank;": '\U00002423', - "blk12;": '\U00002592', - "blk14;": '\U00002591', - "blk34;": '\U00002593', - "block;": '\U00002588', - "bnot;": '\U00002310', - "bopf;": '\U0001D553', - "bot;": '\U000022A5', - "bottom;": '\U000022A5', - "bowtie;": '\U000022C8', - "boxDL;": '\U00002557', - "boxDR;": '\U00002554', - "boxDl;": '\U00002556', - "boxDr;": '\U00002553', - "boxH;": '\U00002550', - "boxHD;": '\U00002566', - "boxHU;": '\U00002569', - "boxHd;": '\U00002564', - "boxHu;": '\U00002567', - "boxUL;": '\U0000255D', - "boxUR;": '\U0000255A', - "boxUl;": '\U0000255C', - "boxUr;": '\U00002559', - "boxV;": '\U00002551', - "boxVH;": '\U0000256C', - "boxVL;": '\U00002563', - "boxVR;": '\U00002560', - "boxVh;": '\U0000256B', - "boxVl;": '\U00002562', - "boxVr;": '\U0000255F', - "boxbox;": '\U000029C9', - "boxdL;": '\U00002555', - "boxdR;": '\U00002552', - "boxdl;": '\U00002510', - "boxdr;": '\U0000250C', - "boxh;": '\U00002500', - "boxhD;": '\U00002565', - "boxhU;": '\U00002568', - "boxhd;": '\U0000252C', - "boxhu;": '\U00002534', - "boxminus;": '\U0000229F', - "boxplus;": '\U0000229E', - "boxtimes;": '\U000022A0', - "boxuL;": '\U0000255B', - "boxuR;": '\U00002558', - "boxul;": '\U00002518', - "boxur;": '\U00002514', - "boxv;": '\U00002502', - "boxvH;": '\U0000256A', - "boxvL;": '\U00002561', - "boxvR;": '\U0000255E', - "boxvh;": '\U0000253C', - "boxvl;": '\U00002524', - "boxvr;": '\U0000251C', - "bprime;": '\U00002035', - "breve;": '\U000002D8', - "brvbar;": '\U000000A6', - "bscr;": '\U0001D4B7', - "bsemi;": '\U0000204F', - "bsim;": '\U0000223D', - "bsime;": '\U000022CD', - "bsol;": '\U0000005C', - "bsolb;": '\U000029C5', - "bsolhsub;": '\U000027C8', - "bull;": '\U00002022', - "bullet;": '\U00002022', - "bump;": '\U0000224E', - "bumpE;": '\U00002AAE', - "bumpe;": '\U0000224F', - "bumpeq;": '\U0000224F', - "cacute;": '\U00000107', - "cap;": '\U00002229', - "capand;": '\U00002A44', - "capbrcup;": '\U00002A49', - "capcap;": '\U00002A4B', - "capcup;": '\U00002A47', - "capdot;": '\U00002A40', - "caret;": '\U00002041', - "caron;": '\U000002C7', - "ccaps;": '\U00002A4D', - "ccaron;": '\U0000010D', - "ccedil;": '\U000000E7', - "ccirc;": '\U00000109', - "ccups;": '\U00002A4C', - "ccupssm;": '\U00002A50', - "cdot;": '\U0000010B', - "cedil;": '\U000000B8', - "cemptyv;": '\U000029B2', - "cent;": '\U000000A2', - "centerdot;": '\U000000B7', - "cfr;": '\U0001D520', - "chcy;": '\U00000447', - "check;": '\U00002713', - "checkmark;": '\U00002713', - "chi;": '\U000003C7', - "cir;": '\U000025CB', - "cirE;": '\U000029C3', - "circ;": '\U000002C6', - "circeq;": '\U00002257', - "circlearrowleft;": '\U000021BA', - "circlearrowright;": '\U000021BB', - "circledR;": '\U000000AE', - "circledS;": '\U000024C8', - "circledast;": '\U0000229B', - "circledcirc;": '\U0000229A', - "circleddash;": '\U0000229D', - "cire;": '\U00002257', - "cirfnint;": '\U00002A10', - "cirmid;": '\U00002AEF', - "cirscir;": '\U000029C2', - "clubs;": '\U00002663', - "clubsuit;": '\U00002663', - "colon;": '\U0000003A', - "colone;": '\U00002254', - "coloneq;": '\U00002254', - "comma;": '\U0000002C', - "commat;": '\U00000040', - "comp;": '\U00002201', - "compfn;": '\U00002218', - "complement;": '\U00002201', - "complexes;": '\U00002102', - "cong;": '\U00002245', - "congdot;": '\U00002A6D', - "conint;": '\U0000222E', - "copf;": '\U0001D554', - "coprod;": '\U00002210', - "copy;": '\U000000A9', - "copysr;": '\U00002117', - "crarr;": '\U000021B5', - "cross;": '\U00002717', - "cscr;": '\U0001D4B8', - "csub;": '\U00002ACF', - "csube;": '\U00002AD1', - "csup;": '\U00002AD0', - "csupe;": '\U00002AD2', - "ctdot;": '\U000022EF', - "cudarrl;": '\U00002938', - "cudarrr;": '\U00002935', - "cuepr;": '\U000022DE', - "cuesc;": '\U000022DF', - "cularr;": '\U000021B6', - "cularrp;": '\U0000293D', - "cup;": '\U0000222A', - "cupbrcap;": '\U00002A48', - "cupcap;": '\U00002A46', - "cupcup;": '\U00002A4A', - "cupdot;": '\U0000228D', - "cupor;": '\U00002A45', - "curarr;": '\U000021B7', - "curarrm;": '\U0000293C', - "curlyeqprec;": '\U000022DE', - "curlyeqsucc;": '\U000022DF', - "curlyvee;": '\U000022CE', - "curlywedge;": '\U000022CF', - "curren;": '\U000000A4', - "curvearrowleft;": '\U000021B6', - "curvearrowright;": '\U000021B7', - "cuvee;": '\U000022CE', - "cuwed;": '\U000022CF', - "cwconint;": '\U00002232', - "cwint;": '\U00002231', - "cylcty;": '\U0000232D', - "dArr;": '\U000021D3', - "dHar;": '\U00002965', - "dagger;": '\U00002020', - "daleth;": '\U00002138', - "darr;": '\U00002193', - "dash;": '\U00002010', - "dashv;": '\U000022A3', - "dbkarow;": '\U0000290F', - "dblac;": '\U000002DD', - "dcaron;": '\U0000010F', - "dcy;": '\U00000434', - "dd;": '\U00002146', - "ddagger;": '\U00002021', - "ddarr;": '\U000021CA', - "ddotseq;": '\U00002A77', - "deg;": '\U000000B0', - "delta;": '\U000003B4', - "demptyv;": '\U000029B1', - "dfisht;": '\U0000297F', - "dfr;": '\U0001D521', - "dharl;": '\U000021C3', - "dharr;": '\U000021C2', - "diam;": '\U000022C4', - "diamond;": '\U000022C4', - "diamondsuit;": '\U00002666', - "diams;": '\U00002666', - "die;": '\U000000A8', - "digamma;": '\U000003DD', - "disin;": '\U000022F2', - "div;": '\U000000F7', - "divide;": '\U000000F7', - "divideontimes;": '\U000022C7', - "divonx;": '\U000022C7', - "djcy;": '\U00000452', - "dlcorn;": '\U0000231E', - "dlcrop;": '\U0000230D', - "dollar;": '\U00000024', - "dopf;": '\U0001D555', - "dot;": '\U000002D9', - "doteq;": '\U00002250', - "doteqdot;": '\U00002251', - "dotminus;": '\U00002238', - "dotplus;": '\U00002214', - "dotsquare;": '\U000022A1', - "doublebarwedge;": '\U00002306', - "downarrow;": '\U00002193', - "downdownarrows;": '\U000021CA', - "downharpoonleft;": '\U000021C3', - "downharpoonright;": '\U000021C2', - "drbkarow;": '\U00002910', - "drcorn;": '\U0000231F', - "drcrop;": '\U0000230C', - "dscr;": '\U0001D4B9', - "dscy;": '\U00000455', - "dsol;": '\U000029F6', - "dstrok;": '\U00000111', - "dtdot;": '\U000022F1', - "dtri;": '\U000025BF', - "dtrif;": '\U000025BE', - "duarr;": '\U000021F5', - "duhar;": '\U0000296F', - "dwangle;": '\U000029A6', - "dzcy;": '\U0000045F', - "dzigrarr;": '\U000027FF', - "eDDot;": '\U00002A77', - "eDot;": '\U00002251', - "eacute;": '\U000000E9', - "easter;": '\U00002A6E', - "ecaron;": '\U0000011B', - "ecir;": '\U00002256', - "ecirc;": '\U000000EA', - "ecolon;": '\U00002255', - "ecy;": '\U0000044D', - "edot;": '\U00000117', - "ee;": '\U00002147', - "efDot;": '\U00002252', - "efr;": '\U0001D522', - "eg;": '\U00002A9A', - "egrave;": '\U000000E8', - "egs;": '\U00002A96', - "egsdot;": '\U00002A98', - "el;": '\U00002A99', - "elinters;": '\U000023E7', - "ell;": '\U00002113', - "els;": '\U00002A95', - "elsdot;": '\U00002A97', - "emacr;": '\U00000113', - "empty;": '\U00002205', - "emptyset;": '\U00002205', - "emptyv;": '\U00002205', - "emsp;": '\U00002003', - "emsp13;": '\U00002004', - "emsp14;": '\U00002005', - "eng;": '\U0000014B', - "ensp;": '\U00002002', - "eogon;": '\U00000119', - "eopf;": '\U0001D556', - "epar;": '\U000022D5', - "eparsl;": '\U000029E3', - "eplus;": '\U00002A71', - "epsi;": '\U000003B5', - "epsilon;": '\U000003B5', - "epsiv;": '\U000003F5', - "eqcirc;": '\U00002256', - "eqcolon;": '\U00002255', - "eqsim;": '\U00002242', - "eqslantgtr;": '\U00002A96', - "eqslantless;": '\U00002A95', - "equals;": '\U0000003D', - "equest;": '\U0000225F', - "equiv;": '\U00002261', - "equivDD;": '\U00002A78', - "eqvparsl;": '\U000029E5', - "erDot;": '\U00002253', - "erarr;": '\U00002971', - "escr;": '\U0000212F', - "esdot;": '\U00002250', - "esim;": '\U00002242', - "eta;": '\U000003B7', - "eth;": '\U000000F0', - "euml;": '\U000000EB', - "euro;": '\U000020AC', - "excl;": '\U00000021', - "exist;": '\U00002203', - "expectation;": '\U00002130', - "exponentiale;": '\U00002147', - "fallingdotseq;": '\U00002252', - "fcy;": '\U00000444', - "female;": '\U00002640', - "ffilig;": '\U0000FB03', - "fflig;": '\U0000FB00', - "ffllig;": '\U0000FB04', - "ffr;": '\U0001D523', - "filig;": '\U0000FB01', - "flat;": '\U0000266D', - "fllig;": '\U0000FB02', - "fltns;": '\U000025B1', - "fnof;": '\U00000192', - "fopf;": '\U0001D557', - "forall;": '\U00002200', - "fork;": '\U000022D4', - "forkv;": '\U00002AD9', - "fpartint;": '\U00002A0D', - "frac12;": '\U000000BD', - "frac13;": '\U00002153', - "frac14;": '\U000000BC', - "frac15;": '\U00002155', - "frac16;": '\U00002159', - "frac18;": '\U0000215B', - "frac23;": '\U00002154', - "frac25;": '\U00002156', - "frac34;": '\U000000BE', - "frac35;": '\U00002157', - "frac38;": '\U0000215C', - "frac45;": '\U00002158', - "frac56;": '\U0000215A', - "frac58;": '\U0000215D', - "frac78;": '\U0000215E', - "frasl;": '\U00002044', - "frown;": '\U00002322', - "fscr;": '\U0001D4BB', - "gE;": '\U00002267', - "gEl;": '\U00002A8C', - "gacute;": '\U000001F5', - "gamma;": '\U000003B3', - "gammad;": '\U000003DD', - "gap;": '\U00002A86', - "gbreve;": '\U0000011F', - "gcirc;": '\U0000011D', - "gcy;": '\U00000433', - "gdot;": '\U00000121', - "ge;": '\U00002265', - "gel;": '\U000022DB', - "geq;": '\U00002265', - "geqq;": '\U00002267', - "geqslant;": '\U00002A7E', - "ges;": '\U00002A7E', - "gescc;": '\U00002AA9', - "gesdot;": '\U00002A80', - "gesdoto;": '\U00002A82', - "gesdotol;": '\U00002A84', - "gesles;": '\U00002A94', - "gfr;": '\U0001D524', - "gg;": '\U0000226B', - "ggg;": '\U000022D9', - "gimel;": '\U00002137', - "gjcy;": '\U00000453', - "gl;": '\U00002277', - "glE;": '\U00002A92', - "gla;": '\U00002AA5', - "glj;": '\U00002AA4', - "gnE;": '\U00002269', - "gnap;": '\U00002A8A', - "gnapprox;": '\U00002A8A', - "gne;": '\U00002A88', - "gneq;": '\U00002A88', - "gneqq;": '\U00002269', - "gnsim;": '\U000022E7', - "gopf;": '\U0001D558', - "grave;": '\U00000060', - "gscr;": '\U0000210A', - "gsim;": '\U00002273', - "gsime;": '\U00002A8E', - "gsiml;": '\U00002A90', - "gt;": '\U0000003E', - "gtcc;": '\U00002AA7', - "gtcir;": '\U00002A7A', - "gtdot;": '\U000022D7', - "gtlPar;": '\U00002995', - "gtquest;": '\U00002A7C', - "gtrapprox;": '\U00002A86', - "gtrarr;": '\U00002978', - "gtrdot;": '\U000022D7', - "gtreqless;": '\U000022DB', - "gtreqqless;": '\U00002A8C', - "gtrless;": '\U00002277', - "gtrsim;": '\U00002273', - "hArr;": '\U000021D4', - "hairsp;": '\U0000200A', - "half;": '\U000000BD', - "hamilt;": '\U0000210B', - "hardcy;": '\U0000044A', - "harr;": '\U00002194', - "harrcir;": '\U00002948', - "harrw;": '\U000021AD', - "hbar;": '\U0000210F', - "hcirc;": '\U00000125', - "hearts;": '\U00002665', - "heartsuit;": '\U00002665', - "hellip;": '\U00002026', - "hercon;": '\U000022B9', - "hfr;": '\U0001D525', - "hksearow;": '\U00002925', - "hkswarow;": '\U00002926', - "hoarr;": '\U000021FF', - "homtht;": '\U0000223B', - "hookleftarrow;": '\U000021A9', - "hookrightarrow;": '\U000021AA', - "hopf;": '\U0001D559', - "horbar;": '\U00002015', - "hscr;": '\U0001D4BD', - "hslash;": '\U0000210F', - "hstrok;": '\U00000127', - "hybull;": '\U00002043', - "hyphen;": '\U00002010', - "iacute;": '\U000000ED', - "ic;": '\U00002063', - "icirc;": '\U000000EE', - "icy;": '\U00000438', - "iecy;": '\U00000435', - "iexcl;": '\U000000A1', - "iff;": '\U000021D4', - "ifr;": '\U0001D526', - "igrave;": '\U000000EC', - "ii;": '\U00002148', - "iiiint;": '\U00002A0C', - "iiint;": '\U0000222D', - "iinfin;": '\U000029DC', - "iiota;": '\U00002129', - "ijlig;": '\U00000133', - "imacr;": '\U0000012B', - "image;": '\U00002111', - "imagline;": '\U00002110', - "imagpart;": '\U00002111', - "imath;": '\U00000131', - "imof;": '\U000022B7', - "imped;": '\U000001B5', - "in;": '\U00002208', - "incare;": '\U00002105', - "infin;": '\U0000221E', - "infintie;": '\U000029DD', - "inodot;": '\U00000131', - "int;": '\U0000222B', - "intcal;": '\U000022BA', - "integers;": '\U00002124', - "intercal;": '\U000022BA', - "intlarhk;": '\U00002A17', - "intprod;": '\U00002A3C', - "iocy;": '\U00000451', - "iogon;": '\U0000012F', - "iopf;": '\U0001D55A', - "iota;": '\U000003B9', - "iprod;": '\U00002A3C', - "iquest;": '\U000000BF', - "iscr;": '\U0001D4BE', - "isin;": '\U00002208', - "isinE;": '\U000022F9', - "isindot;": '\U000022F5', - "isins;": '\U000022F4', - "isinsv;": '\U000022F3', - "isinv;": '\U00002208', - "it;": '\U00002062', - "itilde;": '\U00000129', - "iukcy;": '\U00000456', - "iuml;": '\U000000EF', - "jcirc;": '\U00000135', - "jcy;": '\U00000439', - "jfr;": '\U0001D527', - "jmath;": '\U00000237', - "jopf;": '\U0001D55B', - "jscr;": '\U0001D4BF', - "jsercy;": '\U00000458', - "jukcy;": '\U00000454', - "kappa;": '\U000003BA', - "kappav;": '\U000003F0', - "kcedil;": '\U00000137', - "kcy;": '\U0000043A', - "kfr;": '\U0001D528', - "kgreen;": '\U00000138', - "khcy;": '\U00000445', - "kjcy;": '\U0000045C', - "kopf;": '\U0001D55C', - "kscr;": '\U0001D4C0', - "lAarr;": '\U000021DA', - "lArr;": '\U000021D0', - "lAtail;": '\U0000291B', - "lBarr;": '\U0000290E', - "lE;": '\U00002266', - "lEg;": '\U00002A8B', - "lHar;": '\U00002962', - "lacute;": '\U0000013A', - "laemptyv;": '\U000029B4', - "lagran;": '\U00002112', - "lambda;": '\U000003BB', - "lang;": '\U000027E8', - "langd;": '\U00002991', - "langle;": '\U000027E8', - "lap;": '\U00002A85', - "laquo;": '\U000000AB', - "larr;": '\U00002190', - "larrb;": '\U000021E4', - "larrbfs;": '\U0000291F', - "larrfs;": '\U0000291D', - "larrhk;": '\U000021A9', - "larrlp;": '\U000021AB', - "larrpl;": '\U00002939', - "larrsim;": '\U00002973', - "larrtl;": '\U000021A2', - "lat;": '\U00002AAB', - "latail;": '\U00002919', - "late;": '\U00002AAD', - "lbarr;": '\U0000290C', - "lbbrk;": '\U00002772', - "lbrace;": '\U0000007B', - "lbrack;": '\U0000005B', - "lbrke;": '\U0000298B', - "lbrksld;": '\U0000298F', - "lbrkslu;": '\U0000298D', - "lcaron;": '\U0000013E', - "lcedil;": '\U0000013C', - "lceil;": '\U00002308', - "lcub;": '\U0000007B', - "lcy;": '\U0000043B', - "ldca;": '\U00002936', - "ldquo;": '\U0000201C', - "ldquor;": '\U0000201E', - "ldrdhar;": '\U00002967', - "ldrushar;": '\U0000294B', - "ldsh;": '\U000021B2', - "le;": '\U00002264', - "leftarrow;": '\U00002190', - "leftarrowtail;": '\U000021A2', - "leftharpoondown;": '\U000021BD', - "leftharpoonup;": '\U000021BC', - "leftleftarrows;": '\U000021C7', - "leftrightarrow;": '\U00002194', - "leftrightarrows;": '\U000021C6', - "leftrightharpoons;": '\U000021CB', - "leftrightsquigarrow;": '\U000021AD', - "leftthreetimes;": '\U000022CB', - "leg;": '\U000022DA', - "leq;": '\U00002264', - "leqq;": '\U00002266', - "leqslant;": '\U00002A7D', - "les;": '\U00002A7D', - "lescc;": '\U00002AA8', - "lesdot;": '\U00002A7F', - "lesdoto;": '\U00002A81', - "lesdotor;": '\U00002A83', - "lesges;": '\U00002A93', - "lessapprox;": '\U00002A85', - "lessdot;": '\U000022D6', - "lesseqgtr;": '\U000022DA', - "lesseqqgtr;": '\U00002A8B', - "lessgtr;": '\U00002276', - "lesssim;": '\U00002272', - "lfisht;": '\U0000297C', - "lfloor;": '\U0000230A', - "lfr;": '\U0001D529', - "lg;": '\U00002276', - "lgE;": '\U00002A91', - "lhard;": '\U000021BD', - "lharu;": '\U000021BC', - "lharul;": '\U0000296A', - "lhblk;": '\U00002584', - "ljcy;": '\U00000459', - "ll;": '\U0000226A', - "llarr;": '\U000021C7', - "llcorner;": '\U0000231E', - "llhard;": '\U0000296B', - "lltri;": '\U000025FA', - "lmidot;": '\U00000140', - "lmoust;": '\U000023B0', - "lmoustache;": '\U000023B0', - "lnE;": '\U00002268', - "lnap;": '\U00002A89', - "lnapprox;": '\U00002A89', - "lne;": '\U00002A87', - "lneq;": '\U00002A87', - "lneqq;": '\U00002268', - "lnsim;": '\U000022E6', - "loang;": '\U000027EC', - "loarr;": '\U000021FD', - "lobrk;": '\U000027E6', - "longleftarrow;": '\U000027F5', - "longleftrightarrow;": '\U000027F7', - "longmapsto;": '\U000027FC', - "longrightarrow;": '\U000027F6', - "looparrowleft;": '\U000021AB', - "looparrowright;": '\U000021AC', - "lopar;": '\U00002985', - "lopf;": '\U0001D55D', - "loplus;": '\U00002A2D', - "lotimes;": '\U00002A34', - "lowast;": '\U00002217', - "lowbar;": '\U0000005F', - "loz;": '\U000025CA', - "lozenge;": '\U000025CA', - "lozf;": '\U000029EB', - "lpar;": '\U00000028', - "lparlt;": '\U00002993', - "lrarr;": '\U000021C6', - "lrcorner;": '\U0000231F', - "lrhar;": '\U000021CB', - "lrhard;": '\U0000296D', - "lrm;": '\U0000200E', - "lrtri;": '\U000022BF', - "lsaquo;": '\U00002039', - "lscr;": '\U0001D4C1', - "lsh;": '\U000021B0', - "lsim;": '\U00002272', - "lsime;": '\U00002A8D', - "lsimg;": '\U00002A8F', - "lsqb;": '\U0000005B', - "lsquo;": '\U00002018', - "lsquor;": '\U0000201A', - "lstrok;": '\U00000142', - "lt;": '\U0000003C', - "ltcc;": '\U00002AA6', - "ltcir;": '\U00002A79', - "ltdot;": '\U000022D6', - "lthree;": '\U000022CB', - "ltimes;": '\U000022C9', - "ltlarr;": '\U00002976', - "ltquest;": '\U00002A7B', - "ltrPar;": '\U00002996', - "ltri;": '\U000025C3', - "ltrie;": '\U000022B4', - "ltrif;": '\U000025C2', - "lurdshar;": '\U0000294A', - "luruhar;": '\U00002966', - "mDDot;": '\U0000223A', - "macr;": '\U000000AF', - "male;": '\U00002642', - "malt;": '\U00002720', - "maltese;": '\U00002720', - "map;": '\U000021A6', - "mapsto;": '\U000021A6', - "mapstodown;": '\U000021A7', - "mapstoleft;": '\U000021A4', - "mapstoup;": '\U000021A5', - "marker;": '\U000025AE', - "mcomma;": '\U00002A29', - "mcy;": '\U0000043C', - "mdash;": '\U00002014', - "measuredangle;": '\U00002221', - "mfr;": '\U0001D52A', - "mho;": '\U00002127', - "micro;": '\U000000B5', - "mid;": '\U00002223', - "midast;": '\U0000002A', - "midcir;": '\U00002AF0', - "middot;": '\U000000B7', - "minus;": '\U00002212', - "minusb;": '\U0000229F', - "minusd;": '\U00002238', - "minusdu;": '\U00002A2A', - "mlcp;": '\U00002ADB', - "mldr;": '\U00002026', - "mnplus;": '\U00002213', - "models;": '\U000022A7', - "mopf;": '\U0001D55E', - "mp;": '\U00002213', - "mscr;": '\U0001D4C2', - "mstpos;": '\U0000223E', - "mu;": '\U000003BC', - "multimap;": '\U000022B8', - "mumap;": '\U000022B8', - "nLeftarrow;": '\U000021CD', - "nLeftrightarrow;": '\U000021CE', - "nRightarrow;": '\U000021CF', - "nVDash;": '\U000022AF', - "nVdash;": '\U000022AE', - "nabla;": '\U00002207', - "nacute;": '\U00000144', - "nap;": '\U00002249', - "napos;": '\U00000149', - "napprox;": '\U00002249', - "natur;": '\U0000266E', - "natural;": '\U0000266E', - "naturals;": '\U00002115', - "nbsp;": '\U000000A0', - "ncap;": '\U00002A43', - "ncaron;": '\U00000148', - "ncedil;": '\U00000146', - "ncong;": '\U00002247', - "ncup;": '\U00002A42', - "ncy;": '\U0000043D', - "ndash;": '\U00002013', - "ne;": '\U00002260', - "neArr;": '\U000021D7', - "nearhk;": '\U00002924', - "nearr;": '\U00002197', - "nearrow;": '\U00002197', - "nequiv;": '\U00002262', - "nesear;": '\U00002928', - "nexist;": '\U00002204', - "nexists;": '\U00002204', - "nfr;": '\U0001D52B', - "nge;": '\U00002271', - "ngeq;": '\U00002271', - "ngsim;": '\U00002275', - "ngt;": '\U0000226F', - "ngtr;": '\U0000226F', - "nhArr;": '\U000021CE', - "nharr;": '\U000021AE', - "nhpar;": '\U00002AF2', - "ni;": '\U0000220B', - "nis;": '\U000022FC', - "nisd;": '\U000022FA', - "niv;": '\U0000220B', - "njcy;": '\U0000045A', - "nlArr;": '\U000021CD', - "nlarr;": '\U0000219A', - "nldr;": '\U00002025', - "nle;": '\U00002270', - "nleftarrow;": '\U0000219A', - "nleftrightarrow;": '\U000021AE', - "nleq;": '\U00002270', - "nless;": '\U0000226E', - "nlsim;": '\U00002274', - "nlt;": '\U0000226E', - "nltri;": '\U000022EA', - "nltrie;": '\U000022EC', - "nmid;": '\U00002224', - "nopf;": '\U0001D55F', - "not;": '\U000000AC', - "notin;": '\U00002209', - "notinva;": '\U00002209', - "notinvb;": '\U000022F7', - "notinvc;": '\U000022F6', - "notni;": '\U0000220C', - "notniva;": '\U0000220C', - "notnivb;": '\U000022FE', - "notnivc;": '\U000022FD', - "npar;": '\U00002226', - "nparallel;": '\U00002226', - "npolint;": '\U00002A14', - "npr;": '\U00002280', - "nprcue;": '\U000022E0', - "nprec;": '\U00002280', - "nrArr;": '\U000021CF', - "nrarr;": '\U0000219B', - "nrightarrow;": '\U0000219B', - "nrtri;": '\U000022EB', - "nrtrie;": '\U000022ED', - "nsc;": '\U00002281', - "nsccue;": '\U000022E1', - "nscr;": '\U0001D4C3', - "nshortmid;": '\U00002224', - "nshortparallel;": '\U00002226', - "nsim;": '\U00002241', - "nsime;": '\U00002244', - "nsimeq;": '\U00002244', - "nsmid;": '\U00002224', - "nspar;": '\U00002226', - "nsqsube;": '\U000022E2', - "nsqsupe;": '\U000022E3', - "nsub;": '\U00002284', - "nsube;": '\U00002288', - "nsubseteq;": '\U00002288', - "nsucc;": '\U00002281', - "nsup;": '\U00002285', - "nsupe;": '\U00002289', - "nsupseteq;": '\U00002289', - "ntgl;": '\U00002279', - "ntilde;": '\U000000F1', - "ntlg;": '\U00002278', - "ntriangleleft;": '\U000022EA', - "ntrianglelefteq;": '\U000022EC', - "ntriangleright;": '\U000022EB', - "ntrianglerighteq;": '\U000022ED', - "nu;": '\U000003BD', - "num;": '\U00000023', - "numero;": '\U00002116', - "numsp;": '\U00002007', - "nvDash;": '\U000022AD', - "nvHarr;": '\U00002904', - "nvdash;": '\U000022AC', - "nvinfin;": '\U000029DE', - "nvlArr;": '\U00002902', - "nvrArr;": '\U00002903', - "nwArr;": '\U000021D6', - "nwarhk;": '\U00002923', - "nwarr;": '\U00002196', - "nwarrow;": '\U00002196', - "nwnear;": '\U00002927', - "oS;": '\U000024C8', - "oacute;": '\U000000F3', - "oast;": '\U0000229B', - "ocir;": '\U0000229A', - "ocirc;": '\U000000F4', - "ocy;": '\U0000043E', - "odash;": '\U0000229D', - "odblac;": '\U00000151', - "odiv;": '\U00002A38', - "odot;": '\U00002299', - "odsold;": '\U000029BC', - "oelig;": '\U00000153', - "ofcir;": '\U000029BF', - "ofr;": '\U0001D52C', - "ogon;": '\U000002DB', - "ograve;": '\U000000F2', - "ogt;": '\U000029C1', - "ohbar;": '\U000029B5', - "ohm;": '\U000003A9', - "oint;": '\U0000222E', - "olarr;": '\U000021BA', - "olcir;": '\U000029BE', - "olcross;": '\U000029BB', - "oline;": '\U0000203E', - "olt;": '\U000029C0', - "omacr;": '\U0000014D', - "omega;": '\U000003C9', - "omicron;": '\U000003BF', - "omid;": '\U000029B6', - "ominus;": '\U00002296', - "oopf;": '\U0001D560', - "opar;": '\U000029B7', - "operp;": '\U000029B9', - "oplus;": '\U00002295', - "or;": '\U00002228', - "orarr;": '\U000021BB', - "ord;": '\U00002A5D', - "order;": '\U00002134', - "orderof;": '\U00002134', - "ordf;": '\U000000AA', - "ordm;": '\U000000BA', - "origof;": '\U000022B6', - "oror;": '\U00002A56', - "orslope;": '\U00002A57', - "orv;": '\U00002A5B', - "oscr;": '\U00002134', - "oslash;": '\U000000F8', - "osol;": '\U00002298', - "otilde;": '\U000000F5', - "otimes;": '\U00002297', - "otimesas;": '\U00002A36', - "ouml;": '\U000000F6', - "ovbar;": '\U0000233D', - "par;": '\U00002225', - "para;": '\U000000B6', - "parallel;": '\U00002225', - "parsim;": '\U00002AF3', - "parsl;": '\U00002AFD', - "part;": '\U00002202', - "pcy;": '\U0000043F', - "percnt;": '\U00000025', - "period;": '\U0000002E', - "permil;": '\U00002030', - "perp;": '\U000022A5', - "pertenk;": '\U00002031', - "pfr;": '\U0001D52D', - "phi;": '\U000003C6', - "phiv;": '\U000003D5', - "phmmat;": '\U00002133', - "phone;": '\U0000260E', - "pi;": '\U000003C0', - "pitchfork;": '\U000022D4', - "piv;": '\U000003D6', - "planck;": '\U0000210F', - "planckh;": '\U0000210E', - "plankv;": '\U0000210F', - "plus;": '\U0000002B', - "plusacir;": '\U00002A23', - "plusb;": '\U0000229E', - "pluscir;": '\U00002A22', - "plusdo;": '\U00002214', - "plusdu;": '\U00002A25', - "pluse;": '\U00002A72', - "plusmn;": '\U000000B1', - "plussim;": '\U00002A26', - "plustwo;": '\U00002A27', - "pm;": '\U000000B1', - "pointint;": '\U00002A15', - "popf;": '\U0001D561', - "pound;": '\U000000A3', - "pr;": '\U0000227A', - "prE;": '\U00002AB3', - "prap;": '\U00002AB7', - "prcue;": '\U0000227C', - "pre;": '\U00002AAF', - "prec;": '\U0000227A', - "precapprox;": '\U00002AB7', - "preccurlyeq;": '\U0000227C', - "preceq;": '\U00002AAF', - "precnapprox;": '\U00002AB9', - "precneqq;": '\U00002AB5', - "precnsim;": '\U000022E8', - "precsim;": '\U0000227E', - "prime;": '\U00002032', - "primes;": '\U00002119', - "prnE;": '\U00002AB5', - "prnap;": '\U00002AB9', - "prnsim;": '\U000022E8', - "prod;": '\U0000220F', - "profalar;": '\U0000232E', - "profline;": '\U00002312', - "profsurf;": '\U00002313', - "prop;": '\U0000221D', - "propto;": '\U0000221D', - "prsim;": '\U0000227E', - "prurel;": '\U000022B0', - "pscr;": '\U0001D4C5', - "psi;": '\U000003C8', - "puncsp;": '\U00002008', - "qfr;": '\U0001D52E', - "qint;": '\U00002A0C', - "qopf;": '\U0001D562', - "qprime;": '\U00002057', - "qscr;": '\U0001D4C6', - "quaternions;": '\U0000210D', - "quatint;": '\U00002A16', - "quest;": '\U0000003F', - "questeq;": '\U0000225F', - "quot;": '\U00000022', - "rAarr;": '\U000021DB', - "rArr;": '\U000021D2', - "rAtail;": '\U0000291C', - "rBarr;": '\U0000290F', - "rHar;": '\U00002964', - "racute;": '\U00000155', - "radic;": '\U0000221A', - "raemptyv;": '\U000029B3', - "rang;": '\U000027E9', - "rangd;": '\U00002992', - "range;": '\U000029A5', - "rangle;": '\U000027E9', - "raquo;": '\U000000BB', - "rarr;": '\U00002192', - "rarrap;": '\U00002975', - "rarrb;": '\U000021E5', - "rarrbfs;": '\U00002920', - "rarrc;": '\U00002933', - "rarrfs;": '\U0000291E', - "rarrhk;": '\U000021AA', - "rarrlp;": '\U000021AC', - "rarrpl;": '\U00002945', - "rarrsim;": '\U00002974', - "rarrtl;": '\U000021A3', - "rarrw;": '\U0000219D', - "ratail;": '\U0000291A', - "ratio;": '\U00002236', - "rationals;": '\U0000211A', - "rbarr;": '\U0000290D', - "rbbrk;": '\U00002773', - "rbrace;": '\U0000007D', - "rbrack;": '\U0000005D', - "rbrke;": '\U0000298C', - "rbrksld;": '\U0000298E', - "rbrkslu;": '\U00002990', - "rcaron;": '\U00000159', - "rcedil;": '\U00000157', - "rceil;": '\U00002309', - "rcub;": '\U0000007D', - "rcy;": '\U00000440', - "rdca;": '\U00002937', - "rdldhar;": '\U00002969', - "rdquo;": '\U0000201D', - "rdquor;": '\U0000201D', - "rdsh;": '\U000021B3', - "real;": '\U0000211C', - "realine;": '\U0000211B', - "realpart;": '\U0000211C', - "reals;": '\U0000211D', - "rect;": '\U000025AD', - "reg;": '\U000000AE', - "rfisht;": '\U0000297D', - "rfloor;": '\U0000230B', - "rfr;": '\U0001D52F', - "rhard;": '\U000021C1', - "rharu;": '\U000021C0', - "rharul;": '\U0000296C', - "rho;": '\U000003C1', - "rhov;": '\U000003F1', - "rightarrow;": '\U00002192', - "rightarrowtail;": '\U000021A3', - "rightharpoondown;": '\U000021C1', - "rightharpoonup;": '\U000021C0', - "rightleftarrows;": '\U000021C4', - "rightleftharpoons;": '\U000021CC', - "rightrightarrows;": '\U000021C9', - "rightsquigarrow;": '\U0000219D', - "rightthreetimes;": '\U000022CC', - "ring;": '\U000002DA', - "risingdotseq;": '\U00002253', - "rlarr;": '\U000021C4', - "rlhar;": '\U000021CC', - "rlm;": '\U0000200F', - "rmoust;": '\U000023B1', - "rmoustache;": '\U000023B1', - "rnmid;": '\U00002AEE', - "roang;": '\U000027ED', - "roarr;": '\U000021FE', - "robrk;": '\U000027E7', - "ropar;": '\U00002986', - "ropf;": '\U0001D563', - "roplus;": '\U00002A2E', - "rotimes;": '\U00002A35', - "rpar;": '\U00000029', - "rpargt;": '\U00002994', - "rppolint;": '\U00002A12', - "rrarr;": '\U000021C9', - "rsaquo;": '\U0000203A', - "rscr;": '\U0001D4C7', - "rsh;": '\U000021B1', - "rsqb;": '\U0000005D', - "rsquo;": '\U00002019', - "rsquor;": '\U00002019', - "rthree;": '\U000022CC', - "rtimes;": '\U000022CA', - "rtri;": '\U000025B9', - "rtrie;": '\U000022B5', - "rtrif;": '\U000025B8', - "rtriltri;": '\U000029CE', - "ruluhar;": '\U00002968', - "rx;": '\U0000211E', - "sacute;": '\U0000015B', - "sbquo;": '\U0000201A', - "sc;": '\U0000227B', - "scE;": '\U00002AB4', - "scap;": '\U00002AB8', - "scaron;": '\U00000161', - "sccue;": '\U0000227D', - "sce;": '\U00002AB0', - "scedil;": '\U0000015F', - "scirc;": '\U0000015D', - "scnE;": '\U00002AB6', - "scnap;": '\U00002ABA', - "scnsim;": '\U000022E9', - "scpolint;": '\U00002A13', - "scsim;": '\U0000227F', - "scy;": '\U00000441', - "sdot;": '\U000022C5', - "sdotb;": '\U000022A1', - "sdote;": '\U00002A66', - "seArr;": '\U000021D8', - "searhk;": '\U00002925', - "searr;": '\U00002198', - "searrow;": '\U00002198', - "sect;": '\U000000A7', - "semi;": '\U0000003B', - "seswar;": '\U00002929', - "setminus;": '\U00002216', - "setmn;": '\U00002216', - "sext;": '\U00002736', - "sfr;": '\U0001D530', - "sfrown;": '\U00002322', - "sharp;": '\U0000266F', - "shchcy;": '\U00000449', - "shcy;": '\U00000448', - "shortmid;": '\U00002223', - "shortparallel;": '\U00002225', - "shy;": '\U000000AD', - "sigma;": '\U000003C3', - "sigmaf;": '\U000003C2', - "sigmav;": '\U000003C2', - "sim;": '\U0000223C', - "simdot;": '\U00002A6A', - "sime;": '\U00002243', - "simeq;": '\U00002243', - "simg;": '\U00002A9E', - "simgE;": '\U00002AA0', - "siml;": '\U00002A9D', - "simlE;": '\U00002A9F', - "simne;": '\U00002246', - "simplus;": '\U00002A24', - "simrarr;": '\U00002972', - "slarr;": '\U00002190', - "smallsetminus;": '\U00002216', - "smashp;": '\U00002A33', - "smeparsl;": '\U000029E4', - "smid;": '\U00002223', - "smile;": '\U00002323', - "smt;": '\U00002AAA', - "smte;": '\U00002AAC', - "softcy;": '\U0000044C', - "sol;": '\U0000002F', - "solb;": '\U000029C4', - "solbar;": '\U0000233F', - "sopf;": '\U0001D564', - "spades;": '\U00002660', - "spadesuit;": '\U00002660', - "spar;": '\U00002225', - "sqcap;": '\U00002293', - "sqcup;": '\U00002294', - "sqsub;": '\U0000228F', - "sqsube;": '\U00002291', - "sqsubset;": '\U0000228F', - "sqsubseteq;": '\U00002291', - "sqsup;": '\U00002290', - "sqsupe;": '\U00002292', - "sqsupset;": '\U00002290', - "sqsupseteq;": '\U00002292', - "squ;": '\U000025A1', - "square;": '\U000025A1', - "squarf;": '\U000025AA', - "squf;": '\U000025AA', - "srarr;": '\U00002192', - "sscr;": '\U0001D4C8', - "ssetmn;": '\U00002216', - "ssmile;": '\U00002323', - "sstarf;": '\U000022C6', - "star;": '\U00002606', - "starf;": '\U00002605', - "straightepsilon;": '\U000003F5', - "straightphi;": '\U000003D5', - "strns;": '\U000000AF', - "sub;": '\U00002282', - "subE;": '\U00002AC5', - "subdot;": '\U00002ABD', - "sube;": '\U00002286', - "subedot;": '\U00002AC3', - "submult;": '\U00002AC1', - "subnE;": '\U00002ACB', - "subne;": '\U0000228A', - "subplus;": '\U00002ABF', - "subrarr;": '\U00002979', - "subset;": '\U00002282', - "subseteq;": '\U00002286', - "subseteqq;": '\U00002AC5', - "subsetneq;": '\U0000228A', - "subsetneqq;": '\U00002ACB', - "subsim;": '\U00002AC7', - "subsub;": '\U00002AD5', - "subsup;": '\U00002AD3', - "succ;": '\U0000227B', - "succapprox;": '\U00002AB8', - "succcurlyeq;": '\U0000227D', - "succeq;": '\U00002AB0', - "succnapprox;": '\U00002ABA', - "succneqq;": '\U00002AB6', - "succnsim;": '\U000022E9', - "succsim;": '\U0000227F', - "sum;": '\U00002211', - "sung;": '\U0000266A', - "sup;": '\U00002283', - "sup1;": '\U000000B9', - "sup2;": '\U000000B2', - "sup3;": '\U000000B3', - "supE;": '\U00002AC6', - "supdot;": '\U00002ABE', - "supdsub;": '\U00002AD8', - "supe;": '\U00002287', - "supedot;": '\U00002AC4', - "suphsol;": '\U000027C9', - "suphsub;": '\U00002AD7', - "suplarr;": '\U0000297B', - "supmult;": '\U00002AC2', - "supnE;": '\U00002ACC', - "supne;": '\U0000228B', - "supplus;": '\U00002AC0', - "supset;": '\U00002283', - "supseteq;": '\U00002287', - "supseteqq;": '\U00002AC6', - "supsetneq;": '\U0000228B', - "supsetneqq;": '\U00002ACC', - "supsim;": '\U00002AC8', - "supsub;": '\U00002AD4', - "supsup;": '\U00002AD6', - "swArr;": '\U000021D9', - "swarhk;": '\U00002926', - "swarr;": '\U00002199', - "swarrow;": '\U00002199', - "swnwar;": '\U0000292A', - "szlig;": '\U000000DF', - "target;": '\U00002316', - "tau;": '\U000003C4', - "tbrk;": '\U000023B4', - "tcaron;": '\U00000165', - "tcedil;": '\U00000163', - "tcy;": '\U00000442', - "tdot;": '\U000020DB', - "telrec;": '\U00002315', - "tfr;": '\U0001D531', - "there4;": '\U00002234', - "therefore;": '\U00002234', - "theta;": '\U000003B8', - "thetasym;": '\U000003D1', - "thetav;": '\U000003D1', - "thickapprox;": '\U00002248', - "thicksim;": '\U0000223C', - "thinsp;": '\U00002009', - "thkap;": '\U00002248', - "thksim;": '\U0000223C', - "thorn;": '\U000000FE', - "tilde;": '\U000002DC', - "times;": '\U000000D7', - "timesb;": '\U000022A0', - "timesbar;": '\U00002A31', - "timesd;": '\U00002A30', - "tint;": '\U0000222D', - "toea;": '\U00002928', - "top;": '\U000022A4', - "topbot;": '\U00002336', - "topcir;": '\U00002AF1', - "topf;": '\U0001D565', - "topfork;": '\U00002ADA', - "tosa;": '\U00002929', - "tprime;": '\U00002034', - "trade;": '\U00002122', - "triangle;": '\U000025B5', - "triangledown;": '\U000025BF', - "triangleleft;": '\U000025C3', - "trianglelefteq;": '\U000022B4', - "triangleq;": '\U0000225C', - "triangleright;": '\U000025B9', - "trianglerighteq;": '\U000022B5', - "tridot;": '\U000025EC', - "trie;": '\U0000225C', - "triminus;": '\U00002A3A', - "triplus;": '\U00002A39', - "trisb;": '\U000029CD', - "tritime;": '\U00002A3B', - "trpezium;": '\U000023E2', - "tscr;": '\U0001D4C9', - "tscy;": '\U00000446', - "tshcy;": '\U0000045B', - "tstrok;": '\U00000167', - "twixt;": '\U0000226C', - "twoheadleftarrow;": '\U0000219E', - "twoheadrightarrow;": '\U000021A0', - "uArr;": '\U000021D1', - "uHar;": '\U00002963', - "uacute;": '\U000000FA', - "uarr;": '\U00002191', - "ubrcy;": '\U0000045E', - "ubreve;": '\U0000016D', - "ucirc;": '\U000000FB', - "ucy;": '\U00000443', - "udarr;": '\U000021C5', - "udblac;": '\U00000171', - "udhar;": '\U0000296E', - "ufisht;": '\U0000297E', - "ufr;": '\U0001D532', - "ugrave;": '\U000000F9', - "uharl;": '\U000021BF', - "uharr;": '\U000021BE', - "uhblk;": '\U00002580', - "ulcorn;": '\U0000231C', - "ulcorner;": '\U0000231C', - "ulcrop;": '\U0000230F', - "ultri;": '\U000025F8', - "umacr;": '\U0000016B', - "uml;": '\U000000A8', - "uogon;": '\U00000173', - "uopf;": '\U0001D566', - "uparrow;": '\U00002191', - "updownarrow;": '\U00002195', - "upharpoonleft;": '\U000021BF', - "upharpoonright;": '\U000021BE', - "uplus;": '\U0000228E', - "upsi;": '\U000003C5', - "upsih;": '\U000003D2', - "upsilon;": '\U000003C5', - "upuparrows;": '\U000021C8', - "urcorn;": '\U0000231D', - "urcorner;": '\U0000231D', - "urcrop;": '\U0000230E', - "uring;": '\U0000016F', - "urtri;": '\U000025F9', - "uscr;": '\U0001D4CA', - "utdot;": '\U000022F0', - "utilde;": '\U00000169', - "utri;": '\U000025B5', - "utrif;": '\U000025B4', - "uuarr;": '\U000021C8', - "uuml;": '\U000000FC', - "uwangle;": '\U000029A7', - "vArr;": '\U000021D5', - "vBar;": '\U00002AE8', - "vBarv;": '\U00002AE9', - "vDash;": '\U000022A8', - "vangrt;": '\U0000299C', - "varepsilon;": '\U000003F5', - "varkappa;": '\U000003F0', - "varnothing;": '\U00002205', - "varphi;": '\U000003D5', - "varpi;": '\U000003D6', - "varpropto;": '\U0000221D', - "varr;": '\U00002195', - "varrho;": '\U000003F1', - "varsigma;": '\U000003C2', - "vartheta;": '\U000003D1', - "vartriangleleft;": '\U000022B2', - "vartriangleright;": '\U000022B3', - "vcy;": '\U00000432', - "vdash;": '\U000022A2', - "vee;": '\U00002228', - "veebar;": '\U000022BB', - "veeeq;": '\U0000225A', - "vellip;": '\U000022EE', - "verbar;": '\U0000007C', - "vert;": '\U0000007C', - "vfr;": '\U0001D533', - "vltri;": '\U000022B2', - "vopf;": '\U0001D567', - "vprop;": '\U0000221D', - "vrtri;": '\U000022B3', - "vscr;": '\U0001D4CB', - "vzigzag;": '\U0000299A', - "wcirc;": '\U00000175', - "wedbar;": '\U00002A5F', - "wedge;": '\U00002227', - "wedgeq;": '\U00002259', - "weierp;": '\U00002118', - "wfr;": '\U0001D534', - "wopf;": '\U0001D568', - "wp;": '\U00002118', - "wr;": '\U00002240', - "wreath;": '\U00002240', - "wscr;": '\U0001D4CC', - "xcap;": '\U000022C2', - "xcirc;": '\U000025EF', - "xcup;": '\U000022C3', - "xdtri;": '\U000025BD', - "xfr;": '\U0001D535', - "xhArr;": '\U000027FA', - "xharr;": '\U000027F7', - "xi;": '\U000003BE', - "xlArr;": '\U000027F8', - "xlarr;": '\U000027F5', - "xmap;": '\U000027FC', - "xnis;": '\U000022FB', - "xodot;": '\U00002A00', - "xopf;": '\U0001D569', - "xoplus;": '\U00002A01', - "xotime;": '\U00002A02', - "xrArr;": '\U000027F9', - "xrarr;": '\U000027F6', - "xscr;": '\U0001D4CD', - "xsqcup;": '\U00002A06', - "xuplus;": '\U00002A04', - "xutri;": '\U000025B3', - "xvee;": '\U000022C1', - "xwedge;": '\U000022C0', - "yacute;": '\U000000FD', - "yacy;": '\U0000044F', - "ycirc;": '\U00000177', - "ycy;": '\U0000044B', - "yen;": '\U000000A5', - "yfr;": '\U0001D536', - "yicy;": '\U00000457', - "yopf;": '\U0001D56A', - "yscr;": '\U0001D4CE', - "yucy;": '\U0000044E', - "yuml;": '\U000000FF', - "zacute;": '\U0000017A', - "zcaron;": '\U0000017E', - "zcy;": '\U00000437', - "zdot;": '\U0000017C', - "zeetrf;": '\U00002128', - "zeta;": '\U000003B6', - "zfr;": '\U0001D537', - "zhcy;": '\U00000436', - "zigrarr;": '\U000021DD', - "zopf;": '\U0001D56B', - "zscr;": '\U0001D4CF', - "zwj;": '\U0000200D', - "zwnj;": '\U0000200C', - "AElig": '\U000000C6', - "AMP": '\U00000026', - "Aacute": '\U000000C1', - "Acirc": '\U000000C2', - "Agrave": '\U000000C0', - "Aring": '\U000000C5', - "Atilde": '\U000000C3', - "Auml": '\U000000C4', - "COPY": '\U000000A9', - "Ccedil": '\U000000C7', - "ETH": '\U000000D0', - "Eacute": '\U000000C9', - "Ecirc": '\U000000CA', - "Egrave": '\U000000C8', - "Euml": '\U000000CB', - "GT": '\U0000003E', - "Iacute": '\U000000CD', - "Icirc": '\U000000CE', - "Igrave": '\U000000CC', - "Iuml": '\U000000CF', - "LT": '\U0000003C', - "Ntilde": '\U000000D1', - "Oacute": '\U000000D3', - "Ocirc": '\U000000D4', - "Ograve": '\U000000D2', - "Oslash": '\U000000D8', - "Otilde": '\U000000D5', - "Ouml": '\U000000D6', - "QUOT": '\U00000022', - "REG": '\U000000AE', - "THORN": '\U000000DE', - "Uacute": '\U000000DA', - "Ucirc": '\U000000DB', - "Ugrave": '\U000000D9', - "Uuml": '\U000000DC', - "Yacute": '\U000000DD', - "aacute": '\U000000E1', - "acirc": '\U000000E2', - "acute": '\U000000B4', - "aelig": '\U000000E6', - "agrave": '\U000000E0', - "amp": '\U00000026', - "aring": '\U000000E5', - "atilde": '\U000000E3', - "auml": '\U000000E4', - "brvbar": '\U000000A6', - "ccedil": '\U000000E7', - "cedil": '\U000000B8', - "cent": '\U000000A2', - "copy": '\U000000A9', - "curren": '\U000000A4', - "deg": '\U000000B0', - "divide": '\U000000F7', - "eacute": '\U000000E9', - "ecirc": '\U000000EA', - "egrave": '\U000000E8', - "eth": '\U000000F0', - "euml": '\U000000EB', - "frac12": '\U000000BD', - "frac14": '\U000000BC', - "frac34": '\U000000BE', - "gt": '\U0000003E', - "iacute": '\U000000ED', - "icirc": '\U000000EE', - "iexcl": '\U000000A1', - "igrave": '\U000000EC', - "iquest": '\U000000BF', - "iuml": '\U000000EF', - "laquo": '\U000000AB', - "lt": '\U0000003C', - "macr": '\U000000AF', - "micro": '\U000000B5', - "middot": '\U000000B7', - "nbsp": '\U000000A0', - "not": '\U000000AC', - "ntilde": '\U000000F1', - "oacute": '\U000000F3', - "ocirc": '\U000000F4', - "ograve": '\U000000F2', - "ordf": '\U000000AA', - "ordm": '\U000000BA', - "oslash": '\U000000F8', - "otilde": '\U000000F5', - "ouml": '\U000000F6', - "para": '\U000000B6', - "plusmn": '\U000000B1', - "pound": '\U000000A3', - "quot": '\U00000022', - "raquo": '\U000000BB', - "reg": '\U000000AE', - "sect": '\U000000A7', - "shy": '\U000000AD', - "sup1": '\U000000B9', - "sup2": '\U000000B2', - "sup3": '\U000000B3', - "szlig": '\U000000DF', - "thorn": '\U000000FE', - "times": '\U000000D7', - "uacute": '\U000000FA', - "ucirc": '\U000000FB', - "ugrave": '\U000000F9', - "uml": '\U000000A8', - "uuml": '\U000000FC', - "yacute": '\U000000FD', - "yen": '\U000000A5', - "yuml": '\U000000FF', -} - -// HTML entities that are two unicode codepoints. -var entity2 = map[string][2]rune{ - // TODO(nigeltao): Handle replacements that are wider than their names. - // "nLt;": {'\u226A', '\u20D2'}, - // "nGt;": {'\u226B', '\u20D2'}, - "NotEqualTilde;": {'\u2242', '\u0338'}, - "NotGreaterFullEqual;": {'\u2267', '\u0338'}, - "NotGreaterGreater;": {'\u226B', '\u0338'}, - "NotGreaterSlantEqual;": {'\u2A7E', '\u0338'}, - "NotHumpDownHump;": {'\u224E', '\u0338'}, - "NotHumpEqual;": {'\u224F', '\u0338'}, - "NotLeftTriangleBar;": {'\u29CF', '\u0338'}, - "NotLessLess;": {'\u226A', '\u0338'}, - "NotLessSlantEqual;": {'\u2A7D', '\u0338'}, - "NotNestedGreaterGreater;": {'\u2AA2', '\u0338'}, - "NotNestedLessLess;": {'\u2AA1', '\u0338'}, - "NotPrecedesEqual;": {'\u2AAF', '\u0338'}, - "NotRightTriangleBar;": {'\u29D0', '\u0338'}, - "NotSquareSubset;": {'\u228F', '\u0338'}, - "NotSquareSuperset;": {'\u2290', '\u0338'}, - "NotSubset;": {'\u2282', '\u20D2'}, - "NotSucceedsEqual;": {'\u2AB0', '\u0338'}, - "NotSucceedsTilde;": {'\u227F', '\u0338'}, - "NotSuperset;": {'\u2283', '\u20D2'}, - "ThickSpace;": {'\u205F', '\u200A'}, - "acE;": {'\u223E', '\u0333'}, - "bne;": {'\u003D', '\u20E5'}, - "bnequiv;": {'\u2261', '\u20E5'}, - "caps;": {'\u2229', '\uFE00'}, - "cups;": {'\u222A', '\uFE00'}, - "fjlig;": {'\u0066', '\u006A'}, - "gesl;": {'\u22DB', '\uFE00'}, - "gvertneqq;": {'\u2269', '\uFE00'}, - "gvnE;": {'\u2269', '\uFE00'}, - "lates;": {'\u2AAD', '\uFE00'}, - "lesg;": {'\u22DA', '\uFE00'}, - "lvertneqq;": {'\u2268', '\uFE00'}, - "lvnE;": {'\u2268', '\uFE00'}, - "nGg;": {'\u22D9', '\u0338'}, - "nGtv;": {'\u226B', '\u0338'}, - "nLl;": {'\u22D8', '\u0338'}, - "nLtv;": {'\u226A', '\u0338'}, - "nang;": {'\u2220', '\u20D2'}, - "napE;": {'\u2A70', '\u0338'}, - "napid;": {'\u224B', '\u0338'}, - "nbump;": {'\u224E', '\u0338'}, - "nbumpe;": {'\u224F', '\u0338'}, - "ncongdot;": {'\u2A6D', '\u0338'}, - "nedot;": {'\u2250', '\u0338'}, - "nesim;": {'\u2242', '\u0338'}, - "ngE;": {'\u2267', '\u0338'}, - "ngeqq;": {'\u2267', '\u0338'}, - "ngeqslant;": {'\u2A7E', '\u0338'}, - "nges;": {'\u2A7E', '\u0338'}, - "nlE;": {'\u2266', '\u0338'}, - "nleqq;": {'\u2266', '\u0338'}, - "nleqslant;": {'\u2A7D', '\u0338'}, - "nles;": {'\u2A7D', '\u0338'}, - "notinE;": {'\u22F9', '\u0338'}, - "notindot;": {'\u22F5', '\u0338'}, - "nparsl;": {'\u2AFD', '\u20E5'}, - "npart;": {'\u2202', '\u0338'}, - "npre;": {'\u2AAF', '\u0338'}, - "npreceq;": {'\u2AAF', '\u0338'}, - "nrarrc;": {'\u2933', '\u0338'}, - "nrarrw;": {'\u219D', '\u0338'}, - "nsce;": {'\u2AB0', '\u0338'}, - "nsubE;": {'\u2AC5', '\u0338'}, - "nsubset;": {'\u2282', '\u20D2'}, - "nsubseteqq;": {'\u2AC5', '\u0338'}, - "nsucceq;": {'\u2AB0', '\u0338'}, - "nsupE;": {'\u2AC6', '\u0338'}, - "nsupset;": {'\u2283', '\u20D2'}, - "nsupseteqq;": {'\u2AC6', '\u0338'}, - "nvap;": {'\u224D', '\u20D2'}, - "nvge;": {'\u2265', '\u20D2'}, - "nvgt;": {'\u003E', '\u20D2'}, - "nvle;": {'\u2264', '\u20D2'}, - "nvlt;": {'\u003C', '\u20D2'}, - "nvltrie;": {'\u22B4', '\u20D2'}, - "nvrtrie;": {'\u22B5', '\u20D2'}, - "nvsim;": {'\u223C', '\u20D2'}, - "race;": {'\u223D', '\u0331'}, - "smtes;": {'\u2AAC', '\uFE00'}, - "sqcaps;": {'\u2293', '\uFE00'}, - "sqcups;": {'\u2294', '\uFE00'}, - "varsubsetneq;": {'\u228A', '\uFE00'}, - "varsubsetneqq;": {'\u2ACB', '\uFE00'}, - "varsupsetneq;": {'\u228B', '\uFE00'}, - "varsupsetneqq;": {'\u2ACC', '\uFE00'}, - "vnsub;": {'\u2282', '\u20D2'}, - "vnsup;": {'\u2283', '\u20D2'}, - "vsubnE;": {'\u2ACB', '\uFE00'}, - "vsubne;": {'\u228A', '\uFE00'}, - "vsupnE;": {'\u2ACC', '\uFE00'}, - "vsupne;": {'\u228B', '\uFE00'}, -} diff --git a/src/code.google.com/p/go.net/html/entity_test.go b/src/code.google.com/p/go.net/html/entity_test.go deleted file mode 100644 index b53f866fa2d..00000000000 --- a/src/code.google.com/p/go.net/html/entity_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "testing" - "unicode/utf8" -) - -func TestEntityLength(t *testing.T) { - // We verify that the length of UTF-8 encoding of each value is <= 1 + len(key). - // The +1 comes from the leading "&". This property implies that the length of - // unescaped text is <= the length of escaped text. - for k, v := range entity { - if 1+len(k) < utf8.RuneLen(v) { - t.Error("escaped entity &" + k + " is shorter than its UTF-8 encoding " + string(v)) - } - if len(k) > longestEntityWithoutSemicolon && k[len(k)-1] != ';' { - t.Errorf("entity name %s is %d characters, but longestEntityWithoutSemicolon=%d", k, len(k), longestEntityWithoutSemicolon) - } - } - for k, v := range entity2 { - if 1+len(k) < utf8.RuneLen(v[0])+utf8.RuneLen(v[1]) { - t.Error("escaped entity &" + k + " is shorter than its UTF-8 encoding " + string(v[0]) + string(v[1])) - } - } -} diff --git a/src/code.google.com/p/go.net/html/escape.go b/src/code.google.com/p/go.net/html/escape.go deleted file mode 100644 index 75bddff094f..00000000000 --- a/src/code.google.com/p/go.net/html/escape.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "bytes" - "strings" - "unicode/utf8" -) - -// These replacements permit compatibility with old numeric entities that -// assumed Windows-1252 encoding. -// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference -var replacementTable = [...]rune{ - '\u20AC', // First entry is what 0x80 should be replaced with. - '\u0081', - '\u201A', - '\u0192', - '\u201E', - '\u2026', - '\u2020', - '\u2021', - '\u02C6', - '\u2030', - '\u0160', - '\u2039', - '\u0152', - '\u008D', - '\u017D', - '\u008F', - '\u0090', - '\u2018', - '\u2019', - '\u201C', - '\u201D', - '\u2022', - '\u2013', - '\u2014', - '\u02DC', - '\u2122', - '\u0161', - '\u203A', - '\u0153', - '\u009D', - '\u017E', - '\u0178', // Last entry is 0x9F. - // 0x00->'\uFFFD' is handled programmatically. - // 0x0D->'\u000D' is a no-op. -} - -// unescapeEntity reads an entity like "<" from b[src:] and writes the -// corresponding "<" to b[dst:], returning the incremented dst and src cursors. -// Precondition: b[src] == '&' && dst <= src. -// attribute should be true if parsing an attribute value. -func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { - // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference - - // i starts at 1 because we already know that s[0] == '&'. - i, s := 1, b[src:] - - if len(s) <= 1 { - b[dst] = b[src] - return dst + 1, src + 1 - } - - if s[i] == '#' { - if len(s) <= 3 { // We need to have at least "&#.". - b[dst] = b[src] - return dst + 1, src + 1 - } - i++ - c := s[i] - hex := false - if c == 'x' || c == 'X' { - hex = true - i++ - } - - x := '\x00' - for i < len(s) { - c = s[i] - i++ - if hex { - if '0' <= c && c <= '9' { - x = 16*x + rune(c) - '0' - continue - } else if 'a' <= c && c <= 'f' { - x = 16*x + rune(c) - 'a' + 10 - continue - } else if 'A' <= c && c <= 'F' { - x = 16*x + rune(c) - 'A' + 10 - continue - } - } else if '0' <= c && c <= '9' { - x = 10*x + rune(c) - '0' - continue - } - if c != ';' { - i-- - } - break - } - - if i <= 3 { // No characters matched. - b[dst] = b[src] - return dst + 1, src + 1 - } - - if 0x80 <= x && x <= 0x9F { - // Replace characters from Windows-1252 with UTF-8 equivalents. - x = replacementTable[x-0x80] - } else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF { - // Replace invalid characters with the replacement character. - x = '\uFFFD' - } - - return dst + utf8.EncodeRune(b[dst:], x), src + i - } - - // Consume the maximum number of characters possible, with the - // consumed characters matching one of the named references. - - for i < len(s) { - c := s[i] - i++ - // Lower-cased characters are more common in entities, so we check for them first. - if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { - continue - } - if c != ';' { - i-- - } - break - } - - entityName := string(s[1:i]) - if entityName == "" { - // No-op. - } else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' { - // No-op. - } else if x := entity[entityName]; x != 0 { - return dst + utf8.EncodeRune(b[dst:], x), src + i - } else if x := entity2[entityName]; x[0] != 0 { - dst1 := dst + utf8.EncodeRune(b[dst:], x[0]) - return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i - } else if !attribute { - maxLen := len(entityName) - 1 - if maxLen > longestEntityWithoutSemicolon { - maxLen = longestEntityWithoutSemicolon - } - for j := maxLen; j > 1; j-- { - if x := entity[entityName[:j]]; x != 0 { - return dst + utf8.EncodeRune(b[dst:], x), src + j + 1 - } - } - } - - dst1, src1 = dst+i, src+i - copy(b[dst:dst1], b[src:src1]) - return dst1, src1 -} - -// unescape unescapes b's entities in-place, so that "a<b" becomes "a': - esc = ">" - case '"': - // """ is shorter than """. - esc = """ - case '\r': - esc = " " - default: - panic("unrecognized escape character") - } - s = s[i+1:] - if _, err := w.WriteString(esc); err != nil { - return err - } - i = strings.IndexAny(s, escapedChars) - } - _, err := w.WriteString(s) - return err -} - -// EscapeString escapes special characters like "<" to become "<". It -// escapes only five such characters: <, >, &, ' and ". -// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't -// always true. -func EscapeString(s string) string { - if strings.IndexAny(s, escapedChars) == -1 { - return s - } - var buf bytes.Buffer - escape(&buf, s) - return buf.String() -} - -// UnescapeString unescapes entities like "<" to become "<". It unescapes a -// larger range of entities than EscapeString escapes. For example, "á" -// unescapes to "á", as does "á" and "&xE1;". -// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't -// always true. -func UnescapeString(s string) string { - for _, c := range s { - if c == '&' { - return string(unescape([]byte(s), false)) - } - } - return s -} diff --git a/src/code.google.com/p/go.net/html/escape_test.go b/src/code.google.com/p/go.net/html/escape_test.go deleted file mode 100644 index b405d4b4a77..00000000000 --- a/src/code.google.com/p/go.net/html/escape_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import "testing" - -type unescapeTest struct { - // A short description of the test case. - desc string - // The HTML text. - html string - // The unescaped text. - unescaped string -} - -var unescapeTests = []unescapeTest{ - // Handle no entities. - { - "copy", - "A\ttext\nstring", - "A\ttext\nstring", - }, - // Handle simple named entities. - { - "simple", - "& > <", - "& > <", - }, - // Handle hitting the end of the string. - { - "stringEnd", - "& &", - "& &", - }, - // Handle entities with two codepoints. - { - "multiCodepoint", - "text ⋛︀ blah", - "text \u22db\ufe00 blah", - }, - // Handle decimal numeric entities. - { - "decimalEntity", - "Delta = Δ ", - "Delta = Δ ", - }, - // Handle hexadecimal numeric entities. - { - "hexadecimalEntity", - "Lambda = λ = λ ", - "Lambda = λ = λ ", - }, - // Handle numeric early termination. - { - "numericEnds", - "&# &#x €43 © = ©f = ©", - "&# &#x €43 © = ©f = ©", - }, - // Handle numeric ISO-8859-1 entity replacements. - { - "numericReplacements", - "Footnote‡", - "Footnote‡", - }, -} - -func TestUnescape(t *testing.T) { - for _, tt := range unescapeTests { - unescaped := UnescapeString(tt.html) - if unescaped != tt.unescaped { - t.Errorf("TestUnescape %s: want %q, got %q", tt.desc, tt.unescaped, unescaped) - } - } -} - -func TestUnescapeEscape(t *testing.T) { - ss := []string{ - ``, - `abc def`, - `a & b`, - `a&b`, - `a & b`, - `"`, - `"`, - `"<&>"`, - `"<&>"`, - `3&5==1 && 0<1, "0<1", a+acute=á`, - `The special characters are: <, >, &, ' and "`, - } - for _, s := range ss { - if got := UnescapeString(EscapeString(s)); got != s { - t.Errorf("got %q want %q", got, s) - } - } -} diff --git a/src/code.google.com/p/go.net/html/example_test.go b/src/code.google.com/p/go.net/html/example_test.go deleted file mode 100644 index 47341f020a2..00000000000 --- a/src/code.google.com/p/go.net/html/example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This example demonstrates parsing HTML data and walking the resulting tree. -package html_test - -import ( - "fmt" - "log" - "strings" - - "code.google.com/p/go.net/html" -) - -func ExampleParse() { - s := `

Links:

` - doc, err := html.Parse(strings.NewReader(s)) - if err != nil { - log.Fatal(err) - } - var f func(*html.Node) - f = func(n *html.Node) { - if n.Type == html.ElementNode && n.Data == "a" { - for _, a := range n.Attr { - if a.Key == "href" { - fmt.Println(a.Val) - break - } - } - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - f(c) - } - } - f(doc) - // Output: - // foo - // /bar/baz -} diff --git a/src/code.google.com/p/go.net/html/foreign.go b/src/code.google.com/p/go.net/html/foreign.go deleted file mode 100644 index d3b3844099b..00000000000 --- a/src/code.google.com/p/go.net/html/foreign.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "strings" -) - -func adjustAttributeNames(aa []Attribute, nameMap map[string]string) { - for i := range aa { - if newName, ok := nameMap[aa[i].Key]; ok { - aa[i].Key = newName - } - } -} - -func adjustForeignAttributes(aa []Attribute) { - for i, a := range aa { - if a.Key == "" || a.Key[0] != 'x' { - continue - } - switch a.Key { - case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", - "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink": - j := strings.Index(a.Key, ":") - aa[i].Namespace = a.Key[:j] - aa[i].Key = a.Key[j+1:] - } - } -} - -func htmlIntegrationPoint(n *Node) bool { - if n.Type != ElementNode { - return false - } - switch n.Namespace { - case "math": - if n.Data == "annotation-xml" { - for _, a := range n.Attr { - if a.Key == "encoding" { - val := strings.ToLower(a.Val) - if val == "text/html" || val == "application/xhtml+xml" { - return true - } - } - } - } - case "svg": - switch n.Data { - case "desc", "foreignObject", "title": - return true - } - } - return false -} - -func mathMLTextIntegrationPoint(n *Node) bool { - if n.Namespace != "math" { - return false - } - switch n.Data { - case "mi", "mo", "mn", "ms", "mtext": - return true - } - return false -} - -// Section 12.2.5.5. -var breakout = map[string]bool{ - "b": true, - "big": true, - "blockquote": true, - "body": true, - "br": true, - "center": true, - "code": true, - "dd": true, - "div": true, - "dl": true, - "dt": true, - "em": true, - "embed": true, - "h1": true, - "h2": true, - "h3": true, - "h4": true, - "h5": true, - "h6": true, - "head": true, - "hr": true, - "i": true, - "img": true, - "li": true, - "listing": true, - "menu": true, - "meta": true, - "nobr": true, - "ol": true, - "p": true, - "pre": true, - "ruby": true, - "s": true, - "small": true, - "span": true, - "strong": true, - "strike": true, - "sub": true, - "sup": true, - "table": true, - "tt": true, - "u": true, - "ul": true, - "var": true, -} - -// Section 12.2.5.5. -var svgTagNameAdjustments = map[string]string{ - "altglyph": "altGlyph", - "altglyphdef": "altGlyphDef", - "altglyphitem": "altGlyphItem", - "animatecolor": "animateColor", - "animatemotion": "animateMotion", - "animatetransform": "animateTransform", - "clippath": "clipPath", - "feblend": "feBlend", - "fecolormatrix": "feColorMatrix", - "fecomponenttransfer": "feComponentTransfer", - "fecomposite": "feComposite", - "feconvolvematrix": "feConvolveMatrix", - "fediffuselighting": "feDiffuseLighting", - "fedisplacementmap": "feDisplacementMap", - "fedistantlight": "feDistantLight", - "feflood": "feFlood", - "fefunca": "feFuncA", - "fefuncb": "feFuncB", - "fefuncg": "feFuncG", - "fefuncr": "feFuncR", - "fegaussianblur": "feGaussianBlur", - "feimage": "feImage", - "femerge": "feMerge", - "femergenode": "feMergeNode", - "femorphology": "feMorphology", - "feoffset": "feOffset", - "fepointlight": "fePointLight", - "fespecularlighting": "feSpecularLighting", - "fespotlight": "feSpotLight", - "fetile": "feTile", - "feturbulence": "feTurbulence", - "foreignobject": "foreignObject", - "glyphref": "glyphRef", - "lineargradient": "linearGradient", - "radialgradient": "radialGradient", - "textpath": "textPath", -} - -// Section 12.2.5.1 -var mathMLAttributeAdjustments = map[string]string{ - "definitionurl": "definitionURL", -} - -var svgAttributeAdjustments = map[string]string{ - "attributename": "attributeName", - "attributetype": "attributeType", - "basefrequency": "baseFrequency", - "baseprofile": "baseProfile", - "calcmode": "calcMode", - "clippathunits": "clipPathUnits", - "contentscripttype": "contentScriptType", - "contentstyletype": "contentStyleType", - "diffuseconstant": "diffuseConstant", - "edgemode": "edgeMode", - "externalresourcesrequired": "externalResourcesRequired", - "filterres": "filterRes", - "filterunits": "filterUnits", - "glyphref": "glyphRef", - "gradienttransform": "gradientTransform", - "gradientunits": "gradientUnits", - "kernelmatrix": "kernelMatrix", - "kernelunitlength": "kernelUnitLength", - "keypoints": "keyPoints", - "keysplines": "keySplines", - "keytimes": "keyTimes", - "lengthadjust": "lengthAdjust", - "limitingconeangle": "limitingConeAngle", - "markerheight": "markerHeight", - "markerunits": "markerUnits", - "markerwidth": "markerWidth", - "maskcontentunits": "maskContentUnits", - "maskunits": "maskUnits", - "numoctaves": "numOctaves", - "pathlength": "pathLength", - "patterncontentunits": "patternContentUnits", - "patterntransform": "patternTransform", - "patternunits": "patternUnits", - "pointsatx": "pointsAtX", - "pointsaty": "pointsAtY", - "pointsatz": "pointsAtZ", - "preservealpha": "preserveAlpha", - "preserveaspectratio": "preserveAspectRatio", - "primitiveunits": "primitiveUnits", - "refx": "refX", - "refy": "refY", - "repeatcount": "repeatCount", - "repeatdur": "repeatDur", - "requiredextensions": "requiredExtensions", - "requiredfeatures": "requiredFeatures", - "specularconstant": "specularConstant", - "specularexponent": "specularExponent", - "spreadmethod": "spreadMethod", - "startoffset": "startOffset", - "stddeviation": "stdDeviation", - "stitchtiles": "stitchTiles", - "surfacescale": "surfaceScale", - "systemlanguage": "systemLanguage", - "tablevalues": "tableValues", - "targetx": "targetX", - "targety": "targetY", - "textlength": "textLength", - "viewbox": "viewBox", - "viewtarget": "viewTarget", - "xchannelselector": "xChannelSelector", - "ychannelselector": "yChannelSelector", - "zoomandpan": "zoomAndPan", -} diff --git a/src/code.google.com/p/go.net/html/node.go b/src/code.google.com/p/go.net/html/node.go deleted file mode 100644 index e7b4e50a019..00000000000 --- a/src/code.google.com/p/go.net/html/node.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "code.google.com/p/go.net/html/atom" -) - -// A NodeType is the type of a Node. -type NodeType uint32 - -const ( - ErrorNode NodeType = iota - TextNode - DocumentNode - ElementNode - CommentNode - DoctypeNode - scopeMarkerNode -) - -// Section 12.2.3.3 says "scope markers are inserted when entering applet -// elements, buttons, object elements, marquees, table cells, and table -// captions, and are used to prevent formatting from 'leaking'". -var scopeMarker = Node{Type: scopeMarkerNode} - -// A Node consists of a NodeType and some Data (tag name for element nodes, -// content for text) and are part of a tree of Nodes. Element nodes may also -// have a Namespace and contain a slice of Attributes. Data is unescaped, so -// that it looks like "a 0 { - return (*s)[i-1] - } - return nil -} - -// index returns the index of the top-most occurrence of n in the stack, or -1 -// if n is not present. -func (s *nodeStack) index(n *Node) int { - for i := len(*s) - 1; i >= 0; i-- { - if (*s)[i] == n { - return i - } - } - return -1 -} - -// insert inserts a node at the given index. -func (s *nodeStack) insert(i int, n *Node) { - (*s) = append(*s, nil) - copy((*s)[i+1:], (*s)[i:]) - (*s)[i] = n -} - -// remove removes a node from the stack. It is a no-op if n is not present. -func (s *nodeStack) remove(n *Node) { - i := s.index(n) - if i == -1 { - return - } - copy((*s)[i:], (*s)[i+1:]) - j := len(*s) - 1 - (*s)[j] = nil - *s = (*s)[:j] -} diff --git a/src/code.google.com/p/go.net/html/node_test.go b/src/code.google.com/p/go.net/html/node_test.go deleted file mode 100644 index 471102f3a22..00000000000 --- a/src/code.google.com/p/go.net/html/node_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "fmt" -) - -// checkTreeConsistency checks that a node and its descendants are all -// consistent in their parent/child/sibling relationships. -func checkTreeConsistency(n *Node) error { - return checkTreeConsistency1(n, 0) -} - -func checkTreeConsistency1(n *Node, depth int) error { - if depth == 1e4 { - return fmt.Errorf("html: tree looks like it contains a cycle") - } - if err := checkNodeConsistency(n); err != nil { - return err - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - if err := checkTreeConsistency1(c, depth+1); err != nil { - return err - } - } - return nil -} - -// checkNodeConsistency checks that a node's parent/child/sibling relationships -// are consistent. -func checkNodeConsistency(n *Node) error { - if n == nil { - return nil - } - - nParent := 0 - for p := n.Parent; p != nil; p = p.Parent { - nParent++ - if nParent == 1e4 { - return fmt.Errorf("html: parent list looks like an infinite loop") - } - } - - nForward := 0 - for c := n.FirstChild; c != nil; c = c.NextSibling { - nForward++ - if nForward == 1e6 { - return fmt.Errorf("html: forward list of children looks like an infinite loop") - } - if c.Parent != n { - return fmt.Errorf("html: inconsistent child/parent relationship") - } - } - - nBackward := 0 - for c := n.LastChild; c != nil; c = c.PrevSibling { - nBackward++ - if nBackward == 1e6 { - return fmt.Errorf("html: backward list of children looks like an infinite loop") - } - if c.Parent != n { - return fmt.Errorf("html: inconsistent child/parent relationship") - } - } - - if n.Parent != nil { - if n.Parent == n { - return fmt.Errorf("html: inconsistent parent relationship") - } - if n.Parent == n.FirstChild { - return fmt.Errorf("html: inconsistent parent/first relationship") - } - if n.Parent == n.LastChild { - return fmt.Errorf("html: inconsistent parent/last relationship") - } - if n.Parent == n.PrevSibling { - return fmt.Errorf("html: inconsistent parent/prev relationship") - } - if n.Parent == n.NextSibling { - return fmt.Errorf("html: inconsistent parent/next relationship") - } - - parentHasNAsAChild := false - for c := n.Parent.FirstChild; c != nil; c = c.NextSibling { - if c == n { - parentHasNAsAChild = true - break - } - } - if !parentHasNAsAChild { - return fmt.Errorf("html: inconsistent parent/child relationship") - } - } - - if n.PrevSibling != nil && n.PrevSibling.NextSibling != n { - return fmt.Errorf("html: inconsistent prev/next relationship") - } - if n.NextSibling != nil && n.NextSibling.PrevSibling != n { - return fmt.Errorf("html: inconsistent next/prev relationship") - } - - if (n.FirstChild == nil) != (n.LastChild == nil) { - return fmt.Errorf("html: inconsistent first/last relationship") - } - if n.FirstChild != nil && n.FirstChild == n.LastChild { - // We have a sole child. - if n.FirstChild.PrevSibling != nil || n.FirstChild.NextSibling != nil { - return fmt.Errorf("html: inconsistent sole child's sibling relationship") - } - } - - seen := map[*Node]bool{} - - var last *Node - for c := n.FirstChild; c != nil; c = c.NextSibling { - if seen[c] { - return fmt.Errorf("html: inconsistent repeated child") - } - seen[c] = true - last = c - } - if last != n.LastChild { - return fmt.Errorf("html: inconsistent last relationship") - } - - var first *Node - for c := n.LastChild; c != nil; c = c.PrevSibling { - if !seen[c] { - return fmt.Errorf("html: inconsistent missing child") - } - delete(seen, c) - first = c - } - if first != n.FirstChild { - return fmt.Errorf("html: inconsistent first relationship") - } - - if len(seen) != 0 { - return fmt.Errorf("html: inconsistent forwards/backwards child list") - } - - return nil -} diff --git a/src/code.google.com/p/go.net/html/parse.go b/src/code.google.com/p/go.net/html/parse.go deleted file mode 100644 index bf99ec6ab2d..00000000000 --- a/src/code.google.com/p/go.net/html/parse.go +++ /dev/null @@ -1,2092 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "errors" - "fmt" - "io" - "strings" - - a "code.google.com/p/go.net/html/atom" -) - -// A parser implements the HTML5 parsing algorithm: -// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#tree-construction -type parser struct { - // tokenizer provides the tokens for the parser. - tokenizer *Tokenizer - // tok is the most recently read token. - tok Token - // Self-closing tags like
are treated as start tags, except that - // hasSelfClosingToken is set while they are being processed. - hasSelfClosingToken bool - // doc is the document root element. - doc *Node - // The stack of open elements (section 12.2.3.2) and active formatting - // elements (section 12.2.3.3). - oe, afe nodeStack - // Element pointers (section 12.2.3.4). - head, form *Node - // Other parsing state flags (section 12.2.3.5). - scripting, framesetOK bool - // im is the current insertion mode. - im insertionMode - // originalIM is the insertion mode to go back to after completing a text - // or inTableText insertion mode. - originalIM insertionMode - // fosterParenting is whether new elements should be inserted according to - // the foster parenting rules (section 12.2.5.3). - fosterParenting bool - // quirks is whether the parser is operating in "quirks mode." - quirks bool - // fragment is whether the parser is parsing an HTML fragment. - fragment bool - // context is the context element when parsing an HTML fragment - // (section 12.4). - context *Node -} - -func (p *parser) top() *Node { - if n := p.oe.top(); n != nil { - return n - } - return p.doc -} - -// Stop tags for use in popUntil. These come from section 12.2.3.2. -var ( - defaultScopeStopTags = map[string][]a.Atom{ - "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object}, - "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, - "svg": {a.Desc, a.ForeignObject, a.Title}, - } -) - -type scope int - -const ( - defaultScope scope = iota - listItemScope - buttonScope - tableScope - tableRowScope - tableBodyScope - selectScope -) - -// popUntil pops the stack of open elements at the highest element whose tag -// is in matchTags, provided there is no higher element in the scope's stop -// tags (as defined in section 12.2.3.2). It returns whether or not there was -// such an element. If there was not, popUntil leaves the stack unchanged. -// -// For example, the set of stop tags for table scope is: "html", "table". If -// the stack was: -// ["html", "body", "font", "table", "b", "i", "u"] -// then popUntil(tableScope, "font") would return false, but -// popUntil(tableScope, "i") would return true and the stack would become: -// ["html", "body", "font", "table", "b"] -// -// If an element's tag is in both the stop tags and matchTags, then the stack -// will be popped and the function returns true (provided, of course, there was -// no higher element in the stack that was also in the stop tags). For example, -// popUntil(tableScope, "table") returns true and leaves: -// ["html", "body", "font"] -func (p *parser) popUntil(s scope, matchTags ...a.Atom) bool { - if i := p.indexOfElementInScope(s, matchTags...); i != -1 { - p.oe = p.oe[:i] - return true - } - return false -} - -// indexOfElementInScope returns the index in p.oe of the highest element whose -// tag is in matchTags that is in scope. If no matching element is in scope, it -// returns -1. -func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int { - for i := len(p.oe) - 1; i >= 0; i-- { - tagAtom := p.oe[i].DataAtom - if p.oe[i].Namespace == "" { - for _, t := range matchTags { - if t == tagAtom { - return i - } - } - switch s { - case defaultScope: - // No-op. - case listItemScope: - if tagAtom == a.Ol || tagAtom == a.Ul { - return -1 - } - case buttonScope: - if tagAtom == a.Button { - return -1 - } - case tableScope: - if tagAtom == a.Html || tagAtom == a.Table { - return -1 - } - case selectScope: - if tagAtom != a.Optgroup && tagAtom != a.Option { - return -1 - } - default: - panic("unreachable") - } - } - switch s { - case defaultScope, listItemScope, buttonScope: - for _, t := range defaultScopeStopTags[p.oe[i].Namespace] { - if t == tagAtom { - return -1 - } - } - } - } - return -1 -} - -// elementInScope is like popUntil, except that it doesn't modify the stack of -// open elements. -func (p *parser) elementInScope(s scope, matchTags ...a.Atom) bool { - return p.indexOfElementInScope(s, matchTags...) != -1 -} - -// clearStackToContext pops elements off the stack of open elements until a -// scope-defined element is found. -func (p *parser) clearStackToContext(s scope) { - for i := len(p.oe) - 1; i >= 0; i-- { - tagAtom := p.oe[i].DataAtom - switch s { - case tableScope: - if tagAtom == a.Html || tagAtom == a.Table { - p.oe = p.oe[:i+1] - return - } - case tableRowScope: - if tagAtom == a.Html || tagAtom == a.Tr { - p.oe = p.oe[:i+1] - return - } - case tableBodyScope: - if tagAtom == a.Html || tagAtom == a.Tbody || tagAtom == a.Tfoot || tagAtom == a.Thead { - p.oe = p.oe[:i+1] - return - } - default: - panic("unreachable") - } - } -} - -// generateImpliedEndTags pops nodes off the stack of open elements as long as -// the top node has a tag name of dd, dt, li, option, optgroup, p, rp, or rt. -// If exceptions are specified, nodes with that name will not be popped off. -func (p *parser) generateImpliedEndTags(exceptions ...string) { - var i int -loop: - for i = len(p.oe) - 1; i >= 0; i-- { - n := p.oe[i] - if n.Type == ElementNode { - switch n.DataAtom { - case a.Dd, a.Dt, a.Li, a.Option, a.Optgroup, a.P, a.Rp, a.Rt: - for _, except := range exceptions { - if n.Data == except { - break loop - } - } - continue - } - } - break - } - - p.oe = p.oe[:i+1] -} - -// addChild adds a child node n to the top element, and pushes n onto the stack -// of open elements if it is an element node. -func (p *parser) addChild(n *Node) { - if p.shouldFosterParent() { - p.fosterParent(n) - } else { - p.top().AppendChild(n) - } - - if n.Type == ElementNode { - p.oe = append(p.oe, n) - } -} - -// shouldFosterParent returns whether the next node to be added should be -// foster parented. -func (p *parser) shouldFosterParent() bool { - if p.fosterParenting { - switch p.top().DataAtom { - case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: - return true - } - } - return false -} - -// fosterParent adds a child node according to the foster parenting rules. -// Section 12.2.5.3, "foster parenting". -func (p *parser) fosterParent(n *Node) { - var table, parent, prev *Node - var i int - for i = len(p.oe) - 1; i >= 0; i-- { - if p.oe[i].DataAtom == a.Table { - table = p.oe[i] - break - } - } - - if table == nil { - // The foster parent is the html element. - parent = p.oe[0] - } else { - parent = table.Parent - } - if parent == nil { - parent = p.oe[i-1] - } - - if table != nil { - prev = table.PrevSibling - } else { - prev = parent.LastChild - } - if prev != nil && prev.Type == TextNode && n.Type == TextNode { - prev.Data += n.Data - return - } - - parent.InsertBefore(n, table) -} - -// addText adds text to the preceding node if it is a text node, or else it -// calls addChild with a new text node. -func (p *parser) addText(text string) { - if text == "" { - return - } - - if p.shouldFosterParent() { - p.fosterParent(&Node{ - Type: TextNode, - Data: text, - }) - return - } - - t := p.top() - if n := t.LastChild; n != nil && n.Type == TextNode { - n.Data += text - return - } - p.addChild(&Node{ - Type: TextNode, - Data: text, - }) -} - -// addElement adds a child element based on the current token. -func (p *parser) addElement() { - p.addChild(&Node{ - Type: ElementNode, - DataAtom: p.tok.DataAtom, - Data: p.tok.Data, - Attr: p.tok.Attr, - }) -} - -// Section 12.2.3.3. -func (p *parser) addFormattingElement() { - tagAtom, attr := p.tok.DataAtom, p.tok.Attr - p.addElement() - - // Implement the Noah's Ark clause, but with three per family instead of two. - identicalElements := 0 -findIdenticalElements: - for i := len(p.afe) - 1; i >= 0; i-- { - n := p.afe[i] - if n.Type == scopeMarkerNode { - break - } - if n.Type != ElementNode { - continue - } - if n.Namespace != "" { - continue - } - if n.DataAtom != tagAtom { - continue - } - if len(n.Attr) != len(attr) { - continue - } - compareAttributes: - for _, t0 := range n.Attr { - for _, t1 := range attr { - if t0.Key == t1.Key && t0.Namespace == t1.Namespace && t0.Val == t1.Val { - // Found a match for this attribute, continue with the next attribute. - continue compareAttributes - } - } - // If we get here, there is no attribute that matches a. - // Therefore the element is not identical to the new one. - continue findIdenticalElements - } - - identicalElements++ - if identicalElements >= 3 { - p.afe.remove(n) - } - } - - p.afe = append(p.afe, p.top()) -} - -// Section 12.2.3.3. -func (p *parser) clearActiveFormattingElements() { - for { - n := p.afe.pop() - if len(p.afe) == 0 || n.Type == scopeMarkerNode { - return - } - } -} - -// Section 12.2.3.3. -func (p *parser) reconstructActiveFormattingElements() { - n := p.afe.top() - if n == nil { - return - } - if n.Type == scopeMarkerNode || p.oe.index(n) != -1 { - return - } - i := len(p.afe) - 1 - for n.Type != scopeMarkerNode && p.oe.index(n) == -1 { - if i == 0 { - i = -1 - break - } - i-- - n = p.afe[i] - } - for { - i++ - clone := p.afe[i].clone() - p.addChild(clone) - p.afe[i] = clone - if i == len(p.afe)-1 { - break - } - } -} - -// Section 12.2.4. -func (p *parser) acknowledgeSelfClosingTag() { - p.hasSelfClosingToken = false -} - -// An insertion mode (section 12.2.3.1) is the state transition function from -// a particular state in the HTML5 parser's state machine. It updates the -// parser's fields depending on parser.tok (where ErrorToken means EOF). -// It returns whether the token was consumed. -type insertionMode func(*parser) bool - -// setOriginalIM sets the insertion mode to return to after completing a text or -// inTableText insertion mode. -// Section 12.2.3.1, "using the rules for". -func (p *parser) setOriginalIM() { - if p.originalIM != nil { - panic("html: bad parser state: originalIM was set twice") - } - p.originalIM = p.im -} - -// Section 12.2.3.1, "reset the insertion mode". -func (p *parser) resetInsertionMode() { - for i := len(p.oe) - 1; i >= 0; i-- { - n := p.oe[i] - if i == 0 && p.context != nil { - n = p.context - } - - switch n.DataAtom { - case a.Select: - p.im = inSelectIM - case a.Td, a.Th: - p.im = inCellIM - case a.Tr: - p.im = inRowIM - case a.Tbody, a.Thead, a.Tfoot: - p.im = inTableBodyIM - case a.Caption: - p.im = inCaptionIM - case a.Colgroup: - p.im = inColumnGroupIM - case a.Table: - p.im = inTableIM - case a.Head: - p.im = inBodyIM - case a.Body: - p.im = inBodyIM - case a.Frameset: - p.im = inFramesetIM - case a.Html: - p.im = beforeHeadIM - default: - continue - } - return - } - p.im = inBodyIM -} - -const whitespace = " \t\r\n\f" - -// Section 12.2.5.4.1. -func initialIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case CommentToken: - p.doc.AppendChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - n, quirks := parseDoctype(p.tok.Data) - p.doc.AppendChild(n) - p.quirks = quirks - p.im = beforeHTMLIM - return true - } - p.quirks = true - p.im = beforeHTMLIM - return false -} - -// Section 12.2.5.4.2. -func beforeHTMLIM(p *parser) bool { - switch p.tok.Type { - case DoctypeToken: - // Ignore the token. - return true - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case StartTagToken: - if p.tok.DataAtom == a.Html { - p.addElement() - p.im = beforeHeadIM - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head, a.Body, a.Html, a.Br: - p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.doc.AppendChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - } - p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) - return false -} - -// Section 12.2.5.4.3. -func beforeHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Head: - p.addElement() - p.head = p.top() - p.im = inHeadIM - return true - case a.Html: - return inBodyIM(p) - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head, a.Body, a.Html, a.Br: - p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) - return false -} - -// Section 12.2.5.4.4. -func inHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - s := strings.TrimLeft(p.tok.Data, whitespace) - if len(s) < len(p.tok.Data) { - // Add the initial whitespace to the current node. - p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) - if s == "" { - return true - } - p.tok.Data = s - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - return inBodyIM(p) - case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta: - p.addElement() - p.oe.pop() - p.acknowledgeSelfClosingTag() - return true - case a.Script, a.Title, a.Noscript, a.Noframes, a.Style: - p.addElement() - p.setOriginalIM() - p.im = textIM - return true - case a.Head: - // Ignore the token. - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head: - n := p.oe.pop() - if n.DataAtom != a.Head { - panic("html: bad parser state: element not found, in the in-head insertion mode") - } - p.im = afterHeadIM - return true - case a.Body, a.Html, a.Br: - p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) - return false -} - -// Section 12.2.5.4.6. -func afterHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - s := strings.TrimLeft(p.tok.Data, whitespace) - if len(s) < len(p.tok.Data) { - // Add the initial whitespace to the current node. - p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) - if s == "" { - return true - } - p.tok.Data = s - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - return inBodyIM(p) - case a.Body: - p.addElement() - p.framesetOK = false - p.im = inBodyIM - return true - case a.Frameset: - p.addElement() - p.im = inFramesetIM - return true - case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title: - p.oe = append(p.oe, p.head) - defer p.oe.remove(p.head) - return inHeadIM(p) - case a.Head: - // Ignore the token. - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Body, a.Html, a.Br: - // Drop down to creating an implied tag. - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(StartTagToken, a.Body, a.Body.String()) - p.framesetOK = true - return false -} - -// copyAttributes copies attributes of src not found on dst to dst. -func copyAttributes(dst *Node, src Token) { - if len(src.Attr) == 0 { - return - } - attr := map[string]string{} - for _, t := range dst.Attr { - attr[t.Key] = t.Val - } - for _, t := range src.Attr { - if _, ok := attr[t.Key]; !ok { - dst.Attr = append(dst.Attr, t) - attr[t.Key] = t.Val - } - } -} - -// Section 12.2.5.4.7. -func inBodyIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - d := p.tok.Data - switch n := p.oe.top(); n.DataAtom { - case a.Pre, a.Listing: - if n.FirstChild == nil { - // Ignore a newline at the start of a
 block.
-				if d != "" && d[0] == '\r' {
-					d = d[1:]
-				}
-				if d != "" && d[0] == '\n' {
-					d = d[1:]
-				}
-			}
-		}
-		d = strings.Replace(d, "\x00", "", -1)
-		if d == "" {
-			return true
-		}
-		p.reconstructActiveFormattingElements()
-		p.addText(d)
-		if p.framesetOK && strings.TrimLeft(d, whitespace) != "" {
-			// There were non-whitespace characters inserted.
-			p.framesetOK = false
-		}
-	case StartTagToken:
-		switch p.tok.DataAtom {
-		case a.Html:
-			copyAttributes(p.oe[0], p.tok)
-		case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title:
-			return inHeadIM(p)
-		case a.Body:
-			if len(p.oe) >= 2 {
-				body := p.oe[1]
-				if body.Type == ElementNode && body.DataAtom == a.Body {
-					p.framesetOK = false
-					copyAttributes(body, p.tok)
-				}
-			}
-		case a.Frameset:
-			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body {
-				// Ignore the token.
-				return true
-			}
-			body := p.oe[1]
-			if body.Parent != nil {
-				body.Parent.RemoveChild(body)
-			}
-			p.oe = p.oe[:1]
-			p.addElement()
-			p.im = inFramesetIM
-			return true
-		case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-			p.popUntil(buttonScope, a.P)
-			switch n := p.top(); n.DataAtom {
-			case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-				p.oe.pop()
-			}
-			p.addElement()
-		case a.Pre, a.Listing:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-			// The newline, if any, will be dealt with by the TextToken case.
-			p.framesetOK = false
-		case a.Form:
-			if p.form == nil {
-				p.popUntil(buttonScope, a.P)
-				p.addElement()
-				p.form = p.top()
-			}
-		case a.Li:
-			p.framesetOK = false
-			for i := len(p.oe) - 1; i >= 0; i-- {
-				node := p.oe[i]
-				switch node.DataAtom {
-				case a.Li:
-					p.oe = p.oe[:i]
-				case a.Address, a.Div, a.P:
-					continue
-				default:
-					if !isSpecialElement(node) {
-						continue
-					}
-				}
-				break
-			}
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Dd, a.Dt:
-			p.framesetOK = false
-			for i := len(p.oe) - 1; i >= 0; i-- {
-				node := p.oe[i]
-				switch node.DataAtom {
-				case a.Dd, a.Dt:
-					p.oe = p.oe[:i]
-				case a.Address, a.Div, a.P:
-					continue
-				default:
-					if !isSpecialElement(node) {
-						continue
-					}
-				}
-				break
-			}
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Plaintext:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Button:
-			p.popUntil(defaultScope, a.Button)
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-		case a.A:
-			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
-				if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A {
-					p.inBodyEndTagFormatting(a.A)
-					p.oe.remove(n)
-					p.afe.remove(n)
-					break
-				}
-			}
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement()
-		case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement()
-		case a.Nobr:
-			p.reconstructActiveFormattingElements()
-			if p.elementInScope(defaultScope, a.Nobr) {
-				p.inBodyEndTagFormatting(a.Nobr)
-				p.reconstructActiveFormattingElements()
-			}
-			p.addFormattingElement()
-		case a.Applet, a.Marquee, a.Object:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.afe = append(p.afe, &scopeMarker)
-			p.framesetOK = false
-		case a.Table:
-			if !p.quirks {
-				p.popUntil(buttonScope, a.P)
-			}
-			p.addElement()
-			p.framesetOK = false
-			p.im = inTableIM
-			return true
-		case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-			if p.tok.DataAtom == a.Input {
-				for _, t := range p.tok.Attr {
-					if t.Key == "type" {
-						if strings.ToLower(t.Val) == "hidden" {
-							// Skip setting framesetOK = false
-							return true
-						}
-					}
-				}
-			}
-			p.framesetOK = false
-		case a.Param, a.Source, a.Track:
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-		case a.Hr:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-			p.framesetOK = false
-		case a.Image:
-			p.tok.DataAtom = a.Img
-			p.tok.Data = a.Img.String()
-			return false
-		case a.Isindex:
-			if p.form != nil {
-				// Ignore the token.
-				return true
-			}
-			action := ""
-			prompt := "This is a searchable index. Enter search keywords: "
-			attr := []Attribute{{Key: "name", Val: "isindex"}}
-			for _, t := range p.tok.Attr {
-				switch t.Key {
-				case "action":
-					action = t.Val
-				case "name":
-					// Ignore the attribute.
-				case "prompt":
-					prompt = t.Val
-				default:
-					attr = append(attr, t)
-				}
-			}
-			p.acknowledgeSelfClosingTag()
-			p.popUntil(buttonScope, a.P)
-			p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
-			if action != "" {
-				p.form.Attr = []Attribute{{Key: "action", Val: action}}
-			}
-			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
-			p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
-			p.addText(prompt)
-			p.addChild(&Node{
-				Type:     ElementNode,
-				DataAtom: a.Input,
-				Data:     a.Input.String(),
-				Attr:     attr,
-			})
-			p.oe.pop()
-			p.parseImpliedToken(EndTagToken, a.Label, a.Label.String())
-			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
-			p.parseImpliedToken(EndTagToken, a.Form, a.Form.String())
-		case a.Textarea:
-			p.addElement()
-			p.setOriginalIM()
-			p.framesetOK = false
-			p.im = textIM
-		case a.Xmp:
-			p.popUntil(buttonScope, a.P)
-			p.reconstructActiveFormattingElements()
-			p.framesetOK = false
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Iframe:
-			p.framesetOK = false
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Noembed, a.Noscript:
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Select:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-			p.im = inSelectIM
-			return true
-		case a.Optgroup, a.Option:
-			if p.top().DataAtom == a.Option {
-				p.oe.pop()
-			}
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-		case a.Rp, a.Rt:
-			if p.elementInScope(defaultScope, a.Ruby) {
-				p.generateImpliedEndTags()
-			}
-			p.addElement()
-		case a.Math, a.Svg:
-			p.reconstructActiveFormattingElements()
-			if p.tok.DataAtom == a.Math {
-				adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
-			} else {
-				adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
-			}
-			adjustForeignAttributes(p.tok.Attr)
-			p.addElement()
-			p.top().Namespace = p.tok.Data
-			if p.hasSelfClosingToken {
-				p.oe.pop()
-				p.acknowledgeSelfClosingTag()
-			}
-			return true
-		case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
-			// Ignore the token.
-		default:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-		}
-	case EndTagToken:
-		switch p.tok.DataAtom {
-		case a.Body:
-			if p.elementInScope(defaultScope, a.Body) {
-				p.im = afterBodyIM
-			}
-		case a.Html:
-			if p.elementInScope(defaultScope, a.Body) {
-				p.parseImpliedToken(EndTagToken, a.Body, a.Body.String())
-				return false
-			}
-			return true
-		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
-			p.popUntil(defaultScope, p.tok.DataAtom)
-		case a.Form:
-			node := p.form
-			p.form = nil
-			i := p.indexOfElementInScope(defaultScope, a.Form)
-			if node == nil || i == -1 || p.oe[i] != node {
-				// Ignore the token.
-				return true
-			}
-			p.generateImpliedEndTags()
-			p.oe.remove(node)
-		case a.P:
-			if !p.elementInScope(buttonScope, a.P) {
-				p.parseImpliedToken(StartTagToken, a.P, a.P.String())
-			}
-			p.popUntil(buttonScope, a.P)
-		case a.Li:
-			p.popUntil(listItemScope, a.Li)
-		case a.Dd, a.Dt:
-			p.popUntil(defaultScope, p.tok.DataAtom)
-		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-			p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6)
-		case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
-			p.inBodyEndTagFormatting(p.tok.DataAtom)
-		case a.Applet, a.Marquee, a.Object:
-			if p.popUntil(defaultScope, p.tok.DataAtom) {
-				p.clearActiveFormattingElements()
-			}
-		case a.Br:
-			p.tok.Type = StartTagToken
-			return false
-		default:
-			p.inBodyEndTagOther(p.tok.DataAtom)
-		}
-	case CommentToken:
-		p.addChild(&Node{
-			Type: CommentNode,
-			Data: p.tok.Data,
-		})
-	}
-
-	return true
-}
-
-func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
-	// This is the "adoption agency" algorithm, described at
-	// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#adoptionAgency
-
-	// TODO: this is a fairly literal line-by-line translation of that algorithm.
-	// Once the code successfully parses the comprehensive test suite, we should
-	// refactor this code to be more idiomatic.
-
-	// Steps 1-3. The outer loop.
-	for i := 0; i < 8; i++ {
-		// Step 4. Find the formatting element.
-		var formattingElement *Node
-		for j := len(p.afe) - 1; j >= 0; j-- {
-			if p.afe[j].Type == scopeMarkerNode {
-				break
-			}
-			if p.afe[j].DataAtom == tagAtom {
-				formattingElement = p.afe[j]
-				break
-			}
-		}
-		if formattingElement == nil {
-			p.inBodyEndTagOther(tagAtom)
-			return
-		}
-		feIndex := p.oe.index(formattingElement)
-		if feIndex == -1 {
-			p.afe.remove(formattingElement)
-			return
-		}
-		if !p.elementInScope(defaultScope, tagAtom) {
-			// Ignore the tag.
-			return
-		}
-
-		// Steps 5-6. Find the furthest block.
-		var furthestBlock *Node
-		for _, e := range p.oe[feIndex:] {
-			if isSpecialElement(e) {
-				furthestBlock = e
-				break
-			}
-		}
-		if furthestBlock == nil {
-			e := p.oe.pop()
-			for e != formattingElement {
-				e = p.oe.pop()
-			}
-			p.afe.remove(e)
-			return
-		}
-
-		// Steps 7-8. Find the common ancestor and bookmark node.
-		commonAncestor := p.oe[feIndex-1]
-		bookmark := p.afe.index(formattingElement)
-
-		// Step 9. The inner loop. Find the lastNode to reparent.
-		lastNode := furthestBlock
-		node := furthestBlock
-		x := p.oe.index(node)
-		// Steps 9.1-9.3.
-		for j := 0; j < 3; j++ {
-			// Step 9.4.
-			x--
-			node = p.oe[x]
-			// Step 9.5.
-			if p.afe.index(node) == -1 {
-				p.oe.remove(node)
-				continue
-			}
-			// Step 9.6.
-			if node == formattingElement {
-				break
-			}
-			// Step 9.7.
-			clone := node.clone()
-			p.afe[p.afe.index(node)] = clone
-			p.oe[p.oe.index(node)] = clone
-			node = clone
-			// Step 9.8.
-			if lastNode == furthestBlock {
-				bookmark = p.afe.index(node) + 1
-			}
-			// Step 9.9.
-			if lastNode.Parent != nil {
-				lastNode.Parent.RemoveChild(lastNode)
-			}
-			node.AppendChild(lastNode)
-			// Step 9.10.
-			lastNode = node
-		}
-
-		// Step 10. Reparent lastNode to the common ancestor,
-		// or for misnested table nodes, to the foster parent.
-		if lastNode.Parent != nil {
-			lastNode.Parent.RemoveChild(lastNode)
-		}
-		switch commonAncestor.DataAtom {
-		case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
-			p.fosterParent(lastNode)
-		default:
-			commonAncestor.AppendChild(lastNode)
-		}
-
-		// Steps 11-13. Reparent nodes from the furthest block's children
-		// to a clone of the formatting element.
-		clone := formattingElement.clone()
-		reparentChildren(clone, furthestBlock)
-		furthestBlock.AppendChild(clone)
-
-		// Step 14. Fix up the list of active formatting elements.
-		if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark {
-			// Move the bookmark with the rest of the list.
-			bookmark--
-		}
-		p.afe.remove(formattingElement)
-		p.afe.insert(bookmark, clone)
-
-		// Step 15. Fix up the stack of open elements.
-		p.oe.remove(formattingElement)
-		p.oe.insert(p.oe.index(furthestBlock)+1, clone)
-	}
-}
-
-// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
-func (p *parser) inBodyEndTagOther(tagAtom a.Atom) {
-	for i := len(p.oe) - 1; i >= 0; i-- {
-		if p.oe[i].DataAtom == tagAtom {
-			p.oe = p.oe[:i]
-			break
-		}
-		if isSpecialElement(p.oe[i]) {
-			break
-		}
-	}
-}
-
-// Section 12.2.5.4.8.
-func textIM(p *parser) bool {
-	switch p.tok.Type {
-	case ErrorToken:
-		p.oe.pop()
-	case TextToken:
-		d := p.tok.Data
-		if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil {
-			// Ignore a newline at the start of a -->
-#errors
-#document
-| 
-|   
-|   
-|     -->
-#errors
-#document
-| 
-|   
-|   
-|     
-#errors
-Line: 1 Col: 10 Unexpected start tag (textarea). Expected DOCTYPE.
-#document
-| 
-|   
-|   
-|     
-#errors
-Line: 1 Col: 9 Unexpected end tag (strong). Expected DOCTYPE.
-Line: 1 Col: 9 Unexpected end tag (strong) after the (implied) root element.
-Line: 1 Col: 13 Unexpected end tag (b) after the (implied) root element.
-Line: 1 Col: 18 Unexpected end tag (em) after the (implied) root element.
-Line: 1 Col: 22 Unexpected end tag (i) after the (implied) root element.
-Line: 1 Col: 26 Unexpected end tag (u) after the (implied) root element.
-Line: 1 Col: 35 Unexpected end tag (strike) after the (implied) root element.
-Line: 1 Col: 39 Unexpected end tag (s) after the (implied) root element.
-Line: 1 Col: 47 Unexpected end tag (blink) after the (implied) root element.
-Line: 1 Col: 52 Unexpected end tag (tt) after the (implied) root element.
-Line: 1 Col: 58 Unexpected end tag (pre) after the (implied) root element.
-Line: 1 Col: 64 Unexpected end tag (big) after the (implied) root element.
-Line: 1 Col: 72 Unexpected end tag (small) after the (implied) root element.
-Line: 1 Col: 79 Unexpected end tag (font) after the (implied) root element.
-Line: 1 Col: 88 Unexpected end tag (select) after the (implied) root element.
-Line: 1 Col: 93 Unexpected end tag (h1) after the (implied) root element.
-Line: 1 Col: 98 Unexpected end tag (h2) after the (implied) root element.
-Line: 1 Col: 103 Unexpected end tag (h3) after the (implied) root element.
-Line: 1 Col: 108 Unexpected end tag (h4) after the (implied) root element.
-Line: 1 Col: 113 Unexpected end tag (h5) after the (implied) root element.
-Line: 1 Col: 118 Unexpected end tag (h6) after the (implied) root element.
-Line: 1 Col: 125 Unexpected end tag (body) after the (implied) root element.
-Line: 1 Col: 130 Unexpected end tag (br). Treated as br element.
-Line: 1 Col: 134 End tag (a) violates step 1, paragraph 1 of the adoption agency algorithm.
-Line: 1 Col: 140 This element (img) has no end tag.
-Line: 1 Col: 148 Unexpected end tag (title). Ignored.
-Line: 1 Col: 155 Unexpected end tag (span). Ignored.
-Line: 1 Col: 163 Unexpected end tag (style). Ignored.
-Line: 1 Col: 172 Unexpected end tag (script). Ignored.
-Line: 1 Col: 180 Unexpected end tag (table). Ignored.
-Line: 1 Col: 185 Unexpected end tag (th). Ignored.
-Line: 1 Col: 190 Unexpected end tag (td). Ignored.
-Line: 1 Col: 195 Unexpected end tag (tr). Ignored.
-Line: 1 Col: 203 This element (frame) has no end tag.
-Line: 1 Col: 210 This element (area) has no end tag.
-Line: 1 Col: 217 Unexpected end tag (link). Ignored.
-Line: 1 Col: 225 This element (param) has no end tag.
-Line: 1 Col: 230 This element (hr) has no end tag.
-Line: 1 Col: 238 This element (input) has no end tag.
-Line: 1 Col: 244 Unexpected end tag (col). Ignored.
-Line: 1 Col: 251 Unexpected end tag (base). Ignored.
-Line: 1 Col: 258 Unexpected end tag (meta). Ignored.
-Line: 1 Col: 269 This element (basefont) has no end tag.
-Line: 1 Col: 279 This element (bgsound) has no end tag.
-Line: 1 Col: 287 This element (embed) has no end tag.
-Line: 1 Col: 296 This element (spacer) has no end tag.
-Line: 1 Col: 300 Unexpected end tag (p). Ignored.
-Line: 1 Col: 305 End tag (dd) seen too early. Expected other end tag.
-Line: 1 Col: 310 End tag (dt) seen too early. Expected other end tag.
-Line: 1 Col: 320 Unexpected end tag (caption). Ignored.
-Line: 1 Col: 331 Unexpected end tag (colgroup). Ignored.
-Line: 1 Col: 339 Unexpected end tag (tbody). Ignored.
-Line: 1 Col: 347 Unexpected end tag (tfoot). Ignored.
-Line: 1 Col: 355 Unexpected end tag (thead). Ignored.
-Line: 1 Col: 365 End tag (address) seen too early. Expected other end tag.
-Line: 1 Col: 378 End tag (blockquote) seen too early. Expected other end tag.
-Line: 1 Col: 387 End tag (center) seen too early. Expected other end tag.
-Line: 1 Col: 393 Unexpected end tag (dir). Ignored.
-Line: 1 Col: 399 End tag (div) seen too early. Expected other end tag.
-Line: 1 Col: 404 End tag (dl) seen too early. Expected other end tag.
-Line: 1 Col: 415 End tag (fieldset) seen too early. Expected other end tag.
-Line: 1 Col: 425 End tag (listing) seen too early. Expected other end tag.
-Line: 1 Col: 432 End tag (menu) seen too early. Expected other end tag.
-Line: 1 Col: 437 End tag (ol) seen too early. Expected other end tag.
-Line: 1 Col: 442 End tag (ul) seen too early. Expected other end tag.
-Line: 1 Col: 447 End tag (li) seen too early. Expected other end tag.
-Line: 1 Col: 454 End tag (nobr) violates step 1, paragraph 1 of the adoption agency algorithm.
-Line: 1 Col: 460 This element (wbr) has no end tag.
-Line: 1 Col: 476 End tag (button) seen too early. Expected other end tag.
-Line: 1 Col: 486 End tag (marquee) seen too early. Expected other end tag.
-Line: 1 Col: 495 End tag (object) seen too early. Expected other end tag.
-Line: 1 Col: 513 Unexpected end tag (html). Ignored.
-Line: 1 Col: 513 Unexpected end tag (frameset). Ignored.
-Line: 1 Col: 520 Unexpected end tag (head). Ignored.
-Line: 1 Col: 529 Unexpected end tag (iframe). Ignored.
-Line: 1 Col: 537 This element (image) has no end tag.
-Line: 1 Col: 547 This element (isindex) has no end tag.
-Line: 1 Col: 557 Unexpected end tag (noembed). Ignored.
-Line: 1 Col: 568 Unexpected end tag (noframes). Ignored.
-Line: 1 Col: 579 Unexpected end tag (noscript). Ignored.
-Line: 1 Col: 590 Unexpected end tag (optgroup). Ignored.
-Line: 1 Col: 599 Unexpected end tag (option). Ignored.
-Line: 1 Col: 611 Unexpected end tag (plaintext). Ignored.
-Line: 1 Col: 622 Unexpected end tag (textarea). Ignored.
-#document
-| 
-|   
-|   
-|     
-|

- -#data -

-#errors -Line: 1 Col: 7 Unexpected start tag (table). Expected DOCTYPE. -Line: 1 Col: 20 Unexpected end tag (strong) in table context caused voodoo mode. -Line: 1 Col: 20 End tag (strong) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 24 Unexpected end tag (b) in table context caused voodoo mode. -Line: 1 Col: 24 End tag (b) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 29 Unexpected end tag (em) in table context caused voodoo mode. -Line: 1 Col: 29 End tag (em) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 33 Unexpected end tag (i) in table context caused voodoo mode. -Line: 1 Col: 33 End tag (i) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 37 Unexpected end tag (u) in table context caused voodoo mode. -Line: 1 Col: 37 End tag (u) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 46 Unexpected end tag (strike) in table context caused voodoo mode. -Line: 1 Col: 46 End tag (strike) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 50 Unexpected end tag (s) in table context caused voodoo mode. -Line: 1 Col: 50 End tag (s) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 58 Unexpected end tag (blink) in table context caused voodoo mode. -Line: 1 Col: 58 Unexpected end tag (blink). Ignored. -Line: 1 Col: 63 Unexpected end tag (tt) in table context caused voodoo mode. -Line: 1 Col: 63 End tag (tt) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 69 Unexpected end tag (pre) in table context caused voodoo mode. -Line: 1 Col: 69 End tag (pre) seen too early. Expected other end tag. -Line: 1 Col: 75 Unexpected end tag (big) in table context caused voodoo mode. -Line: 1 Col: 75 End tag (big) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 83 Unexpected end tag (small) in table context caused voodoo mode. -Line: 1 Col: 83 End tag (small) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 90 Unexpected end tag (font) in table context caused voodoo mode. -Line: 1 Col: 90 End tag (font) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 99 Unexpected end tag (select) in table context caused voodoo mode. -Line: 1 Col: 99 Unexpected end tag (select). Ignored. -Line: 1 Col: 104 Unexpected end tag (h1) in table context caused voodoo mode. -Line: 1 Col: 104 End tag (h1) seen too early. Expected other end tag. -Line: 1 Col: 109 Unexpected end tag (h2) in table context caused voodoo mode. -Line: 1 Col: 109 End tag (h2) seen too early. Expected other end tag. -Line: 1 Col: 114 Unexpected end tag (h3) in table context caused voodoo mode. -Line: 1 Col: 114 End tag (h3) seen too early. Expected other end tag. -Line: 1 Col: 119 Unexpected end tag (h4) in table context caused voodoo mode. -Line: 1 Col: 119 End tag (h4) seen too early. Expected other end tag. -Line: 1 Col: 124 Unexpected end tag (h5) in table context caused voodoo mode. -Line: 1 Col: 124 End tag (h5) seen too early. Expected other end tag. -Line: 1 Col: 129 Unexpected end tag (h6) in table context caused voodoo mode. -Line: 1 Col: 129 End tag (h6) seen too early. Expected other end tag. -Line: 1 Col: 136 Unexpected end tag (body) in the table row phase. Ignored. -Line: 1 Col: 141 Unexpected end tag (br) in table context caused voodoo mode. -Line: 1 Col: 141 Unexpected end tag (br). Treated as br element. -Line: 1 Col: 145 Unexpected end tag (a) in table context caused voodoo mode. -Line: 1 Col: 145 End tag (a) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 151 Unexpected end tag (img) in table context caused voodoo mode. -Line: 1 Col: 151 This element (img) has no end tag. -Line: 1 Col: 159 Unexpected end tag (title) in table context caused voodoo mode. -Line: 1 Col: 159 Unexpected end tag (title). Ignored. -Line: 1 Col: 166 Unexpected end tag (span) in table context caused voodoo mode. -Line: 1 Col: 166 Unexpected end tag (span). Ignored. -Line: 1 Col: 174 Unexpected end tag (style) in table context caused voodoo mode. -Line: 1 Col: 174 Unexpected end tag (style). Ignored. -Line: 1 Col: 183 Unexpected end tag (script) in table context caused voodoo mode. -Line: 1 Col: 183 Unexpected end tag (script). Ignored. -Line: 1 Col: 196 Unexpected end tag (th). Ignored. -Line: 1 Col: 201 Unexpected end tag (td). Ignored. -Line: 1 Col: 206 Unexpected end tag (tr). Ignored. -Line: 1 Col: 214 This element (frame) has no end tag. -Line: 1 Col: 221 This element (area) has no end tag. -Line: 1 Col: 228 Unexpected end tag (link). Ignored. -Line: 1 Col: 236 This element (param) has no end tag. -Line: 1 Col: 241 This element (hr) has no end tag. -Line: 1 Col: 249 This element (input) has no end tag. -Line: 1 Col: 255 Unexpected end tag (col). Ignored. -Line: 1 Col: 262 Unexpected end tag (base). Ignored. -Line: 1 Col: 269 Unexpected end tag (meta). Ignored. -Line: 1 Col: 280 This element (basefont) has no end tag. -Line: 1 Col: 290 This element (bgsound) has no end tag. -Line: 1 Col: 298 This element (embed) has no end tag. -Line: 1 Col: 307 This element (spacer) has no end tag. -Line: 1 Col: 311 Unexpected end tag (p). Ignored. -Line: 1 Col: 316 End tag (dd) seen too early. Expected other end tag. -Line: 1 Col: 321 End tag (dt) seen too early. Expected other end tag. -Line: 1 Col: 331 Unexpected end tag (caption). Ignored. -Line: 1 Col: 342 Unexpected end tag (colgroup). Ignored. -Line: 1 Col: 350 Unexpected end tag (tbody). Ignored. -Line: 1 Col: 358 Unexpected end tag (tfoot). Ignored. -Line: 1 Col: 366 Unexpected end tag (thead). Ignored. -Line: 1 Col: 376 End tag (address) seen too early. Expected other end tag. -Line: 1 Col: 389 End tag (blockquote) seen too early. Expected other end tag. -Line: 1 Col: 398 End tag (center) seen too early. Expected other end tag. -Line: 1 Col: 404 Unexpected end tag (dir). Ignored. -Line: 1 Col: 410 End tag (div) seen too early. Expected other end tag. -Line: 1 Col: 415 End tag (dl) seen too early. Expected other end tag. -Line: 1 Col: 426 End tag (fieldset) seen too early. Expected other end tag. -Line: 1 Col: 436 End tag (listing) seen too early. Expected other end tag. -Line: 1 Col: 443 End tag (menu) seen too early. Expected other end tag. -Line: 1 Col: 448 End tag (ol) seen too early. Expected other end tag. -Line: 1 Col: 453 End tag (ul) seen too early. Expected other end tag. -Line: 1 Col: 458 End tag (li) seen too early. Expected other end tag. -Line: 1 Col: 465 End tag (nobr) violates step 1, paragraph 1 of the adoption agency algorithm. -Line: 1 Col: 471 This element (wbr) has no end tag. -Line: 1 Col: 487 End tag (button) seen too early. Expected other end tag. -Line: 1 Col: 497 End tag (marquee) seen too early. Expected other end tag. -Line: 1 Col: 506 End tag (object) seen too early. Expected other end tag. -Line: 1 Col: 524 Unexpected end tag (html). Ignored. -Line: 1 Col: 524 Unexpected end tag (frameset). Ignored. -Line: 1 Col: 531 Unexpected end tag (head). Ignored. -Line: 1 Col: 540 Unexpected end tag (iframe). Ignored. -Line: 1 Col: 548 This element (image) has no end tag. -Line: 1 Col: 558 This element (isindex) has no end tag. -Line: 1 Col: 568 Unexpected end tag (noembed). Ignored. -Line: 1 Col: 579 Unexpected end tag (noframes). Ignored. -Line: 1 Col: 590 Unexpected end tag (noscript). Ignored. -Line: 1 Col: 601 Unexpected end tag (optgroup). Ignored. -Line: 1 Col: 610 Unexpected end tag (option). Ignored. -Line: 1 Col: 622 Unexpected end tag (plaintext). Ignored. -Line: 1 Col: 633 Unexpected end tag (textarea). Ignored. -#document -| -| -| -|
-| -| -| -|

- -#data - -#errors -Line: 1 Col: 10 Unexpected start tag (frameset). Expected DOCTYPE. -Line: 1 Col: 10 Expected closing tag. Unexpected end of file. -#document -| -| -| diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests10.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests10.dat deleted file mode 100644 index 4f8df86f208..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests10.dat +++ /dev/null @@ -1,799 +0,0 @@ -#data - -#errors -#document -| -| -| -| -| - -#data -a -#errors -29: Bogus comment -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| - -#data - -#errors -35: Stray “svg” start tag. -42: Stray end tag “svg” -#document -| -| -| -| -| -#errors -43: Stray “svg” start tag. -50: Stray end tag “svg” -#document -| -| -| -| -|

-#errors -34: Start tag “svg” seen in “table”. -41: Stray end tag “svg”. -#document -| -| -| -| -| -| - -#data -
foo
-#errors -34: Start tag “svg” seen in “table”. -46: Stray end tag “g”. -53: Stray end tag “svg”. -#document -| -| -| -| -| -| -| "foo" -| - -#data -
foobar
-#errors -34: Start tag “svg” seen in “table”. -46: Stray end tag “g”. -58: Stray end tag “g”. -65: Stray end tag “svg”. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -| - -#data -
foobar
-#errors -41: Start tag “svg” seen in “table”. -53: Stray end tag “g”. -65: Stray end tag “g”. -72: Stray end tag “svg”. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -| -| - -#data -
foobar
-#errors -45: Start tag “svg” seen in “table”. -57: Stray end tag “g”. -69: Stray end tag “g”. -76: Stray end tag “svg”. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -| -| -| - -#data -
foobar
-#errors -#document -| -| -| -| -| -| -| -|
-| -| -| "foo" -| -| "bar" - -#data -
foobar

baz

-#errors -#document -| -| -| -| -| -| -| -|
-| -| -| "foo" -| -| "bar" -|

-| "baz" - -#data -
foobar

baz

-#errors -#document -| -| -| -| -| -|
-| -| -| "foo" -| -| "bar" -|

-| "baz" - -#data -
foobar

baz

quux -#errors -70: HTML start tag “p” in a foreign namespace context. -81: “table” closed but “caption” was still open. -#document -| -| -| -| -| -|
-| -| -| "foo" -| -| "bar" -|

-| "baz" -|

-| "quux" - -#data -
foobarbaz

quux -#errors -78: “table” closed but “caption” was still open. -78: Unclosed elements on stack. -#document -| -| -| -| -| -|
-| -| -| "foo" -| -| "bar" -| "baz" -|

-| "quux" - -#data -foobar

baz

quux -#errors -44: Start tag “svg” seen in “table”. -56: Stray end tag “g”. -68: Stray end tag “g”. -71: HTML start tag “p” in a foreign namespace context. -71: Start tag “p” seen in “table”. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -|

-| "baz" -| -| -|

-| "quux" - -#data -

quux -#errors -50: Stray “svg” start tag. -54: Stray “g” start tag. -62: Stray end tag “g” -66: Stray “g” start tag. -74: Stray end tag “g” -77: Stray “p” start tag. -88: “table” end tag with “select” open. -#document -| -| -| -| -| -| -| -|
-|

quux -#errors -36: Start tag “select” seen in “table”. -42: Stray “svg” start tag. -46: Stray “g” start tag. -54: Stray end tag “g” -58: Stray “g” start tag. -66: Stray end tag “g” -69: Stray “p” start tag. -80: “table” end tag with “select” open. -#document -| -| -| -| -| -|

-| "quux" - -#data -foobar

baz -#errors -41: Stray “svg” start tag. -68: HTML start tag “p” in a foreign namespace context. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -|

-| "baz" - -#data -foobar

baz -#errors -34: Stray “svg” start tag. -61: HTML start tag “p” in a foreign namespace context. -#document -| -| -| -| -| -| -| "foo" -| -| "bar" -|

-| "baz" - -#data -

-#errors -31: Stray “svg” start tag. -35: Stray “g” start tag. -40: Stray end tag “g” -44: Stray “g” start tag. -49: Stray end tag “g” -52: Stray “p” start tag. -58: Stray “span” start tag. -58: End of file seen and there were open elements. -#document -| -| -| -| - -#data -

-#errors -42: Stray “svg” start tag. -46: Stray “g” start tag. -51: Stray end tag “g” -55: Stray “g” start tag. -60: Stray end tag “g” -63: Stray “p” start tag. -69: Stray “span” start tag. -#document -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| xlink:href="foo" -| -| xlink href="foo" - -#data - -#errors -#document -| -| -| -| -| xlink:href="foo" -| xml:lang="en" -| -| -| xlink href="foo" -| xml lang="en" - -#data - -#errors -#document -| -| -| -| -| xlink:href="foo" -| xml:lang="en" -| -| -| xlink href="foo" -| xml lang="en" - -#data -bar -#errors -#document -| -| -| -| -| xlink:href="foo" -| xml:lang="en" -| -| -| xlink href="foo" -| xml lang="en" -| "bar" - -#data - -#errors -#document -| -| -| -| - -#data -

a -#errors -#document -| -| -| -|
-| -| "a" - -#data -
a -#errors -#document -| -| -| -|
-| -| -| "a" - -#data -
-#errors -#document -| -| -| -|
-| -| -| - -#data -
a -#errors -#document -| -| -| -|
-| -| -| -| -| "a" - -#data -

a -#errors -#document -| -| -| -|

-| -| -| -|

-| "a" - -#data -
    a -#errors -40: HTML start tag “ul” in a foreign namespace context. -41: End of file in a foreign namespace context. -#document -| -| -| -| -| -| -|
    -| -|
      -| "a" - -#data -
        a -#errors -35: HTML start tag “ul” in a foreign namespace context. -36: End of file in a foreign namespace context. -#document -| -| -| -| -| -| -| -|
          -| "a" - -#data -

          -#errors -#document -| -| -| -| -|

          -| -| -|

          - -#data -

          -#errors -#document -| -| -| -| -|

          -| -| -|

          - -#data -

          -#errors -#document -| -| -| -|

          -| -| -| -|

          -|

          - -#data -
          -#errors -#document -| -| -| -| -| -|
          -| -|
          -| -| - -#data -
          -#errors -#document -| -| -| -| -| -| -| -|
          -|
          -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data -

-#errors -#document -| -| -| -| -|
-| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| -| - -#data -
-#errors -#document -| -| -| -| -| -| -| -|
-| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| -| -| -| -| -| -| -| -| diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests11.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests11.dat deleted file mode 100644 index 638cde479f7..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests11.dat +++ /dev/null @@ -1,482 +0,0 @@ -#data - -#errors -#document -| -| -| -| -| -| attributeName="" -| attributeType="" -| baseFrequency="" -| baseProfile="" -| calcMode="" -| clipPathUnits="" -| contentScriptType="" -| contentStyleType="" -| diffuseConstant="" -| edgeMode="" -| externalResourcesRequired="" -| filterRes="" -| filterUnits="" -| glyphRef="" -| gradientTransform="" -| gradientUnits="" -| kernelMatrix="" -| kernelUnitLength="" -| keyPoints="" -| keySplines="" -| keyTimes="" -| lengthAdjust="" -| limitingConeAngle="" -| markerHeight="" -| markerUnits="" -| markerWidth="" -| maskContentUnits="" -| maskUnits="" -| numOctaves="" -| pathLength="" -| patternContentUnits="" -| patternTransform="" -| patternUnits="" -| pointsAtX="" -| pointsAtY="" -| pointsAtZ="" -| preserveAlpha="" -| preserveAspectRatio="" -| primitiveUnits="" -| refX="" -| refY="" -| repeatCount="" -| repeatDur="" -| requiredExtensions="" -| requiredFeatures="" -| specularConstant="" -| specularExponent="" -| spreadMethod="" -| startOffset="" -| stdDeviation="" -| stitchTiles="" -| surfaceScale="" -| systemLanguage="" -| tableValues="" -| targetX="" -| targetY="" -| textLength="" -| viewBox="" -| viewTarget="" -| xChannelSelector="" -| yChannelSelector="" -| zoomAndPan="" - -#data - -#errors -#document -| -| -| -| -| -| attributeName="" -| attributeType="" -| baseFrequency="" -| baseProfile="" -| calcMode="" -| clipPathUnits="" -| contentScriptType="" -| contentStyleType="" -| diffuseConstant="" -| edgeMode="" -| externalResourcesRequired="" -| filterRes="" -| filterUnits="" -| glyphRef="" -| gradientTransform="" -| gradientUnits="" -| kernelMatrix="" -| kernelUnitLength="" -| keyPoints="" -| keySplines="" -| keyTimes="" -| lengthAdjust="" -| limitingConeAngle="" -| markerHeight="" -| markerUnits="" -| markerWidth="" -| maskContentUnits="" -| maskUnits="" -| numOctaves="" -| pathLength="" -| patternContentUnits="" -| patternTransform="" -| patternUnits="" -| pointsAtX="" -| pointsAtY="" -| pointsAtZ="" -| preserveAlpha="" -| preserveAspectRatio="" -| primitiveUnits="" -| refX="" -| refY="" -| repeatCount="" -| repeatDur="" -| requiredExtensions="" -| requiredFeatures="" -| specularConstant="" -| specularExponent="" -| spreadMethod="" -| startOffset="" -| stdDeviation="" -| stitchTiles="" -| surfaceScale="" -| systemLanguage="" -| tableValues="" -| targetX="" -| targetY="" -| textLength="" -| viewBox="" -| viewTarget="" -| xChannelSelector="" -| yChannelSelector="" -| zoomAndPan="" - -#data - -#errors -#document -| -| -| -| -| -| attributeName="" -| attributeType="" -| baseFrequency="" -| baseProfile="" -| calcMode="" -| clipPathUnits="" -| contentScriptType="" -| contentStyleType="" -| diffuseConstant="" -| edgeMode="" -| externalResourcesRequired="" -| filterRes="" -| filterUnits="" -| glyphRef="" -| gradientTransform="" -| gradientUnits="" -| kernelMatrix="" -| kernelUnitLength="" -| keyPoints="" -| keySplines="" -| keyTimes="" -| lengthAdjust="" -| limitingConeAngle="" -| markerHeight="" -| markerUnits="" -| markerWidth="" -| maskContentUnits="" -| maskUnits="" -| numOctaves="" -| pathLength="" -| patternContentUnits="" -| patternTransform="" -| patternUnits="" -| pointsAtX="" -| pointsAtY="" -| pointsAtZ="" -| preserveAlpha="" -| preserveAspectRatio="" -| primitiveUnits="" -| refX="" -| refY="" -| repeatCount="" -| repeatDur="" -| requiredExtensions="" -| requiredFeatures="" -| specularConstant="" -| specularExponent="" -| spreadMethod="" -| startOffset="" -| stdDeviation="" -| stitchTiles="" -| surfaceScale="" -| systemLanguage="" -| tableValues="" -| targetX="" -| targetY="" -| textLength="" -| viewBox="" -| viewTarget="" -| xChannelSelector="" -| yChannelSelector="" -| zoomAndPan="" - -#data - -#errors -#document -| -| -| -| -| -| attributename="" -| attributetype="" -| basefrequency="" -| baseprofile="" -| calcmode="" -| clippathunits="" -| contentscripttype="" -| contentstyletype="" -| diffuseconstant="" -| edgemode="" -| externalresourcesrequired="" -| filterres="" -| filterunits="" -| glyphref="" -| gradienttransform="" -| gradientunits="" -| kernelmatrix="" -| kernelunitlength="" -| keypoints="" -| keysplines="" -| keytimes="" -| lengthadjust="" -| limitingconeangle="" -| markerheight="" -| markerunits="" -| markerwidth="" -| maskcontentunits="" -| maskunits="" -| numoctaves="" -| pathlength="" -| patterncontentunits="" -| patterntransform="" -| patternunits="" -| pointsatx="" -| pointsaty="" -| pointsatz="" -| preservealpha="" -| preserveaspectratio="" -| primitiveunits="" -| refx="" -| refy="" -| repeatcount="" -| repeatdur="" -| requiredextensions="" -| requiredfeatures="" -| specularconstant="" -| specularexponent="" -| spreadmethod="" -| startoffset="" -| stddeviation="" -| stitchtiles="" -| surfacescale="" -| systemlanguage="" -| tablevalues="" -| targetx="" -| targety="" -| textlength="" -| viewbox="" -| viewtarget="" -| xchannelselector="" -| ychannelselector="" -| zoomandpan="" - -#data - -#errors -#document -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests12.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests12.dat deleted file mode 100644 index 63107d277b6..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests12.dat +++ /dev/null @@ -1,62 +0,0 @@ -#data -

foobazeggs

spam

quuxbar -#errors -#document -| -| -| -| -|

-| "foo" -| -| -| -| "baz" -| -| -| -| -| "eggs" -| -| -|

-| "spam" -| -| -| -|
-| -| -| "quux" -| "bar" - -#data -foobazeggs

spam
quuxbar -#errors -#document -| -| -| -| -| "foo" -| -| -| -| "baz" -| -| -| -| -| "eggs" -| -| -|

-| "spam" -| -| -| -|
-| -| -| "quux" -| "bar" diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests14.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests14.dat deleted file mode 100644 index b8713f88582..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests14.dat +++ /dev/null @@ -1,74 +0,0 @@ -#data - -#errors -#document -| -| -| -| -| - -#data - -#errors -#document -| -| -| -| -| -| - -#data - -#errors -15: Unexpected start tag html -#document -| -| -| abc:def="gh" -| -| -| - -#data - -#errors -15: Unexpected start tag html -#document -| -| -| xml:lang="bar" -| -| - -#data - -#errors -#document -| -| -| 123="456" -| -| - -#data - -#errors -#document -| -| -| 123="456" -| 789="012" -| -| - -#data - -#errors -#document -| -| -| -| -| 789="012" diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests15.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests15.dat deleted file mode 100644 index 6ce1c0d1663..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests15.dat +++ /dev/null @@ -1,208 +0,0 @@ -#data -

X -#errors -Line: 1 Col: 31 Unexpected end tag (p). Ignored. -Line: 1 Col: 36 Expected closing tag. Unexpected end of file. -#document -| -| -| -| -|

-| -| -| -| -| -| -| " " -|

-| "X" - -#data -

-

X -#errors -Line: 1 Col: 3 Unexpected start tag (p). Expected DOCTYPE. -Line: 1 Col: 16 Unexpected end tag (p). Ignored. -Line: 2 Col: 4 Expected closing tag. Unexpected end of file. -#document -| -| -| -|

-| -| -| -| -| -| -| " -" -|

-| "X" - -#data - -#errors -Line: 1 Col: 22 Unexpected end tag (html) after the (implied) root element. -#document -| -| -| -| -| " " - -#data - -#errors -Line: 1 Col: 22 Unexpected end tag (body) after the (implied) root element. -#document -| -| -| -| -| - -#data - -#errors -Line: 1 Col: 6 Unexpected start tag (html). Expected DOCTYPE. -Line: 1 Col: 13 Unexpected end tag (html) after the (implied) root element. -#document -| -| -| -| - -#data -X -#errors -Line: 1 Col: 22 Unexpected end tag (body) after the (implied) root element. -#document -| -| -| -| -| -| "X" - -#data -<!doctype html><table> X<meta></table> -#errors -Line: 1 Col: 24 Unexpected non-space characters in table context caused voodoo mode. -Line: 1 Col: 30 Unexpected start tag (meta) in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| " X" -| <meta> -| <table> - -#data -<!doctype html><table> x</table> -#errors -Line: 1 Col: 24 Unexpected non-space characters in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| " x" -| <table> - -#data -<!doctype html><table> x </table> -#errors -Line: 1 Col: 25 Unexpected non-space characters in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| " x " -| <table> - -#data -<!doctype html><table><tr> x</table> -#errors -Line: 1 Col: 28 Unexpected non-space characters in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| " x" -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><table>X<style> <tr>x </style> </table> -#errors -Line: 1 Col: 23 Unexpected non-space characters in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "X" -| <table> -| <style> -| " <tr>x " -| " " - -#data -<!doctype html><div><table><a>foo</a> <tr><td>bar</td> </tr></table></div> -#errors -Line: 1 Col: 30 Unexpected start tag (a) in table context caused voodoo mode. -Line: 1 Col: 37 Unexpected end tag (a) in table context caused voodoo mode. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <div> -| <a> -| "foo" -| <table> -| " " -| <tbody> -| <tr> -| <td> -| "bar" -| " " - -#data -<frame></frame></frame><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes> -#errors -6: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”. -13: Stray start tag “frame”. -21: Stray end tag “frame”. -29: Stray end tag “frame”. -39: “frameset” start tag after “body” already open. -105: End of file seen inside an [R]CDATA element. -105: End of file seen and there were open elements. -XXX: These errors are wrong, please fix me! -#document -| <html> -| <head> -| <frameset> -| <frame> -| <frameset> -| <frame> -| <noframes> -| "</frameset><noframes>" - -#data -<!DOCTYPE html><object></html> -#errors -1: Expected closing tag. Unexpected end of file -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <object> diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests16.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests16.dat deleted file mode 100644 index c8ef66f0e6e..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests16.dat +++ /dev/null @@ -1,2299 +0,0 @@ -#data -<!doctype html><script> -#errors -Line: 1 Col: 23 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| <body> - -#data -<!doctype html><script>a -#errors -Line: 1 Col: 24 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "a" -| <body> - -#data -<!doctype html><script>< -#errors -Line: 1 Col: 24 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<" -| <body> - -#data -<!doctype html><script></ -#errors -Line: 1 Col: 25 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</" -| <body> - -#data -<!doctype html><script></S -#errors -Line: 1 Col: 26 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</S" -| <body> - -#data -<!doctype html><script></SC -#errors -Line: 1 Col: 27 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</SC" -| <body> - -#data -<!doctype html><script></SCR -#errors -Line: 1 Col: 28 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</SCR" -| <body> - -#data -<!doctype html><script></SCRI -#errors -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</SCRI" -| <body> - -#data -<!doctype html><script></SCRIP -#errors -Line: 1 Col: 30 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</SCRIP" -| <body> - -#data -<!doctype html><script></SCRIPT -#errors -Line: 1 Col: 31 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</SCRIPT" -| <body> - -#data -<!doctype html><script></SCRIPT -#errors -Line: 1 Col: 32 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| <body> - -#data -<!doctype html><script></s -#errors -Line: 1 Col: 26 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</s" -| <body> - -#data -<!doctype html><script></sc -#errors -Line: 1 Col: 27 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</sc" -| <body> - -#data -<!doctype html><script></scr -#errors -Line: 1 Col: 28 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</scr" -| <body> - -#data -<!doctype html><script></scri -#errors -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</scri" -| <body> - -#data -<!doctype html><script></scrip -#errors -Line: 1 Col: 30 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</scrip" -| <body> - -#data -<!doctype html><script></script -#errors -Line: 1 Col: 31 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "</script" -| <body> - -#data -<!doctype html><script></script -#errors -Line: 1 Col: 32 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| <body> - -#data -<!doctype html><script><! -#errors -Line: 1 Col: 25 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!" -| <body> - -#data -<!doctype html><script><!a -#errors -Line: 1 Col: 26 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!a" -| <body> - -#data -<!doctype html><script><!- -#errors -Line: 1 Col: 26 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!-" -| <body> - -#data -<!doctype html><script><!-a -#errors -Line: 1 Col: 27 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!-a" -| <body> - -#data -<!doctype html><script><!-- -#errors -Line: 1 Col: 27 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--" -| <body> - -#data -<!doctype html><script><!--a -#errors -Line: 1 Col: 28 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--a" -| <body> - -#data -<!doctype html><script><!--< -#errors -Line: 1 Col: 28 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<" -| <body> - -#data -<!doctype html><script><!--<a -#errors -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<a" -| <body> - -#data -<!doctype html><script><!--</ -#errors -Line: 1 Col: 27 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--</" -| <body> - -#data -<!doctype html><script><!--</script -#errors -Line: 1 Col: 35 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--</script" -| <body> - -#data -<!doctype html><script><!--</script -#errors -Line: 1 Col: 36 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--" -| <body> - -#data -<!doctype html><script><!--<s -#errors -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<s" -| <body> - -#data -<!doctype html><script><!--<script -#errors -Line: 1 Col: 34 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script" -| <body> - -#data -<!doctype html><script><!--<script -#errors -Line: 1 Col: 35 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script " -| <body> - -#data -<!doctype html><script><!--<script < -#errors -Line: 1 Col: 36 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script <" -| <body> - -#data -<!doctype html><script><!--<script <a -#errors -Line: 1 Col: 37 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script <a" -| <body> - -#data -<!doctype html><script><!--<script </ -#errors -Line: 1 Col: 37 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </" -| <body> - -#data -<!doctype html><script><!--<script </s -#errors -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </s" -| <body> - -#data -<!doctype html><script><!--<script </script -#errors -Line: 1 Col: 43 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script" -| <body> - -#data -<!doctype html><script><!--<script </scripta -#errors -Line: 1 Col: 44 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </scripta" -| <body> - -#data -<!doctype html><script><!--<script </script -#errors -Line: 1 Col: 44 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<!doctype html><script><!--<script </script> -#errors -Line: 1 Col: 44 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script>" -| <body> - -#data -<!doctype html><script><!--<script </script/ -#errors -Line: 1 Col: 44 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script/" -| <body> - -#data -<!doctype html><script><!--<script </script < -#errors -Line: 1 Col: 45 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script <" -| <body> - -#data -<!doctype html><script><!--<script </script <a -#errors -Line: 1 Col: 46 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script <a" -| <body> - -#data -<!doctype html><script><!--<script </script </ -#errors -Line: 1 Col: 46 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script </" -| <body> - -#data -<!doctype html><script><!--<script </script </script -#errors -Line: 1 Col: 52 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script </script" -| <body> - -#data -<!doctype html><script><!--<script </script </script -#errors -Line: 1 Col: 53 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<!doctype html><script><!--<script </script </script/ -#errors -Line: 1 Col: 53 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<!doctype html><script><!--<script </script </script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<!doctype html><script><!--<script - -#errors -Line: 1 Col: 36 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -" -| <body> - -#data -<!doctype html><script><!--<script -a -#errors -Line: 1 Col: 37 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -a" -| <body> - -#data -<!doctype html><script><!--<script -< -#errors -Line: 1 Col: 37 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -<" -| <body> - -#data -<!doctype html><script><!--<script -- -#errors -Line: 1 Col: 37 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --" -| <body> - -#data -<!doctype html><script><!--<script --a -#errors -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --a" -| <body> - -#data -<!doctype html><script><!--<script --< -#errors -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --<" -| <body> - -#data -<!doctype html><script><!--<script --> -#errors -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<!doctype html><script><!--<script -->< -#errors -Line: 1 Col: 39 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --><" -| <body> - -#data -<!doctype html><script><!--<script --></ -#errors -Line: 1 Col: 40 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --></" -| <body> - -#data -<!doctype html><script><!--<script --></script -#errors -Line: 1 Col: 46 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script --></script" -| <body> - -#data -<!doctype html><script><!--<script --></script -#errors -Line: 1 Col: 47 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<!doctype html><script><!--<script --></script/ -#errors -Line: 1 Col: 47 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<!doctype html><script><!--<script --></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<!doctype html><script><!--<script><\/script>--></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script><\/script>-->" -| <body> - -#data -<!doctype html><script><!--<script></scr'+'ipt>--></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></scr'+'ipt>-->" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script>--><!--</script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>--><!--" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script>-- ></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>-- >" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script>- -></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>- ->" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script>- - ></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>- - >" -| <body> - -#data -<!doctype html><script><!--<script></script><script></script>-></script> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>->" -| <body> - -#data -<!doctype html><script><!--<script>--!></script>X -#errors -Line: 1 Col: 49 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script>--!></script>X" -| <body> - -#data -<!doctype html><script><!--<scr'+'ipt></script>--></script> -#errors -Line: 1 Col: 59 Unexpected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<scr'+'ipt>" -| <body> -| "-->" - -#data -<!doctype html><script><!--<script></scr'+'ipt></script>X -#errors -Line: 1 Col: 57 Unexpected end of file. Expected end tag (script). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| "<!--<script></scr'+'ipt></script>X" -| <body> - -#data -<!doctype html><style><!--<style></style>--></style> -#errors -Line: 1 Col: 52 Unexpected end tag (style). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--<style>" -| <body> -| "-->" - -#data -<!doctype html><style><!--</style>X -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--" -| <body> -| "X" - -#data -<!doctype html><style><!--...</style>...--></style> -#errors -Line: 1 Col: 51 Unexpected end tag (style). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--..." -| <body> -| "...-->" - -#data -<!doctype html><style><!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style></style>X -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style>" -| <body> -| "X" - -#data -<!doctype html><style><!--...<style><!--...--!></style>--></style> -#errors -Line: 1 Col: 66 Unexpected end tag (style). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--...<style><!--...--!>" -| <body> -| "-->" - -#data -<!doctype html><style><!--...</style><!-- --><style>@import ...</style> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "<!--..." -| <!-- --> -| <style> -| "@import ..." -| <body> - -#data -<!doctype html><style>...<style><!--...</style><!-- --></style> -#errors -Line: 1 Col: 63 Unexpected end tag (style). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "...<style><!--..." -| <!-- --> -| <body> - -#data -<!doctype html><style>...<!--[if IE]><style>...</style>X -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <style> -| "...<!--[if IE]><style>..." -| <body> -| "X" - -#data -<!doctype html><title><!--<title>--> -#errors -Line: 1 Col: 52 Unexpected end tag (title). -#document -| -| -| -| -| "<!--<title>" -| <body> -| "-->" - -#data -<!doctype html><title></title> -#errors -#document -| -| -| -| -| "" -| - -#data -foo/title><link></head><body>X -#errors -Line: 1 Col: 52 Unexpected end of file. Expected end tag (title). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <title> -| "foo/title><link></head><body>X" -| <body> - -#data -<!doctype html><noscript><!--<noscript></noscript>--></noscript> -#errors -Line: 1 Col: 64 Unexpected end tag (noscript). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <noscript> -| "<!--<noscript>" -| <body> -| "-->" - -#data -<!doctype html><noscript><!--</noscript>X<noscript>--></noscript> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <noscript> -| "<!--" -| <body> -| "X" -| <noscript> -| "-->" - -#data -<!doctype html><noscript><iframe></noscript>X -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <noscript> -| "<iframe>" -| <body> -| "X" - -#data -<!doctype html><noframes><!--<noframes></noframes>--></noframes> -#errors -Line: 1 Col: 64 Unexpected end tag (noframes). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <noframes> -| "<!--<noframes>" -| <body> -| "-->" - -#data -<!doctype html><noframes><body><script><!--...</script></body></noframes></html> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <noframes> -| "<body><script><!--...</script></body>" -| <body> - -#data -<!doctype html><textarea><!--<textarea></textarea>--></textarea> -#errors -Line: 1 Col: 64 Unexpected end tag (textarea). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <textarea> -| "<!--<textarea>" -| "-->" - -#data -<!doctype html><textarea></textarea></textarea> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <textarea> -| "</textarea>" - -#data -<!doctype html><textarea><</textarea> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <textarea> -| "<" - -#data -<!doctype html><textarea>a<b</textarea> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <textarea> -| "a<b" - -#data -<!doctype html><iframe><!--<iframe></iframe>--></iframe> -#errors -Line: 1 Col: 56 Unexpected end tag (iframe). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <iframe> -| "<!--<iframe>" -| "-->" - -#data -<!doctype html><iframe>...<!--X->...<!--/X->...</iframe> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <iframe> -| "...<!--X->...<!--/X->..." - -#data -<!doctype html><xmp><!--<xmp></xmp>--></xmp> -#errors -Line: 1 Col: 44 Unexpected end tag (xmp). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <xmp> -| "<!--<xmp>" -| "-->" - -#data -<!doctype html><noembed><!--<noembed></noembed>--></noembed> -#errors -Line: 1 Col: 60 Unexpected end tag (noembed). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <noembed> -| "<!--<noembed>" -| "-->" - -#data -<script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 8 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| <body> - -#data -<script>a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 9 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "a" -| <body> - -#data -<script>< -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 9 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<" -| <body> - -#data -<script></ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 10 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</" -| <body> - -#data -<script></S -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</S" -| <body> - -#data -<script></SC -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 12 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</SC" -| <body> - -#data -<script></SCR -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 13 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</SCR" -| <body> - -#data -<script></SCRI -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 14 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</SCRI" -| <body> - -#data -<script></SCRIP -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 15 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</SCRIP" -| <body> - -#data -<script></SCRIPT -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 16 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</SCRIPT" -| <body> - -#data -<script></SCRIPT -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 17 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| <body> - -#data -<script></s -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</s" -| <body> - -#data -<script></sc -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 12 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</sc" -| <body> - -#data -<script></scr -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 13 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</scr" -| <body> - -#data -<script></scri -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 14 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</scri" -| <body> - -#data -<script></scrip -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 15 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</scrip" -| <body> - -#data -<script></script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 16 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</script" -| <body> - -#data -<script></script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 17 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| <body> - -#data -<script><! -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 10 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!" -| <body> - -#data -<script><!a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!a" -| <body> - -#data -<script><!- -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!-" -| <body> - -#data -<script><!-a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 12 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!-a" -| <body> - -#data -<script><!-- -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 12 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--" -| <body> - -#data -<script><!--a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 13 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--a" -| <body> - -#data -<script><!--< -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 13 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<" -| <body> - -#data -<script><!--<a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 14 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<a" -| <body> - -#data -<script><!--</ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 14 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--</" -| <body> - -#data -<script><!--</script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 20 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--</script" -| <body> - -#data -<script><!--</script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 21 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--" -| <body> - -#data -<script><!--<s -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 14 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<s" -| <body> - -#data -<script><!--<script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 19 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script" -| <body> - -#data -<script><!--<script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 20 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script " -| <body> - -#data -<script><!--<script < -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 21 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script <" -| <body> - -#data -<script><!--<script <a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 22 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script <a" -| <body> - -#data -<script><!--<script </ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 22 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </" -| <body> - -#data -<script><!--<script </s -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 23 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </s" -| <body> - -#data -<script><!--<script </script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 28 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script" -| <body> - -#data -<script><!--<script </scripta -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </scripta" -| <body> - -#data -<script><!--<script </script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<script><!--<script </script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script>" -| <body> - -#data -<script><!--<script </script/ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 29 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script/" -| <body> - -#data -<script><!--<script </script < -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 30 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script <" -| <body> - -#data -<script><!--<script </script <a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 31 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script <a" -| <body> - -#data -<script><!--<script </script </ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 31 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script </" -| <body> - -#data -<script><!--<script </script </script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script </script" -| <body> - -#data -<script><!--<script </script </script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<script><!--<script </script </script/ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 38 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<script><!--<script </script </script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script </script " -| <body> - -#data -<script><!--<script - -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 21 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script -" -| <body> - -#data -<script><!--<script -a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 22 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script -a" -| <body> - -#data -<script><!--<script -- -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 22 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script --" -| <body> - -#data -<script><!--<script --a -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 23 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script --a" -| <body> - -#data -<script><!--<script --> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 23 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<script><!--<script -->< -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 24 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script --><" -| <body> - -#data -<script><!--<script --></ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 25 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script --></" -| <body> - -#data -<script><!--<script --></script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 31 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script --></script" -| <body> - -#data -<script><!--<script --></script -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 32 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<script><!--<script --></script/ -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 32 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<script><!--<script --></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script -->" -| <body> - -#data -<script><!--<script><\/script>--></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script><\/script>-->" -| <body> - -#data -<script><!--<script></scr'+'ipt>--></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></scr'+'ipt>-->" -| <body> - -#data -<script><!--<script></script><script></script></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>" -| <body> - -#data -<script><!--<script></script><script></script>--><!--</script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>--><!--" -| <body> - -#data -<script><!--<script></script><script></script>-- ></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>-- >" -| <body> - -#data -<script><!--<script></script><script></script>- -></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>- ->" -| <body> - -#data -<script><!--<script></script><script></script>- - ></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>- - >" -| <body> - -#data -<script><!--<script></script><script></script>-></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -#document -| <html> -| <head> -| <script> -| "<!--<script></script><script></script>->" -| <body> - -#data -<script><!--<script>--!></script>X -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 34 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script>--!></script>X" -| <body> - -#data -<script><!--<scr'+'ipt></script>--></script> -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 44 Unexpected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<scr'+'ipt>" -| <body> -| "-->" - -#data -<script><!--<script></scr'+'ipt></script>X -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 42 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "<!--<script></scr'+'ipt></script>X" -| <body> - -#data -<style><!--<style></style>--></style> -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -Line: 1 Col: 37 Unexpected end tag (style). -#document -| <html> -| <head> -| <style> -| "<!--<style>" -| <body> -| "-->" - -#data -<style><!--</style>X -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| <html> -| <head> -| <style> -| "<!--" -| <body> -| "X" - -#data -<style><!--...</style>...--></style> -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -Line: 1 Col: 36 Unexpected end tag (style). -#document -| <html> -| <head> -| <style> -| "<!--..." -| <body> -| "...-->" - -#data -<style><!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style></style>X -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| <html> -| <head> -| <style> -| "<!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style>" -| <body> -| "X" - -#data -<style><!--...<style><!--...--!></style>--></style> -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -Line: 1 Col: 51 Unexpected end tag (style). -#document -| <html> -| <head> -| <style> -| "<!--...<style><!--...--!>" -| <body> -| "-->" - -#data -<style><!--...</style><!-- --><style>@import ...</style> -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| <html> -| <head> -| <style> -| "<!--..." -| <!-- --> -| <style> -| "@import ..." -| <body> - -#data -<style>...<style><!--...</style><!-- --></style> -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -Line: 1 Col: 48 Unexpected end tag (style). -#document -| <html> -| <head> -| <style> -| "...<style><!--..." -| <!-- --> -| <body> - -#data -<style>...<!--[if IE]><style>...</style>X -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| <html> -| <head> -| <style> -| "...<!--[if IE]><style>..." -| <body> -| "X" - -#data -<title><!--<title>--> -#errors -Line: 1 Col: 7 Unexpected start tag (title). Expected DOCTYPE. -Line: 1 Col: 37 Unexpected end tag (title). -#document -| -| -| -| "<!--<title>" -| <body> -| "-->" - -#data -<title></title> -#errors -Line: 1 Col: 7 Unexpected start tag (title). Expected DOCTYPE. -#document -| -| -| -| "" -| - -#data -foo/title><link></head><body>X -#errors -Line: 1 Col: 7 Unexpected start tag (title). Expected DOCTYPE. -Line: 1 Col: 37 Unexpected end of file. Expected end tag (title). -#document -| <html> -| <head> -| <title> -| "foo/title><link></head><body>X" -| <body> - -#data -<noscript><!--<noscript></noscript>--></noscript> -#errors -Line: 1 Col: 10 Unexpected start tag (noscript). Expected DOCTYPE. -Line: 1 Col: 49 Unexpected end tag (noscript). -#document -| <html> -| <head> -| <noscript> -| "<!--<noscript>" -| <body> -| "-->" - -#data -<noscript><!--</noscript>X<noscript>--></noscript> -#errors -Line: 1 Col: 10 Unexpected start tag (noscript). Expected DOCTYPE. -#document -| <html> -| <head> -| <noscript> -| "<!--" -| <body> -| "X" -| <noscript> -| "-->" - -#data -<noscript><iframe></noscript>X -#errors -Line: 1 Col: 10 Unexpected start tag (noscript). Expected DOCTYPE. -#document -| <html> -| <head> -| <noscript> -| "<iframe>" -| <body> -| "X" - -#data -<noframes><!--<noframes></noframes>--></noframes> -#errors -Line: 1 Col: 10 Unexpected start tag (noframes). Expected DOCTYPE. -Line: 1 Col: 49 Unexpected end tag (noframes). -#document -| <html> -| <head> -| <noframes> -| "<!--<noframes>" -| <body> -| "-->" - -#data -<noframes><body><script><!--...</script></body></noframes></html> -#errors -Line: 1 Col: 10 Unexpected start tag (noframes). Expected DOCTYPE. -#document -| <html> -| <head> -| <noframes> -| "<body><script><!--...</script></body>" -| <body> - -#data -<textarea><!--<textarea></textarea>--></textarea> -#errors -Line: 1 Col: 10 Unexpected start tag (textarea). Expected DOCTYPE. -Line: 1 Col: 49 Unexpected end tag (textarea). -#document -| <html> -| <head> -| <body> -| <textarea> -| "<!--<textarea>" -| "-->" - -#data -<textarea></textarea></textarea> -#errors -Line: 1 Col: 10 Unexpected start tag (textarea). Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| <textarea> -| "</textarea>" - -#data -<iframe><!--<iframe></iframe>--></iframe> -#errors -Line: 1 Col: 8 Unexpected start tag (iframe). Expected DOCTYPE. -Line: 1 Col: 41 Unexpected end tag (iframe). -#document -| <html> -| <head> -| <body> -| <iframe> -| "<!--<iframe>" -| "-->" - -#data -<iframe>...<!--X->...<!--/X->...</iframe> -#errors -Line: 1 Col: 8 Unexpected start tag (iframe). Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| <iframe> -| "...<!--X->...<!--/X->..." - -#data -<xmp><!--<xmp></xmp>--></xmp> -#errors -Line: 1 Col: 5 Unexpected start tag (xmp). Expected DOCTYPE. -Line: 1 Col: 29 Unexpected end tag (xmp). -#document -| <html> -| <head> -| <body> -| <xmp> -| "<!--<xmp>" -| "-->" - -#data -<noembed><!--<noembed></noembed>--></noembed> -#errors -Line: 1 Col: 9 Unexpected start tag (noembed). Expected DOCTYPE. -Line: 1 Col: 45 Unexpected end tag (noembed). -#document -| <html> -| <head> -| <body> -| <noembed> -| "<!--<noembed>" -| "-->" - -#data -<!doctype html><table> - -#errors -Line 2 Col 0 Unexpected end of file. Expected table content. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| " -" - -#data -<!doctype html><table><td><span><font></span><span> -#errors -Line 1 Col 26 Unexpected table cell start tag (td) in the table body phase. -Line 1 Col 45 Unexpected end tag (span). -Line 1 Col 51 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <span> -| <font> -| <font> -| <span> - -#data -<!doctype html><form><table></form><form></table></form> -#errors -35: Stray end tag “form”. -41: Start tag “form” seen in “table”. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> -| <table> -| <form> diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests17.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests17.dat deleted file mode 100644 index 7b555f888de..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests17.dat +++ /dev/null @@ -1,153 +0,0 @@ -#data -<!doctype html><table><tbody><select><tr> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><table><tr><select><td> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <table> -| <tbody> -| <tr> -| <td> - -#data -<!doctype html><table><tr><td><select><td> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <select> -| <td> - -#data -<!doctype html><table><tr><th><select><td> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <th> -| <select> -| <td> - -#data -<!doctype html><table><caption><select><tr> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <caption> -| <select> -| <tbody> -| <tr> - -#data -<!doctype html><select><tr> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><td> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><th> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><tbody> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><thead> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><tfoot> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><select><caption> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><table><tr></table>a -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| "a" diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests18.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests18.dat deleted file mode 100644 index 680e1f068a6..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests18.dat +++ /dev/null @@ -1,269 +0,0 @@ -#data -<!doctype html><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" - -#data -<!doctype html><table><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" -| <table> - -#data -<!doctype html><table><tbody><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" -| <table> -| <tbody> - -#data -<!doctype html><table><tbody><tr><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><table><tbody><tr><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><table><td><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <plaintext> -| "</plaintext>" - -#data -<!doctype html><table><caption><plaintext></plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <caption> -| <plaintext> -| "</plaintext>" - -#data -<!doctype html><table><tr><style></script></style>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "abc" -| <table> -| <tbody> -| <tr> -| <style> -| "</script>" - -#data -<!doctype html><table><tr><script></style></script>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "abc" -| <table> -| <tbody> -| <tr> -| <script> -| "</style>" - -#data -<!doctype html><table><caption><style></script></style>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <caption> -| <style> -| "</script>" -| "abc" - -#data -<!doctype html><table><td><style></script></style>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <style> -| "</script>" -| "abc" - -#data -<!doctype html><select><script></style></script>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <script> -| "</style>" -| "abc" - -#data -<!doctype html><table><select><script></style></script>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <script> -| "</style>" -| "abc" -| <table> - -#data -<!doctype html><table><tr><select><script></style></script>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <script> -| "</style>" -| "abc" -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><frameset></frameset><noframes>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <noframes> -| "abc" - -#data -<!doctype html><frameset></frameset><noframes>abc</noframes><!--abc--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <noframes> -| "abc" -| <!-- abc --> - -#data -<!doctype html><frameset></frameset></html><noframes>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <noframes> -| "abc" - -#data -<!doctype html><frameset></frameset></html><noframes>abc</noframes><!--abc--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <noframes> -| "abc" -| <!-- abc --> - -#data -<!doctype html><table><tr></tbody><tfoot> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <tfoot> - -#data -<!doctype html><table><td><svg></svg>abc<td> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <svg svg> -| "abc" -| <td> diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests19.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests19.dat deleted file mode 100644 index 0d62f5a5b02..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests19.dat +++ /dev/null @@ -1,1237 +0,0 @@ -#data -<!doctype html><math><mn DefinitionUrl="foo"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <math math> -| <math mn> -| definitionURL="foo" - -#data -<!doctype html><html></p><!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <!-- foo --> -| <head> -| <body> - -#data -<!doctype html><head></head></p><!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <!-- foo --> -| <body> - -#data -<!doctype html><body><p><pre> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <pre> - -#data -<!doctype html><body><p><listing> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <listing> - -#data -<!doctype html><p><plaintext> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <plaintext> - -#data -<!doctype html><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <h1> - -#data -<!doctype html><form><isindex> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> - -#data -<!doctype html><isindex action="POST"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> -| action="POST" -| <hr> -| <label> -| "This is a searchable index. Enter search keywords: " -| <input> -| name="isindex" -| <hr> - -#data -<!doctype html><isindex prompt="this is isindex"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> -| <hr> -| <label> -| "this is isindex" -| <input> -| name="isindex" -| <hr> - -#data -<!doctype html><isindex type="hidden"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> -| <hr> -| <label> -| "This is a searchable index. Enter search keywords: " -| <input> -| name="isindex" -| type="hidden" -| <hr> - -#data -<!doctype html><isindex name="foo"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <form> -| <hr> -| <label> -| "This is a searchable index. Enter search keywords: " -| <input> -| name="isindex" -| <hr> - -#data -<!doctype html><ruby><p><rp> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <p> -| <rp> - -#data -<!doctype html><ruby><div><span><rp> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <div> -| <span> -| <rp> - -#data -<!doctype html><ruby><div><p><rp> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <div> -| <p> -| <rp> - -#data -<!doctype html><ruby><p><rt> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <p> -| <rt> - -#data -<!doctype html><ruby><div><span><rt> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <div> -| <span> -| <rt> - -#data -<!doctype html><ruby><div><p><rt> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <ruby> -| <div> -| <p> -| <rt> - -#data -<!doctype html><math/><foo> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <math math> -| <foo> - -#data -<!doctype html><svg/><foo> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <svg svg> -| <foo> - -#data -<!doctype html><div></body><!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <div> -| <!-- foo --> - -#data -<!doctype html><h1><div><h3><span></h1>foo -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <h1> -| <div> -| <h3> -| <span> -| "foo" - -#data -<!doctype html><p></h3>foo -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| "foo" - -#data -<!doctype html><h3><li>abc</h2>foo -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <h3> -| <li> -| "abc" -| "foo" - -#data -<!doctype html><table>abc<!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "abc" -| <table> -| <!-- foo --> - -#data -<!doctype html><table> <!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| " " -| <!-- foo --> - -#data -<!doctype html><table> b <!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| " b " -| <table> -| <!-- foo --> - -#data -<!doctype html><select><option><option> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <option> -| <option> - -#data -<!doctype html><select><option></optgroup> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <option> - -#data -<!doctype html><select><option></optgroup> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <option> - -#data -<!doctype html><p><math><mi><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math mi> -| <p> -| <h1> - -#data -<!doctype html><p><math><mo><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math mo> -| <p> -| <h1> - -#data -<!doctype html><p><math><mn><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math mn> -| <p> -| <h1> - -#data -<!doctype html><p><math><ms><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math ms> -| <p> -| <h1> - -#data -<!doctype html><p><math><mtext><p><h1> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math mtext> -| <p> -| <h1> - -#data -<!doctype html><frameset></noframes> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!doctype html><html c=d><body></html><html a=b> -#errors -#document -| <!DOCTYPE html> -| <html> -| a="b" -| c="d" -| <head> -| <body> - -#data -<!doctype html><html c=d><frameset></frameset></html><html a=b> -#errors -#document -| <!DOCTYPE html> -| <html> -| a="b" -| c="d" -| <head> -| <frameset> - -#data -<!doctype html><html><frameset></frameset></html><!--foo--> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <!-- foo --> - -#data -<!doctype html><html><frameset></frameset></html> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| " " - -#data -<!doctype html><html><frameset></frameset></html>abc -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!doctype html><html><frameset></frameset></html><p> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!doctype html><html><frameset></frameset></html></p> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<html><frameset></frameset></html><!doctype html> -#errors -#document -| <html> -| <head> -| <frameset> - -#data -<!doctype html><body><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> - -#data -<!doctype html><p><frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<!doctype html><p>a<frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| "a" - -#data -<!doctype html><p> <frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<!doctype html><pre><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <pre> - -#data -<!doctype html><listing><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <listing> - -#data -<!doctype html><li><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <li> - -#data -<!doctype html><dd><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <dd> - -#data -<!doctype html><dt><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <dt> - -#data -<!doctype html><button><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <button> - -#data -<!doctype html><applet><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <applet> - -#data -<!doctype html><marquee><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <marquee> - -#data -<!doctype html><object><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <object> - -#data -<!doctype html><table><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> - -#data -<!doctype html><area><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <area> - -#data -<!doctype html><basefont><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <basefont> -| <frameset> - -#data -<!doctype html><bgsound><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <bgsound> -| <frameset> - -#data -<!doctype html><br><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <br> - -#data -<!doctype html><embed><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <embed> - -#data -<!doctype html><img><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <img> - -#data -<!doctype html><input><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <input> - -#data -<!doctype html><keygen><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <keygen> - -#data -<!doctype html><wbr><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <wbr> - -#data -<!doctype html><hr><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <hr> - -#data -<!doctype html><textarea></textarea><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <textarea> - -#data -<!doctype html><xmp></xmp><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <xmp> - -#data -<!doctype html><iframe></iframe><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <iframe> - -#data -<!doctype html><select></select><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> - -#data -<!doctype html><svg></svg><frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<!doctype html><math></math><frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<!doctype html><svg><foreignObject><div> <frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<!doctype html><svg>a</svg><frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <svg svg> -| "a" - -#data -<!doctype html><svg> </svg><frameset><frame> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> -| <frame> - -#data -<html>aaa<frameset></frameset> -#errors -#document -| <html> -| <head> -| <body> -| "aaa" - -#data -<html> a <frameset></frameset> -#errors -#document -| <html> -| <head> -| <body> -| "a " - -#data -<!doctype html><div><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!doctype html><div><body><frameset> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <div> - -#data -<!doctype html><p><math></p>a -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| "a" - -#data -<!doctype html><p><math><mn><span></p>a -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <math math> -| <math mn> -| <span> -| <p> -| "a" - -#data -<!doctype html><math></html> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <math math> - -#data -<!doctype html><meta charset="ascii"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <meta> -| charset="ascii" -| <body> - -#data -<!doctype html><meta http-equiv="content-type" content="text/html;charset=ascii"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <meta> -| content="text/html;charset=ascii" -| http-equiv="content-type" -| <body> - -#data -<!doctype html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset="utf8"> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <!-- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --> -| <meta> -| charset="utf8" -| <body> - -#data -<!doctype html><html a=b><head></head><html c=d> -#errors -#document -| <!DOCTYPE html> -| <html> -| a="b" -| c="d" -| <head> -| <body> - -#data -<!doctype html><image/> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <img> - -#data -<!doctype html>a<i>b<table>c<b>d</i>e</b>f -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "a" -| <i> -| "bc" -| <b> -| "de" -| "f" -| <table> - -#data -<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <b> -| "b" -| <b> -| <div> -| <b> -| <i> -| "c" -| <a> -| "d" -| <a> -| "e" -| <a> -| "f" -| <table> - -#data -<!doctype html><i>a<b>b<div>c<a>d</i>e</b>f -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <b> -| "b" -| <b> -| <div> -| <b> -| <i> -| "c" -| <a> -| "d" -| <a> -| "e" -| <a> -| "f" - -#data -<!doctype html><table><i>a<b>b<div>c</i> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <b> -| "b" -| <b> -| <div> -| <i> -| "c" -| <table> - -#data -<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <b> -| "b" -| <b> -| <div> -| <b> -| <i> -| "c" -| <a> -| "d" -| <a> -| "e" -| <a> -| "f" -| <table> - -#data -<!doctype html><table><i>a<div>b<tr>c<b>d</i>e -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <div> -| "b" -| <i> -| "c" -| <b> -| "d" -| <b> -| "e" -| <table> -| <tbody> -| <tr> - -#data -<!doctype html><table><td><table><i>a<div>b<b>c</i>d -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| <i> -| "a" -| <div> -| <i> -| "b" -| <b> -| "c" -| <b> -| "d" -| <table> - -#data -<!doctype html><body><bgsound> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <bgsound> - -#data -<!doctype html><body><basefont> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <basefont> - -#data -<!doctype html><a><b></a><basefont> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <a> -| <b> -| <basefont> - -#data -<!doctype html><a><b></a><bgsound> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <a> -| <b> -| <bgsound> - -#data -<!doctype html><figcaption><article></figcaption>a -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <figcaption> -| <article> -| "a" - -#data -<!doctype html><summary><article></summary>a -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <summary> -| <article> -| "a" - -#data -<!doctype html><p><a><plaintext>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <a> -| <plaintext> -| <a> -| "b" - -#data -<!DOCTYPE html><div>a<a></div>b<p>c</p>d -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <div> -| "a" -| <a> -| <a> -| "b" -| <p> -| "c" -| "d" diff --git a/src/code.google.com/p/go.net/html/testdata/webkit/tests2.dat b/src/code.google.com/p/go.net/html/testdata/webkit/tests2.dat deleted file mode 100644 index 60d85922162..00000000000 --- a/src/code.google.com/p/go.net/html/testdata/webkit/tests2.dat +++ /dev/null @@ -1,763 +0,0 @@ -#data -<!DOCTYPE html>Test -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "Test" - -#data -<textarea>test</div>test -#errors -Line: 1 Col: 10 Unexpected start tag (textarea). Expected DOCTYPE. -Line: 1 Col: 24 Expected closing tag. Unexpected end of file. -#document -| <html> -| <head> -| <body> -| <textarea> -| "test</div>test" - -#data -<table><td> -#errors -Line: 1 Col: 7 Unexpected start tag (table). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected table cell start tag (td) in the table body phase. -Line: 1 Col: 11 Expected closing tag. Unexpected end of file. -#document -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> - -#data -<table><td>test</tbody></table> -#errors -Line: 1 Col: 7 Unexpected start tag (table). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected table cell start tag (td) in the table body phase. -#document -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| "test" - -#data -<frame>test -#errors -Line: 1 Col: 7 Unexpected start tag (frame). Expected DOCTYPE. -Line: 1 Col: 7 Unexpected start tag frame. Ignored. -#document -| <html> -| <head> -| <body> -| "test" - -#data -<!DOCTYPE html><frameset>test -#errors -Line: 1 Col: 29 Unepxected characters in the frameset phase. Characters ignored. -Line: 1 Col: 29 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!DOCTYPE html><frameset><!DOCTYPE html> -#errors -Line: 1 Col: 40 Unexpected DOCTYPE. Ignored. -Line: 1 Col: 40 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <frameset> - -#data -<!DOCTYPE html><font><p><b>test</font> -#errors -Line: 1 Col: 38 End tag (font) violates step 1, paragraph 3 of the adoption agency algorithm. -Line: 1 Col: 38 End tag (font) violates step 1, paragraph 3 of the adoption agency algorithm. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <font> -| <p> -| <font> -| <b> -| "test" - -#data -<!DOCTYPE html><dt><div><dd> -#errors -Line: 1 Col: 28 Missing end tag (div, dt). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <dt> -| <div> -| <dd> - -#data -<script></x -#errors -Line: 1 Col: 8 Unexpected start tag (script). Expected DOCTYPE. -Line: 1 Col: 11 Unexpected end of file. Expected end tag (script). -#document -| <html> -| <head> -| <script> -| "</x" -| <body> - -#data -<table><plaintext><td> -#errors -Line: 1 Col: 7 Unexpected start tag (table). Expected DOCTYPE. -Line: 1 Col: 18 Unexpected start tag (plaintext) in table context caused voodoo mode. -Line: 1 Col: 22 Unexpected end of file. Expected table content. -#document -| <html> -| <head> -| <body> -| <plaintext> -| "<td>" -| <table> - -#data -<plaintext></plaintext> -#errors -Line: 1 Col: 11 Unexpected start tag (plaintext). Expected DOCTYPE. -Line: 1 Col: 23 Expected closing tag. Unexpected end of file. -#document -| <html> -| <head> -| <body> -| <plaintext> -| "</plaintext>" - -#data -<!DOCTYPE html><table><tr>TEST -#errors -Line: 1 Col: 30 Unexpected non-space characters in table context caused voodoo mode. -Line: 1 Col: 30 Unexpected end of file. Expected table content. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "TEST" -| <table> -| <tbody> -| <tr> - -#data -<!DOCTYPE html><body t1=1><body t2=2><body t3=3 t4=4> -#errors -Line: 1 Col: 37 Unexpected start tag (body). -Line: 1 Col: 53 Unexpected start tag (body). -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| t1="1" -| t2="2" -| t3="3" -| t4="4" - -#data -</b test -#errors -Line: 1 Col: 8 Unexpected end of file in attribute name. -Line: 1 Col: 8 End tag contains unexpected attributes. -Line: 1 Col: 8 Unexpected end tag (b). Expected DOCTYPE. -Line: 1 Col: 8 Unexpected end tag (b) after the (implied) root element. -#document -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html></b test<b &=&>X -#errors -Line: 1 Col: 32 Named entity didn't end with ';'. -Line: 1 Col: 33 End tag contains unexpected attributes. -Line: 1 Col: 33 Unexpected end tag (b) after the (implied) root element. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "X" - -#data -<!doctypehtml><scrIPt type=text/x-foobar;baz>X</SCRipt -#errors -Line: 1 Col: 9 No space after literal string 'DOCTYPE'. -Line: 1 Col: 54 Unexpected end of file in the tag name. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <script> -| type="text/x-foobar;baz" -| "X</SCRipt" -| <body> - -#data -& -#errors -Line: 1 Col: 1 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&" - -#data -&# -#errors -Line: 1 Col: 1 Numeric entity expected. Got end of file instead. -Line: 1 Col: 1 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&#" - -#data -&#X -#errors -Line: 1 Col: 3 Numeric entity expected but none found. -Line: 1 Col: 3 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&#X" - -#data -&#x -#errors -Line: 1 Col: 3 Numeric entity expected but none found. -Line: 1 Col: 3 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&#x" - -#data -- -#errors -Line: 1 Col: 4 Numeric entity didn't end with ';'. -Line: 1 Col: 4 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "-" - -#data -&x-test -#errors -Line: 1 Col: 1 Named entity expected. Got none. -Line: 1 Col: 1 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&x-test" - -#data -<!doctypehtml><p><li> -#errors -Line: 1 Col: 9 No space after literal string 'DOCTYPE'. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <li> - -#data -<!doctypehtml><p><dt> -#errors -Line: 1 Col: 9 No space after literal string 'DOCTYPE'. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <dt> - -#data -<!doctypehtml><p><dd> -#errors -Line: 1 Col: 9 No space after literal string 'DOCTYPE'. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <dd> - -#data -<!doctypehtml><p><form> -#errors -Line: 1 Col: 9 No space after literal string 'DOCTYPE'. -Line: 1 Col: 23 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| <form> - -#data -<!DOCTYPE html><p></P>X -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <p> -| "X" - -#data -& -#errors -Line: 1 Col: 4 Named entity didn't end with ';'. -Line: 1 Col: 4 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&" - -#data -&AMp; -#errors -Line: 1 Col: 1 Named entity expected. Got none. -Line: 1 Col: 1 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "&AMp;" - -#data -<!DOCTYPE html><html><head></head><body><thisISasillyTESTelementNameToMakeSureCrazyTagNamesArePARSEDcorrectLY> -#errors -Line: 1 Col: 110 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly> - -#data -<!DOCTYPE html>X</body>X -#errors -Line: 1 Col: 24 Unexpected non-space characters in the after body phase. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| "XX" - -#data -<!DOCTYPE html><!-- X -#errors -Line: 1 Col: 21 Unexpected end of file in comment. -#document -| <!DOCTYPE html> -| <!-- X --> -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html><table><caption>test TEST</caption><td>test -#errors -Line: 1 Col: 54 Unexpected table cell start tag (td) in the table body phase. -Line: 1 Col: 58 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <table> -| <caption> -| "test TEST" -| <tbody> -| <tr> -| <td> -| "test" - -#data -<!DOCTYPE html><select><option><optgroup> -#errors -Line: 1 Col: 41 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <option> -| <optgroup> - -#data -<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option> -#errors -Line: 1 Col: 68 Unexpected select start tag in the select phase treated as select end tag. -Line: 1 Col: 76 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <optgroup> -| <option> -| <option> -| <option> - -#data -<!DOCTYPE html><select><optgroup><option><optgroup> -#errors -Line: 1 Col: 51 Expected closing tag. Unexpected end of file. -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <optgroup> -| <option> -| <optgroup> - -#data -<!DOCTYPE html><datalist><option>foo</datalist>bar -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <datalist> -| <option> -| "foo" -| "bar" - -#data -<!DOCTYPE html><font><input><input></font> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <font> -| <input> -| <input> - -#data -<!DOCTYPE html><!-- XXX - XXX --> -#errors -#document -| <!DOCTYPE html> -| <!-- XXX - XXX --> -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html><!-- XXX - XXX -#errors -Line: 1 Col: 29 Unexpected end of file in comment (-) -#document -| <!DOCTYPE html> -| <!-- XXX - XXX --> -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html><!-- XXX - XXX - XXX --> -#errors -#document -| <!DOCTYPE html> -| <!-- XXX - XXX - XXX --> -| <html> -| <head> -| <body> - -#data -<isindex test=x name=x> -#errors -Line: 1 Col: 23 Unexpected start tag (isindex). Expected DOCTYPE. -Line: 1 Col: 23 Unexpected start tag isindex. Don't use it! -#document -| <html> -| <head> -| <body> -| <form> -| <hr> -| <label> -| "This is a searchable index. Enter search keywords: " -| <input> -| name="isindex" -| test="x" -| <hr> - -#data -test -test -#errors -Line: 2 Col: 4 Unexpected non-space characters. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> -| "test -test" - -#data -<!DOCTYPE html><body><title>test</body> -#errors -#document -| -| -| -| -| -| "test</body>" - -#data -<!DOCTYPE html><body><title>X -#errors -#document -| -| -| -| -| -| "X" -| <meta> -| name="z" -| <link> -| rel="foo" -| <style> -| " -x { content:"</style" } " - -#data -<!DOCTYPE html><select><optgroup></optgroup></select> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <optgroup> - -#data - - -#errors -Line: 2 Col: 1 Unexpected End of file. Expected DOCTYPE. -#document -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html> <html> -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> - -#data -<!DOCTYPE html><script> -</script> <title>x -#errors -#document -| -| -| -| -#errors -Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE. -Line: 1 Col: 21 Unexpected start tag (script) that can be in head. Moved. -#document -| -| -| -#errors -Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE. -Line: 1 Col: 28 Unexpected start tag (style) that can be in head. Moved. -#document -| -| -| -#errors -Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE. -#document -| -| -| -| -| "x" -| x -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -Line: 1 Col: 22 Unexpected end of file. Expected end tag (style). -#document -| -| -| --> x -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| -| -| x -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| -| -| x -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| -| -| x -#errors -Line: 1 Col: 7 Unexpected start tag (style). Expected DOCTYPE. -#document -| -| -|

-#errors -#document -| -| -| -| -| -| ddd -#errors -#document -| -| -| -#errors -#document -| -| -| -| -|
  • -| -| ", - "