├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── issue-bug.yml │ └── issue-enhance.yml ├── PULL_REQUEST_TEMPLATE.md └── TEMPLATE-README.md ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── NOTICE ├── README.md ├── README.md.erb ├── commands ├── actual_lrp_groups.go ├── actual_lrp_groups_for_guid.go ├── actual_lrp_groups_for_guid_test.go ├── actual_lrp_groups_test.go ├── actual_lrps.go ├── actual_lrps_test.go ├── bbs_flags.go ├── bbs_flags_test.go ├── cancel_task.go ├── cancel_task_test.go ├── cell.go ├── cell_state.go ├── cell_state_test.go ├── cell_states.go ├── cell_states_test.go ├── cell_test.go ├── cells.go ├── cells_test.go ├── cfdot_error.go ├── cfdot_error_test.go ├── claim_lock.go ├── claim_lock_test.go ├── claim_presence.go ├── claim_presence_test.go ├── commands_suite_test.go ├── create_desired_lrp.go ├── create_desired_lrp_test.go ├── create_task.go ├── create_task_test.go ├── delete_desired_lrp.go ├── delete_desired_lrp_test.go ├── delete_task.go ├── delete_task_test.go ├── desired_lrp.go ├── desired_lrp_scheduling_infos.go ├── desired_lrp_scheduling_infos_test.go ├── desired_lrp_test.go ├── desired_lrps.go ├── desired_lrps_test.go ├── domains.go ├── domains_test.go ├── fixtures │ ├── bbsCACert.crt │ ├── bbsClient.crt │ ├── bbsClient.key │ ├── bbsClientBadPermissions.key │ ├── randmoClient.key │ ├── randomCACert.crt │ ├── randomClient.crt │ └── randomClient.key ├── help.go ├── helpers │ ├── clients.go │ └── package.go ├── locket_flags.go ├── locket_flags_test.go ├── locks.go ├── locks_test.go ├── lrp_events.go ├── lrp_events_test.go ├── package.go ├── presences.go ├── presences_test.go ├── release_lock.go ├── release_lock_test.go ├── retire_actual_lrp.go ├── retire_actual_lrp_test.go ├── root.go ├── set_domain.go ├── set_domain_test.go ├── task.go ├── task_events.go ├── task_events_test.go ├── task_test.go ├── tasks.go ├── tasks_test.go ├── timeout_flag.go ├── timeout_flag_test.go ├── tls_flags.go ├── tls_flags_test.go ├── update_desired_lrp.go ├── update_desired_lrp_test.go └── validators.go ├── docs ├── 010-current-commands.md ├── 020-running-from-a-BOSH-deployed-vm.md ├── 030-examples.md ├── 040-building-from-source.md └── 050-design-tenets.md ├── git-hooks └── pre-commit ├── integration ├── actual_lrp_groups_for_guid_test.go ├── actual_lrp_groups_test.go ├── actual_lrps_test.go ├── cancel_task_test.go ├── cell_state_test.go ├── cell_test.go ├── cells_test.go ├── claim_presence_test.go ├── create_desired_lrp_test.go ├── create_task_test.go ├── delete_desired_lrp_test.go ├── delete_task_test.go ├── desired_lrp_scheduling_infos_test.go ├── desired_lrp_test.go ├── desired_lrps_test.go ├── domains_test.go ├── fixtures │ ├── locketCA.crt │ ├── locketClient.crt │ ├── locketClient.key │ ├── locketServer.crt │ ├── locketServer.key │ └── regenerate-certs.sh ├── help_test.go ├── integration_suite_test.go ├── locket_commands_test.go ├── lrp_events_test.go ├── package.go ├── presences_test.go ├── retire_actual_lrp_test.go ├── set_domain_test.go ├── task_events_test.go ├── task_test.go ├── tasks_test.go └── update_desired_lrp_test.go ├── main.go ├── package.go └── scripts ├── generate-cfdot-documentation └── install-git-hooks /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: CloudFoundry slack 4 | url: https://cloudfoundry.slack.com 5 | about: For help or questions about this component, you can reach the maintainers on Slack 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report a defect, such as a bug or regression. 3 | title: "Start the title with a verb (e.g. Change header styles). Use the imperative mood in the title (e.g. Fix, not Fixed or Fixes header styles)" 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | id: current 9 | attributes: 10 | label: Current behavior 11 | validations: 12 | required: true 13 | - type: markdown 14 | id: current_md 15 | attributes: 16 | value: | 17 | - Explain, in detail, what the current state of the world is 18 | - Include code snippets, log output, and analysis as necessary to explain the whole problem 19 | - Include links to logs, GitHub issues, slack conversations, etc.. to tell us where the problem came from 20 | - Steps to reproduce 21 | - type: textarea 22 | id: desired 23 | attributes: 24 | label: Desired behavior 25 | validations: 26 | required: true 27 | - type: markdown 28 | id: desired_md 29 | attributes: 30 | value: | 31 | - Describe how the problem should be fixed 32 | - Does this require a new bosh release? 33 | - Does it require configuration changes in cf-deployment? 34 | - Do we need to have a special release note? 35 | - Do we need to update repo documentation? 36 | - type: input 37 | id: version 38 | attributes: 39 | label: Affected Version 40 | description: Please enter the version 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-enhance.yml: -------------------------------------------------------------------------------- 1 | name: Enhance 2 | description: Propose an enhancement or new feature. 3 | title: "Start the title with a verb (e.g. Change header styles). Use the imperative mood in the title (e.g. Fix, not Fixed or Fixes header styles)" 4 | labels: 5 | - enhancement 6 | body: 7 | - type: textarea 8 | id: change 9 | attributes: 10 | label: Proposed Change 11 | validations: 12 | required: true 13 | - type: markdown 14 | id: change_md 15 | attributes: 16 | value: | 17 | Briefly explain why this feature is necessary in the following format 18 | 19 | **As a** *developer/operator/whatever* 20 | **I want** *this ability to do X* 21 | **So that** *I can do Y* 22 | 23 | - Provide details of where this request is coming from including links, GitHub Issues, etc.. 24 | - Provide details of prior work (if applicable) including links to commits, github issues, etc... 25 | - type: textarea 26 | id: acceptance 27 | attributes: 28 | label: Acceptance criteria 29 | validations: 30 | required: true 31 | - type: markdown 32 | id: acceptance_md 33 | attributes: 34 | value: | 35 | Detail the exact work that is required to accept this story in the following format 36 | 37 | **Scenario:** *describe scenario* 38 | **Given** *I have some sort of configuration* 39 | **When** *I do X* 40 | **And** *do Y* 41 | **Then** *I see the desired behavior* 42 | 43 | - type: textarea 44 | id: related 45 | attributes: 46 | label: Related links 47 | description: Please list related links for this issue 48 | placeholder: | 49 | - [ ] code.cloudfoundry.org/bbs for links 50 | - [x] cloudfoundry/rep#123 for issues/prs 51 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Read the [Contributing document](../blob/-/.github/CONTRIBUTING.md). 2 | 3 | Summary 4 | --------------- 5 | 10 | 11 | 12 | Backward Compatibility 13 | --------------- 14 | Breaking Change? **Yes/No** 15 | 22 | -------------------------------------------------------------------------------- /.github/TEMPLATE-README.md: -------------------------------------------------------------------------------- 1 | 2 | > [!IMPORTANT] 3 | > Content in this directory is managed by the CI task `sync-dot-github-dir`. 4 | 5 | Changing templates 6 | --------------- 7 | These templates are synced from [these shared templates](https://github.com/cloudfoundry/wg-app-platform-runtime-ci/tree/main/shared/github). 8 | Each pipeline will contain a `sync-dot-github-dir-*` job for updating the content of these files. 9 | If you would like to modify these, please change them in the shared group. 10 | It's also possible to override the templates on pipeline's parent directory by introducing a custom 11 | template in `$PARENT_TEMPLATE_DIR/github/FILENAME` or `$PARENT_TEMPLATE_DIR/github/REPO_NAME/FILENAME` in CI repo 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cfdot 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloudfoundry/wg-app-runtime-platform-diego-approvers 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | This project may include a number of subcomponents with separate 16 | copyright notices and license terms. Your use of these subcomponents 17 | is subject to the terms and conditions of each subcomponent's license, 18 | as noted in the LICENSE file. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfdot 2 | 3 | [![Go Report 4 | Card](https://goreportcard.com/badge/code.cloudfoundry.org/cfdot)](https://goreportcard.com/report/code.cloudfoundry.org/cfdot) 5 | [![Go 6 | Reference](https://pkg.go.dev/badge/code.cloudfoundry.org/cfdot.svg)](https://pkg.go.dev/code.cloudfoundry.org/cfdot) 7 | 8 | `cfdot` is the CF Diego Operator Toolkit, a CLI tool designed to 9 | interact with Diego components. 10 | 11 | At present, its commands focus on the Diego BBS API, the main public 12 | interface to a Diego deployment. 13 | 14 | > \[!NOTE\] 15 | > 16 | > This repository should be imported as `code.cloudfoundry.org/cfdot`. 17 | 18 | # Docs 19 | 20 | - [Current Commands](./docs/010-current-commands.md) 21 | - [Running from a BOSH deployed 22 | VM](./docs/020-running-from-a-BOSH-deployed-vm.md) 23 | - [Examples](./docs/030-examples.md) 24 | - [Building from Source](./docs/040-building-from-source.md) 25 | - [Design Tenets](./docs/050-design-tenets.md) 26 | 27 | # Contributing 28 | 29 | See the [Contributing.md](./.github/CONTRIBUTING.md) for more 30 | information on how to contribute. 31 | 32 | # Working Group Charter 33 | 34 | This repository is maintained by [App Runtime 35 | Platform](https://github.com/cloudfoundry/community/blob/main/toc/working-groups/app-runtime-platform.md) 36 | under `Diego` area. 37 | 38 | > \[!IMPORTANT\] 39 | > 40 | > Content in this file is managed by the [CI task 41 | > `sync-readme`](https://github.com/cloudfoundry/wg-app-platform-runtime-ci/blob/main/shared/tasks/sync-readme/metadata.yml) 42 | > and is generated by CI following a convention. 43 | -------------------------------------------------------------------------------- /README.md.erb: -------------------------------------------------------------------------------- 1 | # cfdot 2 | 3 | `cfdot` is the CF Diego Operator Toolkit, a CLI tool designed to interact with 4 | Diego components. 5 | 6 | At present, its commands focus on the Diego BBS API, the main public interface 7 | to a Diego deployment. 8 | 9 | 10 | ## Current Commands 11 | 12 | ```bash 13 | $ cfdot --help 14 | <%= `go run main.go` %> 15 | ``` 16 | 17 | ## Running from a BOSH-deployed VM 18 | 19 | `cfdot` is most useful in the context of a running Diego deployment. If you 20 | use the [`generate-deployment-manifest`](https://github.com/cloudfoundry/diego-release/blob/master/scripts/generate-deployment-manifest) 21 | script in diego-release to generate your Diego manifest, `cfdot` is already 22 | available on the BOSH-deployed Diego VMs. To use it: 23 | 24 | ```bash 25 | bosh ssh / 26 | cfdot 27 | ``` 28 | 29 | The `cfdot` pre-start script installs the `setup` script into `/etc/profile.d`. 30 | This `setup` script does 3 things: 31 | 32 | - Exports environment variables to target the BBS API in the deployment. 33 | - Puts the `cfdot` binary on the `PATH`. 34 | - Puts a `jq` binary on the `PATH`. 35 | 36 | ## Basic Examples 37 | 38 | ```bash 39 | # count the total number of desired instances 40 | $ cfdot desired-lrp-scheduling-infos | jq '.instances' | jq -s 'add' 41 | 568 42 | 43 | # show instance counts by state 44 | $ cfdot actual-lrps | jq -s -r 'group_by(.state)[] | .[0].state + ": " + (length | tostring)' 45 | CRASHED: 36 46 | RUNNING: 531 47 | UNCLAIMED: 1 48 | ``` 49 | 50 | ## Building from Source 51 | 52 | `cfdot` requires the [Diego BBS client library](https://github.com/cloudfoundry/bbs). 53 | If you have already cloned [diego-release](https://github.com/cloudfoundry/diego-release), 54 | you can run the following commands using that diego-release directory as your 55 | GOPATH. Alternatively, run these commands with any other GOPATH and `go get` 56 | will automatically fetch the latest BBS code from diego-release. 57 | 58 | ```bash 59 | # Get cfdot and required dependencies 60 | go get code.cloudfoundry.org/cfdot 61 | cd src/code.cloudfoundry.org/cfdot 62 | 63 | # Build for Linux 64 | GOOS=linux go build . 65 | 66 | # Build for Mac 67 | GOOS=darwin go build . 68 | 69 | # Build for Windows 70 | GOOS=windows go build . 71 | ``` 72 | 73 | ## Design Tenets 74 | 75 | - Execution is stateless: configuration is specified either as flags or as environment variables. 76 | - Conform to UNIX conventions of successful output on stdout and error messages on stderr. 77 | - For BBS API commands, output is a stream of JSON values, one per line, optimal for processing with `jq` and suitable for processing with `bash` and other line-based UNIX utilities. 78 | -------------------------------------------------------------------------------- /commands/actual_lrp_groups.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // flags 15 | var ( 16 | actualLRPGroupsDomainFlag, actualLRPGroupsCellIdFlag string 17 | ) 18 | 19 | var actualLRPGroupsCmd = &cobra.Command{ 20 | Use: "actual-lrp-groups", 21 | Short: `[DEPRECATED] use "actual-lrps" command instead. List actual LRP groups`, 22 | Long: "List actual LRP groups from the BBS", 23 | RunE: actualLRPGroups, 24 | Deprecated: `use "actual-lrps" instead.`, 25 | } 26 | 27 | func init() { 28 | AddBBSAndTimeoutFlags(actualLRPGroupsCmd) 29 | 30 | actualLRPGroupsCmd.Flags().StringVarP(&actualLRPGroupsDomainFlag, "domain", "d", "", "retrieve only actual lrps for the given domain") 31 | actualLRPGroupsCmd.Flags().StringVarP(&actualLRPGroupsCellIdFlag, "cell-id", "c", "", "retrieve only actual lrps for the given cell id") 32 | 33 | RootCmd.AddCommand(actualLRPGroupsCmd) 34 | } 35 | 36 | func actualLRPGroups(cmd *cobra.Command, args []string) error { 37 | err := ValidateActualLRPGroupsArguments(args) 38 | if err != nil { 39 | return NewCFDotValidationError(cmd, err) 40 | } 41 | 42 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 43 | if err != nil { 44 | return NewCFDotError(cmd, err) 45 | } 46 | 47 | err = ActualLRPGroups( 48 | cmd.OutOrStdout(), 49 | cmd.OutOrStderr(), 50 | bbsClient, 51 | actualLRPGroupsDomainFlag, 52 | actualLRPGroupsCellIdFlag, 53 | ) 54 | if err != nil { 55 | return NewCFDotError(cmd, err) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func ValidateActualLRPGroupsArguments(args []string) error { 62 | if len(args) > 0 { 63 | return errExtraArguments 64 | } 65 | return nil 66 | } 67 | 68 | func ActualLRPGroups(stdout, stderr io.Writer, bbsClient bbs.Client, domain, cellID string) error { 69 | traceID := trace.GenerateTraceID() 70 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("actual-lrp-groups"), traceID) 71 | 72 | encoder := json.NewEncoder(stdout) 73 | 74 | actualLRPFilter := models.ActualLRPFilter{ 75 | CellID: cellID, 76 | Domain: domain, 77 | } 78 | 79 | actualLRPGroups, err := bbsClient.ActualLRPGroups(logger, traceID, actualLRPFilter) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | for _, actualLRPGroup := range actualLRPGroups { 85 | err = encoder.Encode(actualLRPGroup) 86 | if err != nil { 87 | logger.Error("failed-to-marshal", err) 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /commands/actual_lrp_groups_for_guid.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strconv" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "code.cloudfoundry.org/bbs/trace" 12 | 13 | "code.cloudfoundry.org/cfdot/commands/helpers" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // flags 18 | var ( 19 | actualLRPGroupsGuidIndexFlag string 20 | ) 21 | 22 | var actualLRPGroupsByProcessGuidCmd = &cobra.Command{ 23 | Use: "actual-lrp-groups-for-guid PROCESS_GUID", 24 | Short: `[DEPRECATED] use "actual-lrps" command with the "--process-guid" flag instead. List actual LRP groups for a process guid`, 25 | Long: fmt.Sprintf("List actual LRP groups from the BBS for a given process guid. Process guids can be obtained by running %s actual-lrp-groups", os.Args[0]), 26 | RunE: actualLRPGroupsByProcessGuid, 27 | Deprecated: `use "actual-lrps" with the "--process-guid" flag instead.`, 28 | } 29 | 30 | func init() { 31 | AddBBSAndTimeoutFlags(actualLRPGroupsByProcessGuidCmd) 32 | 33 | // String flag because logic for optional int flag is not clear 34 | actualLRPGroupsByProcessGuidCmd.Flags().StringVarP(&actualLRPGroupsGuidIndexFlag, "index", "i", "", "retrieve actual lrp for the given index") 35 | 36 | RootCmd.AddCommand(actualLRPGroupsByProcessGuidCmd) 37 | } 38 | 39 | func actualLRPGroupsByProcessGuid(cmd *cobra.Command, args []string) error { 40 | processGuid, index, err := ValidateActualLRPGroupsForGuidArgs(args, actualLRPGroupsGuidIndexFlag) 41 | if err != nil { 42 | return NewCFDotValidationError(cmd, err) 43 | } 44 | 45 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 46 | if err != nil { 47 | return NewCFDotError(cmd, err) 48 | } 49 | 50 | err = ActualLRPGroupsForGuid(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, processGuid, index) 51 | if err != nil { 52 | return NewCFDotError(cmd, err) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func ValidateActualLRPGroupsForGuidArgs(args []string, indexFlag string) (string, int, error) { 59 | if len(args) < 1 { 60 | return "", 0, errMissingArguments 61 | } 62 | 63 | if len(args) > 1 { 64 | return "", 0, errExtraArguments 65 | } 66 | 67 | if args[0] == "" { 68 | return "", 0, errInvalidProcessGuid 69 | } 70 | 71 | index := -1 72 | if indexFlag != "" { 73 | var err error 74 | index, err = strconv.Atoi(indexFlag) 75 | if err != nil || index < 0 { 76 | return "", 0, errInvalidIndex 77 | } 78 | } 79 | 80 | return args[0], index, nil 81 | } 82 | 83 | func ActualLRPGroupsForGuid(stdout, stderr io.Writer, bbsClient bbs.Client, processGuid string, index int) error { 84 | traceID := trace.GenerateTraceID() 85 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("actual-lrp-groups-for-guid"), traceID) 86 | 87 | encoder := json.NewEncoder(stdout) 88 | if index < 0 { 89 | actualLRPGroups, err := bbsClient.ActualLRPGroupsByProcessGuid(logger, traceID, processGuid) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | for _, group := range actualLRPGroups { 95 | err = encoder.Encode(group) 96 | if err != nil { 97 | logger.Error("failed-to-marshal", err) 98 | } 99 | } 100 | 101 | return nil 102 | } else { 103 | actualLRPGroup, err := bbsClient.ActualLRPGroupByProcessGuidAndIndex(logger, traceID, processGuid, index) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | return encoder.Encode(actualLRPGroup) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /commands/actual_lrp_groups_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/openzipkin/zipkin-go/model" 14 | ) 15 | 16 | var _ = Describe("ActualLRPGroups", func() { 17 | var ( 18 | fakeBBSClient *fake_bbs.FakeClient 19 | 20 | //lint:ignore SA1019 - deprecated model used for testing deprecated functionality 21 | actualLRPGroups []*models.ActualLRPGroup 22 | returnedError error 23 | stdout, stderr *gbytes.Buffer 24 | ) 25 | 26 | BeforeEach(func() { 27 | actualLRPGroups = nil 28 | returnedError = nil 29 | stdout = gbytes.NewBuffer() 30 | stderr = gbytes.NewBuffer() 31 | fakeBBSClient = &fake_bbs.FakeClient{} 32 | }) 33 | 34 | JustBeforeEach(func() { 35 | fakeBBSClient.ActualLRPGroupsReturns(actualLRPGroups, returnedError) 36 | }) 37 | 38 | Context("when the bbs responds with actual lrp groups", func() { 39 | BeforeEach(func() { 40 | //lint:ignore SA1019 - deprecated model used for testing deprecated functionality 41 | actualLRPGroups = []*models.ActualLRPGroup{ 42 | { 43 | Instance: &models.ActualLRP{ 44 | State: "running", 45 | }, 46 | }, 47 | } 48 | }) 49 | 50 | It("prints a json stream of all the actual lrp groups", func() { 51 | err := commands.ActualLRPGroups(stdout, stderr, fakeBBSClient, "domain-1", "cell-1") 52 | Expect(err).NotTo(HaveOccurred()) 53 | 54 | Expect(fakeBBSClient.ActualLRPGroupsCallCount()).To(Equal(1)) 55 | 56 | _, traceID, filter := fakeBBSClient.ActualLRPGroupsArgsForCall(0) 57 | Expect(filter).To(Equal(models.ActualLRPFilter{CellID: "cell-1", Domain: "domain-1"})) 58 | _, err = model.TraceIDFromHex(traceID) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | expectedOutput := "" 62 | for _, group := range actualLRPGroups { 63 | d, err := json.Marshal(group) 64 | Expect(err).NotTo(HaveOccurred()) 65 | expectedOutput += string(d) + "\n" 66 | } 67 | 68 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 69 | }) 70 | }) 71 | 72 | Context("when the bbs errors", func() { 73 | BeforeEach(func() { 74 | returnedError = models.ErrUnknownError 75 | }) 76 | 77 | It("fails with a relevant error", func() { 78 | err := commands.ActualLRPGroups(stdout, stderr, fakeBBSClient, "", "") 79 | Expect(err).To(HaveOccurred()) 80 | 81 | Expect(err).To(Equal(models.ErrUnknownError)) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /commands/actual_lrps.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // flags 15 | var ( 16 | actualLRPsDomainFlag, actualLRPsCellIdFlag, actualLRPsProcessGuidFlag string 17 | actualLRPsIndexFlag int32 18 | ) 19 | 20 | var actualLRPsCmd = &cobra.Command{ 21 | Use: "actual-lrps", 22 | Short: "List actual LRPs", 23 | Long: "List actual LRPs from the BBS", 24 | RunE: actualLRPs, 25 | } 26 | 27 | func init() { 28 | AddBBSAndTimeoutFlags(actualLRPsCmd) 29 | 30 | actualLRPsCmd.Flags().StringVarP(&actualLRPsDomainFlag, "domain", "d", "", "retrieve only actual lrps for the given domain") 31 | actualLRPsCmd.Flags().StringVarP(&actualLRPsCellIdFlag, "cell-id", "c", "", "retrieve only actual lrps for the given cell id") 32 | actualLRPsCmd.Flags().StringVarP(&actualLRPsProcessGuidFlag, "process-guid", "p", "", "retrieve only actual lrps for the given process guid") 33 | actualLRPsCmd.Flags().Int32VarP(&actualLRPsIndexFlag, "index", "i", 0, "retrieve only actual lrps for the given index") 34 | 35 | RootCmd.AddCommand(actualLRPsCmd) 36 | } 37 | 38 | func actualLRPs(cmd *cobra.Command, args []string) error { 39 | err := ValidateActualLRPsArguments(args) 40 | if err != nil { 41 | return NewCFDotValidationError(cmd, err) 42 | } 43 | 44 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 45 | if err != nil { 46 | return NewCFDotError(cmd, err) 47 | } 48 | 49 | var index *int32 50 | if cmd.Flag("index").Changed { 51 | index = &actualLRPsIndexFlag 52 | } 53 | 54 | err = ActualLRPs( 55 | cmd.OutOrStdout(), 56 | cmd.OutOrStderr(), 57 | bbsClient, 58 | actualLRPsDomainFlag, 59 | actualLRPsCellIdFlag, 60 | actualLRPsProcessGuidFlag, 61 | index, 62 | ) 63 | if err != nil { 64 | return NewCFDotError(cmd, err) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func ValidateActualLRPsArguments(args []string) error { 71 | if len(args) > 0 { 72 | return errExtraArguments 73 | } 74 | return nil 75 | } 76 | 77 | func ActualLRPs(stdout, stderr io.Writer, bbsClient bbs.Client, domain, cellID, processGuid string, index *int32) error { 78 | traceID := trace.GenerateTraceID() 79 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("actual-lrps"), traceID) 80 | 81 | encoder := json.NewEncoder(stdout) 82 | 83 | actualLRPFilter := models.ActualLRPFilter{ 84 | CellID: cellID, 85 | Domain: domain, 86 | ProcessGuid: processGuid, 87 | Index: index, 88 | } 89 | 90 | actualLRPs, err := bbsClient.ActualLRPs(logger, traceID, actualLRPFilter) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | for _, actualLRP := range actualLRPs { 96 | err = encoder.Encode(actualLRP) 97 | if err != nil { 98 | logger.Error("failed-to-marshal", err) 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /commands/actual_lrps_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/openzipkin/zipkin-go/model" 13 | ) 14 | 15 | var _ = Describe("ActualLRPs", func() { 16 | var ( 17 | fakeBBSClient *fake_bbs.FakeClient 18 | actualLRPs []*models.ActualLRP 19 | returnedError error 20 | stdout, stderr *gbytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | actualLRPs = nil 25 | returnedError = nil 26 | stdout = gbytes.NewBuffer() 27 | stderr = gbytes.NewBuffer() 28 | fakeBBSClient = &fake_bbs.FakeClient{} 29 | }) 30 | 31 | JustBeforeEach(func() { 32 | fakeBBSClient.ActualLRPsReturns(actualLRPs, returnedError) 33 | }) 34 | 35 | Context("when the bbs responds with actual lrps", func() { 36 | BeforeEach(func() { 37 | actualLRPs = []*models.ActualLRP{ 38 | &models.ActualLRP{ 39 | State: "running", 40 | }, 41 | } 42 | }) 43 | 44 | It("prints a json stream of all the actual lrps", func() { 45 | index := int32(4) 46 | err := commands.ActualLRPs(stdout, stderr, fakeBBSClient, "domain-1", "cell-1", "pg-2", &index) 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | Expect(fakeBBSClient.ActualLRPsCallCount()).To(Equal(1)) 50 | 51 | _, traceID, filter := fakeBBSClient.ActualLRPsArgsForCall(0) 52 | Expect(filter).To(Equal(models.ActualLRPFilter{ 53 | CellID: "cell-1", 54 | Domain: "domain-1", 55 | ProcessGuid: "pg-2", 56 | Index: &index, 57 | })) 58 | _, err = model.TraceIDFromHex(traceID) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | expectedOutput := "" 62 | for _, lrp := range actualLRPs { 63 | d, err := json.Marshal(lrp) 64 | Expect(err).NotTo(HaveOccurred()) 65 | expectedOutput += string(d) + "\n" 66 | } 67 | 68 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 69 | }) 70 | }) 71 | 72 | Context("when the bbs errors", func() { 73 | BeforeEach(func() { 74 | returnedError = models.ErrUnknownError 75 | }) 76 | 77 | It("fails with a relevant error", func() { 78 | err := commands.ActualLRPs(stdout, stderr, fakeBBSClient, "", "", "", nil) 79 | Expect(err).To(HaveOccurred()) 80 | 81 | Expect(err).To(Equal(models.ErrUnknownError)) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /commands/bbs_flags.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "net/url" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | bbsUrl string 15 | ) 16 | 17 | // errors 18 | var ( 19 | errMissingBBSUrl = errors.New("BBS URL not set. Please specify one with the '--bbsURL' flag or the 'BBS_URL' environment variable.") 20 | ) 21 | 22 | func AddBBSFlags(cmd *cobra.Command) { 23 | AddTLSFlags(cmd) 24 | cmd.Flags().StringVar(&bbsUrl, "bbsURL", "", "URL of BBS server to target [environment variable equivalent: BBS_URL]") 25 | cmd.PreRunE = BBSPrehook 26 | } 27 | 28 | func BBSPrehook(cmd *cobra.Command, args []string) error { 29 | if err := setBBSFlags(cmd, args); err != nil { 30 | return err 31 | } 32 | return tlsPreHook(cmd, args) 33 | } 34 | 35 | func setBBSFlags(cmd *cobra.Command, args []string) error { 36 | var err, returnErr error 37 | 38 | if bbsUrl == "" { 39 | bbsUrl = os.Getenv("BBS_URL") 40 | } 41 | 42 | if bbsUrl == "" { 43 | returnErr = NewCFDotValidationError(cmd, errMissingBBSUrl) 44 | return returnErr 45 | } 46 | 47 | Config.BBSUrl = bbsUrl 48 | 49 | var parsedURL *url.URL 50 | if parsedURL, err = url.Parse(Config.BBSUrl); err != nil { 51 | returnErr = NewCFDotValidationError( 52 | cmd, 53 | fmt.Errorf( 54 | "The value '%s' is not a valid BBS URL. Please specify one with the '--bbsURL' flag or the 'BBS_URL' environment variable.", 55 | Config.BBSUrl), 56 | ) 57 | return returnErr 58 | } 59 | 60 | if parsedURL.Scheme != "https" { 61 | returnErr = NewCFDotValidationError( 62 | cmd, 63 | fmt.Errorf( 64 | "The URL '%s' does not have an 'https' scheme. Please "+ 65 | "specify one with the '--bbsURL' flag or the 'BBS_URL' environment "+ 66 | "variable.", Config.BBSUrl), 67 | ) 68 | return returnErr 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func validateReadableFile(cmd *cobra.Command, filename, filetype string) error { 75 | file, err := os.Open(filename) 76 | if err != nil { 77 | 78 | return NewCFDotValidationError( 79 | cmd, 80 | fmt.Errorf("%s file '%s' doesn't exist or is not readable: %s", filetype, filename, err.Error()), 81 | ) 82 | } 83 | defer file.Close() 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /commands/cancel_task.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "code.cloudfoundry.org/bbs" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | ) 12 | 13 | var cancelTaskCmd = &cobra.Command{ 14 | Use: "cancel-task TASK_GUID", 15 | Short: "Cancel task", 16 | Long: "Cancel the specified task", 17 | RunE: cancelTask, 18 | } 19 | 20 | func init() { 21 | AddBBSAndTimeoutFlags(cancelTaskCmd) 22 | RootCmd.AddCommand(cancelTaskCmd) 23 | } 24 | 25 | func cancelTask(cmd *cobra.Command, args []string) error { 26 | guid, err := ValidateTaskArgs(args) 27 | if err != nil { 28 | return NewCFDotValidationError(cmd, err) 29 | } 30 | 31 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 32 | if err != nil { 33 | return NewCFDotError(cmd, err) 34 | } 35 | 36 | if err := CancelTaskByGuid(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, guid); err != nil { 37 | return NewCFDotError(cmd, err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func CancelTaskByGuid(stdout, _ io.Writer, bbsClient bbs.Client, taskGuid string) error { 44 | traceID := trace.GenerateTraceID() 45 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("cancel-task-by-guid"), traceID) 46 | 47 | err := bbsClient.CancelTask(logger, traceID, taskGuid) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func ValidateCancelTaskArgs(args []string) (string, error) { 56 | if len(args) == 0 || args[0] == "" { 57 | return "", errMissingArguments 58 | } 59 | 60 | if len(args) > 1 { 61 | return "", errExtraArguments 62 | } 63 | 64 | return args[0], nil 65 | } 66 | -------------------------------------------------------------------------------- /commands/cancel_task_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/bbs/fake_bbs" 5 | "code.cloudfoundry.org/bbs/models" 6 | "code.cloudfoundry.org/cfdot/commands" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/openzipkin/zipkin-go/model" 12 | ) 13 | 14 | var _ = Describe("CancelTask", func() { 15 | Describe("CancelTaskByGuid", func() { 16 | var ( 17 | fakeBBSClient *fake_bbs.FakeClient 18 | stdout, stderr *gbytes.Buffer 19 | ) 20 | 21 | BeforeEach(func() { 22 | stdout = gbytes.NewBuffer() 23 | stderr = gbytes.NewBuffer() 24 | fakeBBSClient = &fake_bbs.FakeClient{} 25 | }) 26 | 27 | It("passes through the task guid to the BBS", func() { 28 | taskGuid := "task-guid" 29 | 30 | err := commands.CancelTaskByGuid(stdout, stderr, fakeBBSClient, taskGuid) 31 | Expect(err).NotTo(HaveOccurred()) 32 | 33 | Expect(fakeBBSClient.CancelTaskCallCount()).To(Equal(1)) 34 | 35 | _, traceID, guid := fakeBBSClient.CancelTaskArgsForCall(0) 36 | Expect(guid).To(Equal(taskGuid)) 37 | _, err = model.TraceIDFromHex(traceID) 38 | Expect(err).NotTo(HaveOccurred()) 39 | }) 40 | 41 | Context("when the bbs client errors", func() { 42 | It("returns an error back", func() { 43 | fakeBBSClient.CancelTaskReturns(models.ErrResourceNotFound) 44 | 45 | err := commands.CancelTaskByGuid(stdout, stderr, fakeBBSClient, "broken") 46 | Expect(err).To(HaveOccurred()) 47 | Expect(err).To(Equal(models.ErrResourceNotFound)) 48 | }) 49 | }) 50 | }) 51 | 52 | Describe("ValidateCancelTaskArgs", func() { 53 | Context("when one argument is passed", func() { 54 | It("returns the argument and no error", func() { 55 | guid, err := commands.ValidateCancelTaskArgs([]string{"guid"}) 56 | Expect(err).NotTo(HaveOccurred()) 57 | Expect(guid).To(Equal("guid")) 58 | }) 59 | }) 60 | 61 | Context("when no arguments are passed", func() { 62 | It("returns an error", func() { 63 | _, err := commands.ValidateCancelTaskArgs([]string{}) 64 | Expect(err).To(HaveOccurred()) 65 | }) 66 | }) 67 | 68 | Context("when two arguments are passed", func() { 69 | It("returns an error", func() { 70 | _, err := commands.ValidateCancelTaskArgs([]string{"guid1", "guid2"}) 71 | Expect(err).To(HaveOccurred()) 72 | }) 73 | }) 74 | 75 | Context("when one empty argument is passed", func() { 76 | It("returns an error", func() { 77 | _, err := commands.ValidateCancelTaskArgs([]string{""}) 78 | Expect(err).To(HaveOccurred()) 79 | }) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /commands/cell.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | 8 | "code.cloudfoundry.org/bbs" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var cellCmd = &cobra.Command{ 16 | Use: "cell CELL_ID", 17 | Short: "Show the specified cell presence", 18 | Long: "Show the cell presence specified by the given cell id", 19 | RunE: cell, 20 | } 21 | 22 | func init() { 23 | AddBBSAndTimeoutFlags(cellCmd) 24 | RootCmd.AddCommand(cellCmd) 25 | } 26 | 27 | func cell(cmd *cobra.Command, args []string) error { 28 | err := ValidateCellArguments(args) 29 | if err != nil { 30 | return NewCFDotValidationError(cmd, err) 31 | } 32 | 33 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 34 | if err != nil { 35 | return NewCFDotError(cmd, err) 36 | } 37 | 38 | err = Cell( 39 | cmd.OutOrStdout(), 40 | cmd.OutOrStderr(), 41 | bbsClient, 42 | args[0], 43 | ) 44 | if err != nil { 45 | return NewCFDotError(cmd, err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func ValidateCellArguments(args []string) error { 52 | switch { 53 | case len(args) > 1: 54 | return errExtraArguments 55 | case len(args) < 1: 56 | return errMissingArguments 57 | default: 58 | return nil 59 | } 60 | } 61 | 62 | func Cell(stdout, stderr io.Writer, bbsClient bbs.Client, cellId string) error { 63 | traceID := trace.GenerateTraceID() 64 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("cell-presence"), traceID) 65 | 66 | encoder := json.NewEncoder(stdout) 67 | 68 | cells, err := bbsClient.Cells(logger, traceID) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | for _, cell := range cells { 74 | if cell.CellId == cellId { 75 | err = encoder.Encode(cell) 76 | if err != nil { 77 | logger.Error("failed-to-marshal", err) 78 | } 79 | 80 | return err 81 | } 82 | } 83 | 84 | return errors.New("Cell not found") 85 | } 86 | -------------------------------------------------------------------------------- /commands/cell_state.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "code.cloudfoundry.org/bbs/models" 12 | "code.cloudfoundry.org/bbs/trace" 13 | "code.cloudfoundry.org/cfdot/commands/helpers" 14 | cfhttp "code.cloudfoundry.org/cfhttp/v2" 15 | "code.cloudfoundry.org/rep" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var cellStateCmd = &cobra.Command{ 20 | Use: "cell-state CELL_ID", 21 | Short: "Show the specified cell state", 22 | Long: "Show the cell state specified by the given cell id", 23 | RunE: cellState, 24 | } 25 | 26 | func init() { 27 | AddBBSAndTimeoutFlags(cellStateCmd) 28 | RootCmd.AddCommand(cellStateCmd) 29 | } 30 | 31 | func cellState(cmd *cobra.Command, args []string) error { 32 | err := ValidateCellStateArguments(args) 33 | if err != nil { 34 | return NewCFDotValidationError(cmd, err) 35 | } 36 | 37 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 38 | if err != nil { 39 | return NewCFDotError(cmd, err) 40 | } 41 | 42 | traceID := trace.GenerateTraceID() 43 | cellRegistration, err := FetchCellRegistration(bbsClient, traceID, args[0]) 44 | if err != nil { 45 | return NewCFDotError(cmd, err) 46 | } 47 | 48 | httpClient := cfhttp.NewClient() 49 | stateClient := cfhttp.NewClient( 50 | cfhttp.WithRequestTimeout(10 * time.Second), 51 | ) 52 | 53 | repTLSConfig := &rep.TLSConfig{ 54 | CaCertFile: Config.CACertFile, 55 | CertFile: Config.CertFile, 56 | KeyFile: Config.KeyFile, 57 | } 58 | repClientFactory, err := rep.NewClientFactory(httpClient, stateClient, repTLSConfig) 59 | if err != nil { 60 | return NewCFDotComponentError(cmd, fmt.Errorf("Failed creating rep client factory: %s", err)) 61 | } 62 | 63 | err = FetchCellState( 64 | cmd.OutOrStdout(), 65 | cmd.OutOrStderr(), 66 | repClientFactory, 67 | cellRegistration, 68 | traceID, 69 | ) 70 | if err != nil { 71 | return NewCFDotComponentError(cmd, fmt.Errorf("Rep error: Failed to get cell state for cell %s: %s", args[0], err.Error())) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func ValidateCellStateArguments(args []string) error { 78 | switch { 79 | case len(args) > 1: 80 | return errExtraArguments 81 | case len(args) < 1: 82 | return errMissingArguments 83 | default: 84 | return nil 85 | } 86 | } 87 | 88 | func FetchCellRegistration(bbsClient bbs.Client, traceID string, cellId string) (*models.CellPresence, error) { 89 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("fetch-cell-presence"), traceID) 90 | 91 | cells, err := bbsClient.Cells(logger, traceID) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | for _, cell := range cells { 97 | if cell.CellId == cellId { 98 | return cell, nil 99 | } 100 | } 101 | 102 | return nil, errors.New("Cell not found") 103 | } 104 | 105 | func FetchCellState(stdout, stderr io.Writer, clientFactory rep.ClientFactory, registration *models.CellPresence, traceID string) error { 106 | repClient, err := clientFactory.CreateClient(registration.RepAddress, registration.RepUrl, traceID) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("cell-state"), traceID) 112 | encoder := json.NewEncoder(stdout) 113 | 114 | state, err := repClient.State(logger) 115 | if err != nil { 116 | logger.Error("failed-to-fetch-cell-state", err) 117 | return err 118 | } 119 | 120 | err = encoder.Encode(state) 121 | if err != nil { 122 | logger.Error("failed-to-marshal", err) 123 | return err 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /commands/cell_states.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "code.cloudfoundry.org/bbs" 10 | "code.cloudfoundry.org/bbs/trace" 11 | "code.cloudfoundry.org/cfdot/commands/helpers" 12 | cfhttp "code.cloudfoundry.org/cfhttp/v2" 13 | "code.cloudfoundry.org/rep" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var cellStatesCmd = &cobra.Command{ 18 | Use: "cell-states", 19 | Short: "Show cell states for all cells", 20 | Long: "Show the cell state for all the cells", 21 | RunE: cellStates, 22 | } 23 | 24 | func init() { 25 | AddBBSAndTimeoutFlags(cellStatesCmd) 26 | RootCmd.AddCommand(cellStatesCmd) 27 | } 28 | 29 | func cellStates(cmd *cobra.Command, args []string) error { 30 | err := ValidateCellStatesArguments(args) 31 | if err != nil { 32 | return NewCFDotValidationError(cmd, err) 33 | } 34 | 35 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 36 | if err != nil { 37 | return NewCFDotError(cmd, err) 38 | } 39 | 40 | httpClient := cfhttp.NewClient() 41 | stateClient := cfhttp.NewClient( 42 | cfhttp.WithRequestTimeout(10 * time.Second), 43 | ) 44 | 45 | repTLSConfig := &rep.TLSConfig{ 46 | CaCertFile: Config.CACertFile, 47 | CertFile: Config.CertFile, 48 | KeyFile: Config.KeyFile, 49 | } 50 | repClientFactory, err := rep.NewClientFactory(httpClient, stateClient, repTLSConfig) 51 | if err != nil { 52 | return NewCFDotComponentError(cmd, fmt.Errorf("Failed creating rep client factory: %s", err)) 53 | } 54 | 55 | return FetchCellStates(cmd, cmd.OutOrStdout(), cmd.OutOrStderr(), repClientFactory, bbsClient) 56 | } 57 | 58 | func ValidateCellStatesArguments(args []string) error { 59 | switch { 60 | case len(args) > 0: 61 | return errExtraArguments 62 | default: 63 | return nil 64 | } 65 | } 66 | 67 | func FetchCellStates(cmd *cobra.Command, stdout, stderr io.Writer, clientFactory rep.ClientFactory, bbsClient bbs.Client) error { 68 | traceID := trace.GenerateTraceID() 69 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("cell-states"), traceID) 70 | 71 | registrations, err := bbsClient.Cells(logger, traceID) 72 | if err != nil { 73 | return NewCFDotComponentError(cmd, fmt.Errorf("BBS error: Failed to get cell registrations from BBS: %s", err)) 74 | } 75 | errs := "" 76 | for _, registration := range registrations { 77 | err := FetchCellState(stdout, stderr, clientFactory, registration, traceID) 78 | if err != nil { 79 | errs += fmt.Sprintf("Rep error: Failed to get cell state for cell %s: %s\n", registration.CellId, err) 80 | } 81 | } 82 | 83 | if errs != "" { 84 | return NewCFDotComponentError(cmd, errors.New(errs)) 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /commands/cell_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/bbs/fake_bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/cfdot/commands" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | ) 14 | 15 | var _ = Describe("Cell", func() { 16 | Context("ValidateCellArguments", func() { 17 | It("validates the arguments", func() { 18 | err := commands.ValidateCellArguments([]string{"cell-id"}) 19 | Expect(err).NotTo(HaveOccurred()) 20 | }) 21 | 22 | Context("more than 1 argument", func() { 23 | It("returns an extra arguments error", func() { 24 | err := commands.ValidateCellArguments([]string{"cell-id", "extra-arg"}) 25 | Expect(err).To(MatchError("Too many arguments specified")) 26 | }) 27 | }) 28 | 29 | Context("no arguments", func() { 30 | It("returns a missing arguments error", func() { 31 | err := commands.ValidateCellArguments([]string{}) 32 | Expect(err).To(MatchError("Missing arguments")) 33 | }) 34 | }) 35 | }) 36 | 37 | Context("Cell", func() { 38 | var ( 39 | fakeBBSClient *fake_bbs.FakeClient 40 | stdout, stderr *gbytes.Buffer 41 | presence *models.CellPresence 42 | cellId string 43 | ) 44 | 45 | BeforeEach(func() { 46 | cellId = "cell-id" 47 | presence = &models.CellPresence{ 48 | CellId: cellId, 49 | } 50 | 51 | stdout = gbytes.NewBuffer() 52 | stderr = gbytes.NewBuffer() 53 | fakeBBSClient = &fake_bbs.FakeClient{} 54 | fakeBBSClient.CellsReturns([]*models.CellPresence{ 55 | { 56 | CellId: "cell-id2", 57 | }, 58 | presence, 59 | { 60 | CellId: "cell-id3", 61 | }, 62 | }, nil) 63 | }) 64 | 65 | It("fetches the cell presence", func() { 66 | err := commands.Cell(stdout, stderr, fakeBBSClient, cellId) 67 | Expect(err).NotTo(HaveOccurred()) 68 | Expect(fakeBBSClient.CellsCallCount()).To(Equal(1)) 69 | }) 70 | 71 | It("outputs the cell presence to stdout", func() { 72 | err := commands.Cell(stdout, stderr, fakeBBSClient, cellId) 73 | Expect(err).NotTo(HaveOccurred()) 74 | Expect(fakeBBSClient.CellsCallCount()).To(Equal(1)) 75 | 76 | var receivedPresence models.CellPresence 77 | err = json.Unmarshal(stdout.Contents(), &receivedPresence) 78 | Expect(err).NotTo(HaveOccurred()) 79 | Expect(receivedPresence).To(Equal(*presence)) 80 | }) 81 | 82 | Context("when encoding to stdout fails", func() { 83 | BeforeEach(func() { 84 | Expect(stdout.Close()).To(Succeed()) 85 | }) 86 | 87 | It("returns the error", func() { 88 | err := commands.Cell(stdout, stderr, fakeBBSClient, cellId) 89 | Expect(err).To(HaveOccurred()) 90 | }) 91 | }) 92 | 93 | Context("when fetching the cells fails", func() { 94 | BeforeEach(func() { 95 | fakeBBSClient.CellsReturns(nil, errors.New("boom")) 96 | }) 97 | 98 | It("returns the error", func() { 99 | err := commands.Cell(stdout, stderr, fakeBBSClient, cellId) 100 | Expect(err).To(HaveOccurred()) 101 | }) 102 | }) 103 | 104 | Context("when the cell doesn't exist", func() { 105 | It("returns an error", func() { 106 | err := commands.Cell(stdout, stderr, fakeBBSClient, "non-existent") 107 | Expect(err).To(HaveOccurred()) 108 | }) 109 | }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /commands/cells.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/trace" 9 | 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var cellsCmd = &cobra.Command{ 15 | Use: "cells", 16 | Short: "List registered cell presences", 17 | Long: "List registered cell presences from the BBS", 18 | RunE: cells, 19 | } 20 | 21 | func init() { 22 | AddBBSAndTimeoutFlags(cellsCmd) 23 | 24 | RootCmd.AddCommand(cellsCmd) 25 | } 26 | 27 | func cells(cmd *cobra.Command, args []string) error { 28 | err := ValidateCellsArguments(args) 29 | if err != nil { 30 | return NewCFDotValidationError(cmd, err) 31 | } 32 | 33 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 34 | if err != nil { 35 | return NewCFDotError(cmd, err) 36 | } 37 | 38 | err = Cells( 39 | cmd.OutOrStdout(), 40 | cmd.OutOrStderr(), 41 | bbsClient, 42 | ) 43 | if err != nil { 44 | return NewCFDotError(cmd, err) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func ValidateCellsArguments(args []string) error { 51 | if len(args) > 0 { 52 | return errExtraArguments 53 | } 54 | return nil 55 | } 56 | 57 | func Cells(stdout, stderr io.Writer, bbsClient bbs.Client) error { 58 | traceID := trace.GenerateTraceID() 59 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("cell-presences"), traceID) 60 | 61 | encoder := json.NewEncoder(stdout) 62 | 63 | cellPresences, err := bbsClient.Cells(logger, traceID) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, cellPresence := range cellPresences { 69 | err = encoder.Encode(cellPresence) 70 | if err != nil { 71 | logger.Error("failed-to-marshal", err) 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /commands/cells_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | ) 14 | 15 | var _ = Describe("Cells", func() { 16 | var ( 17 | fakeBBSClient *fake_bbs.FakeClient 18 | cellPresences []*models.CellPresence 19 | returnedError error 20 | stdout, stderr *gbytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | cellPresences = nil 25 | returnedError = nil 26 | stdout = gbytes.NewBuffer() 27 | stderr = gbytes.NewBuffer() 28 | fakeBBSClient = &fake_bbs.FakeClient{} 29 | }) 30 | 31 | JustBeforeEach(func() { 32 | fakeBBSClient.CellsReturns(cellPresences, returnedError) 33 | }) 34 | 35 | Context("when the bbs responds with cell presences", func() { 36 | BeforeEach(func() { 37 | cellPresences = []*models.CellPresence{ 38 | { 39 | CellId: "cell-1", 40 | RepAddress: "rep-1", 41 | Zone: "zone1", 42 | Capacity: &models.CellCapacity{ 43 | MemoryMb: 1024, 44 | DiskMb: 1024, 45 | Containers: 10, 46 | }, 47 | RootfsProviders: []*models.Provider{ 48 | { 49 | Name: "rootfs1", 50 | }, 51 | }, 52 | RepUrl: "http://rep1.com", 53 | }, 54 | } 55 | }) 56 | 57 | It("prints a json stream of all the cell presences", func() { 58 | err := commands.Cells(stdout, stderr, fakeBBSClient) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | Expect(fakeBBSClient.CellsCallCount()).To(Equal(1)) 62 | 63 | expectedOutput := "" 64 | for _, cellPresence := range cellPresences { 65 | d, err := json.Marshal(cellPresence) 66 | Expect(err).NotTo(HaveOccurred()) 67 | expectedOutput += string(d) + "\n" 68 | } 69 | 70 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 71 | }) 72 | }) 73 | 74 | Context("when the bbs errors", func() { 75 | JustBeforeEach(func() { 76 | fakeBBSClient.CellsReturns(nil, models.ErrUnknownError) 77 | }) 78 | 79 | It("fails with a relevant error", func() { 80 | err := commands.Cells(stdout, stderr, fakeBBSClient) 81 | Expect(err).To(HaveOccurred()) 82 | 83 | Expect(err).To(Equal(models.ErrUnknownError)) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /commands/cfdot_error.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "code.cloudfoundry.org/bbs/models" 9 | ) 10 | 11 | type CFDotError struct { 12 | err error 13 | exitCode int 14 | } 15 | 16 | func (a CFDotError) Error() string { 17 | if err, ok := a.err.(*models.Error); ok { 18 | return fmt.Sprintf(`BBS error 19 | Type %d: %s 20 | Message: %s`, 21 | err.Type, err.Type.String(), err.Message) 22 | } 23 | 24 | return a.err.Error() 25 | } 26 | 27 | func (a CFDotError) ExitCode() int { 28 | return a.exitCode 29 | } 30 | 31 | func NewCFDotError(cmd *cobra.Command, err error) CFDotError { 32 | cmd.SilenceUsage = true 33 | 34 | if _, ok := err.(*models.Error); ok { 35 | return CFDotError{ 36 | err: err, 37 | exitCode: 4, 38 | } 39 | } 40 | 41 | return CFDotError{ 42 | err: err, 43 | exitCode: 5, 44 | } 45 | } 46 | 47 | func NewCFDotComponentError(cmd *cobra.Command, err error) CFDotError { 48 | cmd.SilenceUsage = true 49 | 50 | return CFDotError{ 51 | err: err, 52 | exitCode: 4, 53 | } 54 | } 55 | 56 | func NewCFDotValidationError(cmd *cobra.Command, err error) CFDotError { 57 | return CFDotError{ 58 | err: err, 59 | exitCode: 3, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /commands/cfdot_error_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/bbs/models" 7 | "code.cloudfoundry.org/cfdot/commands" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var _ = Describe("CfDotError", func() { 15 | var ( 16 | err commands.CFDotError 17 | cmd *cobra.Command 18 | ) 19 | 20 | BeforeEach(func() { 21 | cmd = &cobra.Command{} 22 | }) 23 | 24 | Context("when a bbs error occurs", func() { 25 | BeforeEach(func() { 26 | err = commands.NewCFDotError(cmd, &models.Error{ 27 | Type: models.Error_Deadlock, 28 | Message: "The request failed due to a deadlock", 29 | }) 30 | }) 31 | 32 | Describe("Error()", func() { 33 | It("Returns the error code and error message", func() { 34 | Expect(err.Error()).To(ContainSubstring("BBS error")) 35 | Expect(err.Error()).To(ContainSubstring("Type 28: Deadlock")) 36 | Expect(err.Error()).To(ContainSubstring("Message: The request failed due to a deadlock")) 37 | }) 38 | }) 39 | 40 | It("returns an exit code of 4", func() { 41 | Expect(err.ExitCode()).To(Equal(4)) 42 | }) 43 | 44 | It("silence the usage message", func() { 45 | Expect(cmd.SilenceUsage).To(BeTrue()) 46 | }) 47 | }) 48 | 49 | Context("when a component error occurs", func() { 50 | BeforeEach(func() { 51 | err = commands.NewCFDotComponentError(cmd, &models.Error{ 52 | Type: models.Error_UnknownError, 53 | Message: "connection refused", 54 | }) 55 | }) 56 | It("returns an exit code of 4", func() { 57 | Expect(err.ExitCode()).To(Equal(4)) 58 | }) 59 | 60 | It("silence the usage message", func() { 61 | Expect(cmd.SilenceUsage).To(BeTrue()) 62 | }) 63 | }) 64 | 65 | Context("when a validation error occurs", func() { 66 | BeforeEach(func() { 67 | err = commands.NewCFDotValidationError(cmd, errors.New("some error")) 68 | }) 69 | 70 | Describe("Error()", func() { 71 | It("Error() returns the error text", func() { 72 | Expect(err.Error()).To(Equal("some error")) 73 | }) 74 | }) 75 | 76 | It("returns an exit code of 3", func() { 77 | Expect(err.ExitCode()).To(Equal(3)) 78 | }) 79 | 80 | It("does not silence the usage message", func() { 81 | Expect(cmd.SilenceUsage).To(BeFalse()) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /commands/claim_lock.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "code.cloudfoundry.org/locket/models" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // flags 14 | var ( 15 | lockKey string 16 | lockOwner string 17 | lockValue string 18 | ttlInSeconds int 19 | ) 20 | 21 | var claimLockCmd = &cobra.Command{ 22 | Use: "claim-lock", 23 | Short: "Claim Locket lock", 24 | Long: "Claims a Locket lock with the given key, owner, and optional value", 25 | RunE: claimLock, 26 | } 27 | 28 | func init() { 29 | AddLocketFlags(claimLockCmd) 30 | claimLockCmd.Flags().StringVarP(&lockKey, "key", "k", "", "the key of the lock being claimed") 31 | claimLockCmd.Flags().StringVarP(&lockOwner, "owner", "o", "", "the lock owner") 32 | claimLockCmd.Flags().StringVarP(&lockValue, "value", "v", "", "the value associated with the key") 33 | claimLockCmd.Flags().IntVarP(&ttlInSeconds, "ttl", "t", 0, "the TTL for the lock") 34 | RootCmd.AddCommand(claimLockCmd) 35 | } 36 | 37 | func claimLock(cmd *cobra.Command, args []string) error { 38 | err := ValidateClaimLocksArguments(cmd, args, lockKey, lockOwner, lockValue, ttlInSeconds) 39 | if err != nil { 40 | return NewCFDotValidationError(cmd, err) 41 | } 42 | 43 | logger := globalLogger.Session("locket-client") 44 | locketClient, err := helpers.NewLocketClient(logger, cmd, Config) 45 | if err != nil { 46 | return NewCFDotComponentError(cmd, err) 47 | } 48 | 49 | err = ClaimLock( 50 | cmd.OutOrStdout(), 51 | cmd.OutOrStderr(), 52 | locketClient, 53 | lockKey, 54 | lockOwner, 55 | lockValue, 56 | int64(ttlInSeconds), 57 | ) 58 | if err != nil { 59 | return NewCFDotComponentError(cmd, err) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func ValidateClaimLocksArguments(cmd *cobra.Command, args []string, lockKey, lockOwner, lockValue string, ttlInSeconds int) error { 66 | if len(args) > 0 { 67 | return errExtraArguments 68 | } 69 | 70 | var err error 71 | 72 | err = ValidateConflictingShortAndLongFlag("-k", "--key", cmd) 73 | if err != nil { 74 | return NewCFDotValidationError(cmd, err) 75 | } 76 | 77 | err = ValidateConflictingShortAndLongFlag("-o", "--owner", cmd) 78 | if err != nil { 79 | return NewCFDotValidationError(cmd, err) 80 | } 81 | 82 | err = ValidateConflictingShortAndLongFlag("-v", "--value", cmd) 83 | if err != nil { 84 | return NewCFDotValidationError(cmd, err) 85 | } 86 | 87 | err = ValidateConflictingShortAndLongFlag("-t", "--ttl", cmd) 88 | if err != nil { 89 | return NewCFDotValidationError(cmd, err) 90 | } 91 | 92 | if lockKey == "" { 93 | return NewCFDotValidationError(cmd, errors.New("key cannot be empty")) 94 | } 95 | 96 | if lockOwner == "" { 97 | return NewCFDotValidationError(cmd, errors.New("owner cannot be empty")) 98 | } 99 | 100 | if ttlInSeconds <= 0 { 101 | return NewCFDotValidationError(cmd, errors.New("ttl should be an integer greater than zero")) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func ClaimLock( 108 | stdout, stderr io.Writer, 109 | locketClient models.LocketClient, 110 | lockKey, lockOwner, lockValue string, 111 | ttlInSeconds int64) error { 112 | logger := globalLogger.Session("claim-lock") 113 | 114 | req := &models.LockRequest{ 115 | Resource: &models.Resource{ 116 | Key: lockKey, 117 | Owner: lockOwner, 118 | Value: lockValue, 119 | TypeCode: models.LOCK, 120 | }, 121 | TtlInSeconds: ttlInSeconds, 122 | } 123 | _, err := locketClient.Lock(context.Background(), req) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | logger.Info("completed") 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /commands/claim_lock_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | "code.cloudfoundry.org/locket/models" 8 | "code.cloudfoundry.org/locket/models/modelsfakes" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | ) 14 | 15 | var _ = Describe("ClaimLock", func() { 16 | var ( 17 | fakeLocketClient *modelsfakes.FakeLocketClient 18 | stdout, stderr *gbytes.Buffer 19 | ) 20 | 21 | BeforeEach(func() { 22 | stdout = gbytes.NewBuffer() 23 | stderr = gbytes.NewBuffer() 24 | fakeLocketClient = &modelsfakes.FakeLocketClient{} 25 | }) 26 | 27 | Context("when there is no error", func() { 28 | BeforeEach(func() { 29 | response := &models.LockResponse{} 30 | fakeLocketClient.LockReturns(response, nil) 31 | }) 32 | 33 | It("should claim the lock successfully", func() { 34 | err := commands.ClaimLock( 35 | stdout, stderr, fakeLocketClient, 36 | "key", "owner", "value", 60) 37 | Expect(err).NotTo(HaveOccurred()) 38 | 39 | Expect(fakeLocketClient.LockCallCount()).To(Equal(1)) 40 | 41 | _, req, _ := fakeLocketClient.LockArgsForCall(0) 42 | Expect(req).To(Equal(&models.LockRequest{ 43 | Resource: &models.Resource{ 44 | Key: "key", 45 | Owner: "owner", 46 | Value: "value", 47 | TypeCode: models.LOCK, 48 | }, 49 | TtlInSeconds: 60, 50 | })) 51 | }) 52 | }) 53 | 54 | Context("when there is an error", func() { 55 | BeforeEach(func() { 56 | fakeLocketClient.LockReturns(nil, errors.New("random-error")) 57 | }) 58 | 59 | It("should return an error", func() { 60 | err := commands.ClaimLock( 61 | stdout, stderr, fakeLocketClient, 62 | "key", "owner", "value", 60) 63 | Expect(err).To(HaveOccurred()) 64 | Expect(err).To(Equal(errors.New("random-error"))) 65 | }) 66 | }) 67 | 68 | Context("Validations", func() { 69 | var ( 70 | key string 71 | owner string 72 | value string 73 | ttlInSeconds int 74 | ) 75 | 76 | BeforeEach(func() { 77 | key = "key" 78 | owner = "owner" 79 | value = "value" 80 | ttlInSeconds = 1 81 | }) 82 | 83 | It("should not allow for any arguments", func() { 84 | err := commands.ValidateClaimLocksArguments(nil, []string{"1"}, key, owner, value, ttlInSeconds) 85 | Expect(err).To(HaveOccurred()) 86 | Expect(err).To(MatchError(errors.New("Too many arguments specified"))) 87 | }) 88 | 89 | It("should not allow for missing key", func() { 90 | key = "" 91 | err := commands.ValidateClaimLocksArguments(nil, []string{}, key, owner, value, ttlInSeconds) 92 | Expect(err).To(HaveOccurred()) 93 | Expect(err.Error()).To(Equal("key cannot be empty")) 94 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 95 | }) 96 | 97 | It("should not allow for missing owner", func() { 98 | owner = "" 99 | err := commands.ValidateClaimLocksArguments(nil, []string{}, key, owner, value, ttlInSeconds) 100 | Expect(err).To(HaveOccurred()) 101 | Expect(err.Error()).To(Equal("owner cannot be empty")) 102 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 103 | }) 104 | 105 | It("should not allow for negative ttl", func() { 106 | ttlInSeconds = -1 107 | err := commands.ValidateClaimLocksArguments(nil, []string{}, key, owner, value, ttlInSeconds) 108 | Expect(err).To(HaveOccurred()) 109 | Expect(err.Error()).To(Equal("ttl should be an integer greater than zero")) 110 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /commands/claim_presence.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "code.cloudfoundry.org/locket/models" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var claimPresenceCmd = &cobra.Command{ 14 | Use: "claim-presence", 15 | Short: "Claim Locket presence", 16 | Long: "Claims a Locket presence with the given key, owner, and optional value", 17 | RunE: claimPresence, 18 | } 19 | 20 | func init() { 21 | AddLocketFlags(claimPresenceCmd) 22 | claimPresenceCmd.Flags().StringVarP(&lockKey, "key", "k", "", "the key of the presence being claimed") 23 | claimPresenceCmd.Flags().StringVarP(&lockOwner, "owner", "o", "", "the presence owner") 24 | claimPresenceCmd.Flags().StringVarP(&lockValue, "value", "v", "", "the value associated with the presence") 25 | claimPresenceCmd.Flags().IntVarP(&ttlInSeconds, "ttl", "t", 0, "the TTL for the presence") 26 | RootCmd.AddCommand(claimPresenceCmd) 27 | } 28 | 29 | func claimPresence(cmd *cobra.Command, args []string) error { 30 | err := ValidateClaimPresenceArguments(cmd, args, lockKey, lockOwner, lockValue, ttlInSeconds) 31 | if err != nil { 32 | return NewCFDotValidationError(cmd, err) 33 | } 34 | 35 | logger := globalLogger.Session("locket-client") 36 | locketClient, err := helpers.NewLocketClient(logger, cmd, Config) 37 | if err != nil { 38 | return NewCFDotComponentError(cmd, err) 39 | } 40 | 41 | err = ClaimPresence( 42 | cmd.OutOrStdout(), 43 | cmd.OutOrStderr(), 44 | locketClient, 45 | lockKey, 46 | lockOwner, 47 | lockValue, 48 | int64(ttlInSeconds), 49 | ) 50 | if err != nil { 51 | return NewCFDotComponentError(cmd, err) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func ValidateClaimPresenceArguments(cmd *cobra.Command, args []string, lockKey, lockOwner, lockValue string, ttlInSeconds int) error { 58 | if len(args) > 0 { 59 | return errExtraArguments 60 | } 61 | 62 | var err error 63 | 64 | err = ValidateConflictingShortAndLongFlag("-k", "--key", cmd) 65 | if err != nil { 66 | return NewCFDotValidationError(cmd, err) 67 | } 68 | 69 | err = ValidateConflictingShortAndLongFlag("-o", "--owner", cmd) 70 | if err != nil { 71 | return NewCFDotValidationError(cmd, err) 72 | } 73 | 74 | err = ValidateConflictingShortAndLongFlag("-v", "--value", cmd) 75 | if err != nil { 76 | return NewCFDotValidationError(cmd, err) 77 | } 78 | 79 | err = ValidateConflictingShortAndLongFlag("-t", "--ttl", cmd) 80 | if err != nil { 81 | return NewCFDotValidationError(cmd, err) 82 | } 83 | 84 | if lockKey == "" { 85 | return NewCFDotValidationError(cmd, errors.New("key cannot be empty")) 86 | } 87 | 88 | if lockOwner == "" { 89 | return NewCFDotValidationError(cmd, errors.New("owner cannot be empty")) 90 | } 91 | 92 | if ttlInSeconds <= 0 { 93 | return NewCFDotValidationError(cmd, errors.New("ttl should be an integer greater than zero")) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func ClaimPresence( 100 | stdout, stderr io.Writer, 101 | locketClient models.LocketClient, 102 | lockKey, lockOwner, lockValue string, 103 | ttlInSeconds int64) error { 104 | logger := globalLogger.Session("claim-presence") 105 | 106 | req := &models.LockRequest{ 107 | Resource: &models.Resource{ 108 | Key: lockKey, 109 | Owner: lockOwner, 110 | Value: lockValue, 111 | TypeCode: models.PRESENCE, 112 | }, 113 | TtlInSeconds: ttlInSeconds, 114 | } 115 | _, err := locketClient.Lock(context.Background(), req) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | logger.Info("completed") 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /commands/claim_presence_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | "code.cloudfoundry.org/locket/models" 8 | "code.cloudfoundry.org/locket/models/modelsfakes" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | ) 14 | 15 | var _ = Describe("ClaimPresence", func() { 16 | var ( 17 | fakeLocketClient *modelsfakes.FakeLocketClient 18 | stdout, stderr *gbytes.Buffer 19 | ) 20 | 21 | BeforeEach(func() { 22 | stdout = gbytes.NewBuffer() 23 | stderr = gbytes.NewBuffer() 24 | fakeLocketClient = &modelsfakes.FakeLocketClient{} 25 | }) 26 | 27 | Context("when there is no error", func() { 28 | BeforeEach(func() { 29 | response := &models.LockResponse{} 30 | fakeLocketClient.LockReturns(response, nil) 31 | }) 32 | 33 | It("should claim the lock successfully", func() { 34 | err := commands.ClaimPresence( 35 | stdout, stderr, fakeLocketClient, 36 | "key", "owner", "value", 60) 37 | Expect(err).NotTo(HaveOccurred()) 38 | 39 | Expect(fakeLocketClient.LockCallCount()).To(Equal(1)) 40 | 41 | _, req, _ := fakeLocketClient.LockArgsForCall(0) 42 | Expect(req).To(Equal(&models.LockRequest{ 43 | Resource: &models.Resource{ 44 | Key: "key", 45 | Owner: "owner", 46 | Value: "value", 47 | TypeCode: models.PRESENCE, 48 | }, 49 | TtlInSeconds: 60, 50 | })) 51 | }) 52 | }) 53 | 54 | Context("when there is an error", func() { 55 | BeforeEach(func() { 56 | fakeLocketClient.LockReturns(nil, errors.New("random-error")) 57 | }) 58 | 59 | It("should return an error", func() { 60 | err := commands.ClaimPresence( 61 | stdout, stderr, fakeLocketClient, 62 | "key", "owner", "value", 60) 63 | Expect(err).To(HaveOccurred()) 64 | Expect(err).To(Equal(errors.New("random-error"))) 65 | }) 66 | }) 67 | 68 | Context("Validations", func() { 69 | var ( 70 | key string 71 | owner string 72 | value string 73 | ttlInSeconds int 74 | ) 75 | 76 | BeforeEach(func() { 77 | key = "key" 78 | owner = "owner" 79 | value = "value" 80 | ttlInSeconds = 1 81 | }) 82 | 83 | It("should not allow for any arguments", func() { 84 | err := commands.ValidateClaimPresenceArguments(nil, []string{"1"}, key, owner, value, ttlInSeconds) 85 | Expect(err).To(HaveOccurred()) 86 | Expect(err).To(MatchError(errors.New("Too many arguments specified"))) 87 | }) 88 | 89 | It("should not allow for missing key", func() { 90 | key = "" 91 | err := commands.ValidateClaimPresenceArguments(nil, []string{}, key, owner, value, ttlInSeconds) 92 | Expect(err).To(HaveOccurred()) 93 | Expect(err.Error()).To(Equal("key cannot be empty")) 94 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 95 | }) 96 | 97 | It("should not allow for missing owner", func() { 98 | owner = "" 99 | err := commands.ValidateClaimPresenceArguments(nil, []string{}, key, owner, value, ttlInSeconds) 100 | Expect(err).To(HaveOccurred()) 101 | Expect(err.Error()).To(Equal("owner cannot be empty")) 102 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 103 | }) 104 | 105 | It("should not allow for negative ttl", func() { 106 | ttlInSeconds = -1 107 | err := commands.ValidateClaimPresenceArguments(nil, []string{}, key, owner, value, ttlInSeconds) 108 | Expect(err).To(HaveOccurred()) 109 | Expect(err.Error()).To(Equal("ttl should be an integer greater than zero")) 110 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /commands/commands_suite_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "os" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | "code.cloudfoundry.org/cfdot/commands/helpers" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "testing" 12 | ) 13 | 14 | var _ = SynchronizedBeforeSuite(func() []byte { 15 | chmodErr := os.Chmod("fixtures/bbsClientBadPermissions.key", 0300) 16 | Expect(chmodErr).NotTo(HaveOccurred()) 17 | 18 | return nil 19 | }, func(_ []byte) {}) 20 | 21 | var _ = SynchronizedAfterSuite(func() { 22 | }, func() { 23 | chmodErr := os.Chmod("fixtures/bbsClientBadPermissions.key", 0644) 24 | Expect(chmodErr).NotTo(HaveOccurred()) 25 | }) 26 | 27 | var _ = BeforeEach(func() { 28 | commands.Config = helpers.TLSConfig{} 29 | }) 30 | 31 | func TestCommands(t *testing.T) { 32 | RegisterFailHandler(Fail) 33 | RunSpecs(t, "Commands Suite") 34 | } 35 | 36 | func removeFlag(flags map[string]string, toRemove string) []string { 37 | delete(flags, toRemove) 38 | 39 | return buildArgList(flags) 40 | } 41 | 42 | func replaceFlagValue(flags map[string]string, key string, newValue string) []string { 43 | flags[key] = newValue 44 | 45 | return buildArgList(flags) 46 | } 47 | 48 | func mergeFlags(flags map[string]string, moreFlags map[string]string) []string { 49 | for k, v := range moreFlags { 50 | flags[k] = v 51 | } 52 | return buildArgList(flags) 53 | } 54 | 55 | func buildArgList(flags map[string]string) []string { 56 | list := []string{} 57 | 58 | for key, value := range flags { 59 | list = append(list, key+"="+value) 60 | } 61 | 62 | return list 63 | } 64 | -------------------------------------------------------------------------------- /commands/create_desired_lrp.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "code.cloudfoundry.org/bbs/models" 12 | "code.cloudfoundry.org/bbs/trace" 13 | "code.cloudfoundry.org/cfdot/commands/helpers" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var createDesiredLRPCmd = &cobra.Command{ 18 | Use: "create-desired-lrp (SPEC|@FILE)", 19 | Short: "Create a desired LRP", 20 | Long: "Create a desired LRP from the given spec. Spec can either be json encoded desired-lrp, e.g. '{\"process_guid\":\"some-guid\"}' or a file containing json encoded desired-lrp, e.g. @/path/to/spec/file", 21 | RunE: createDesiredLRP, 22 | } 23 | 24 | func init() { 25 | AddBBSAndTimeoutFlags(createDesiredLRPCmd) 26 | RootCmd.AddCommand(createDesiredLRPCmd) 27 | } 28 | 29 | func createDesiredLRP(cmd *cobra.Command, args []string) error { 30 | if len(args) != 1 { 31 | return NewCFDotValidationError(cmd, fmt.Errorf("missing spec argument")) 32 | } 33 | 34 | spec, err := ValidateCreateDesiredLRPArguments(args) 35 | if err != nil { 36 | return NewCFDotValidationError(cmd, err) 37 | } 38 | 39 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 40 | if err != nil { 41 | return NewCFDotError(cmd, err) 42 | } 43 | 44 | err = CreateDesiredLRP(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, spec) 45 | if err != nil { 46 | return NewCFDotError(cmd, err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func ValidateCreateDesiredLRPArguments(args []string) ([]byte, error) { 53 | var desiredLRP *models.DesiredLRP 54 | var err error 55 | var spec []byte 56 | argValue := args[0] 57 | if strings.HasPrefix(argValue, "@") { 58 | _, err := os.Stat(argValue[1:]) 59 | if err != nil { 60 | return nil, err 61 | } 62 | spec, err = os.ReadFile(argValue[1:]) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | } else { 68 | spec = []byte(argValue) 69 | } 70 | err = json.Unmarshal([]byte(spec), &desiredLRP) 71 | if err != nil { 72 | return nil, fmt.Errorf("Invalid JSON: %s", err.Error()) 73 | } 74 | return spec, nil 75 | } 76 | 77 | func CreateDesiredLRP(stdout, stderr io.Writer, bbsClient bbs.Client, spec []byte) error { 78 | traceID := trace.GenerateTraceID() 79 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("create-desired-lrp"), traceID) 80 | 81 | var desiredLRP *models.DesiredLRP 82 | err := json.Unmarshal(spec, &desiredLRP) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = bbsClient.DesireLRP(logger, traceID, desiredLRP) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /commands/create_desired_lrp_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "code.cloudfoundry.org/bbs/fake_bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/cfdot/commands" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | "github.com/openzipkin/zipkin-go/model" 15 | ) 16 | 17 | var _ = Describe("CreateDesiredLRP", func() { 18 | var ( 19 | fakeBBSClient *fake_bbs.FakeClient 20 | stdout, stderr *gbytes.Buffer 21 | expectedDesiredLRP *models.DesiredLRP 22 | spec []byte 23 | ) 24 | 25 | BeforeEach(func() { 26 | fakeBBSClient = &fake_bbs.FakeClient{} 27 | stdout = gbytes.NewBuffer() 28 | stderr = gbytes.NewBuffer() 29 | 30 | expectedDesiredLRP = &models.DesiredLRP{ 31 | ProcessGuid: "some-desired-lrp", 32 | } 33 | var err error 34 | spec, err = json.Marshal(expectedDesiredLRP) 35 | Expect(err).NotTo(HaveOccurred()) 36 | }) 37 | 38 | It("creates the desired lrp", func() { 39 | err := commands.CreateDesiredLRP(stdout, stderr, fakeBBSClient, spec) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | Expect(fakeBBSClient.DesireLRPCallCount()).To(Equal(1)) 43 | _, traceID, lrp := fakeBBSClient.DesireLRPArgsForCall(0) 44 | 45 | _, err = model.TraceIDFromHex(traceID) 46 | Expect(err).NotTo(HaveOccurred()) 47 | 48 | Expect(lrp).To(Equal(expectedDesiredLRP)) 49 | }) 50 | 51 | Context("when a file is passed as an argument", func() { 52 | var filename string 53 | 54 | BeforeEach(func() { 55 | f, err := os.CreateTemp(os.TempDir(), "spec_file") 56 | Expect(err).NotTo(HaveOccurred()) 57 | defer f.Close() 58 | _, err = f.Write(spec) 59 | Expect(err).NotTo(HaveOccurred()) 60 | filename = f.Name() 61 | }) 62 | 63 | It("validates the input file successfully", func() { 64 | args := []string{"@" + filename} 65 | actualSpec, err := commands.ValidateCreateDesiredLRPArguments(args) 66 | Expect(err).NotTo(HaveOccurred()) 67 | Expect(actualSpec).To(Equal(spec)) 68 | }) 69 | 70 | }) 71 | 72 | Context("when the bbs errors", func() { 73 | BeforeEach(func() { 74 | fakeBBSClient.DesireLRPReturns(models.ErrUnknownError) 75 | }) 76 | 77 | It("fails with a relevant error", func() { 78 | err := commands.CreateDesiredLRP(stdout, stderr, fakeBBSClient, []byte("{}")) 79 | Expect(err).To(HaveOccurred()) 80 | Expect(err).To(Equal(models.ErrUnknownError)) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /commands/create_task.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "code.cloudfoundry.org/bbs/models" 12 | "code.cloudfoundry.org/bbs/trace" 13 | "code.cloudfoundry.org/cfdot/commands/helpers" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var createTaskCmd = &cobra.Command{ 18 | Use: "create-task (SPEC|@FILE)", 19 | Short: "Create a Task", 20 | Long: "Create a Task from the given spec. Spec can either be json encoded task, e.g. '{\"task_guid\":\"some-guid\"}' or a file containing json encoded task, e.g. @/path/to/spec/file", 21 | RunE: createTask, 22 | } 23 | 24 | func init() { 25 | AddBBSAndTimeoutFlags(createTaskCmd) 26 | RootCmd.AddCommand(createTaskCmd) 27 | } 28 | 29 | func createTask(cmd *cobra.Command, args []string) error { 30 | if len(args) != 1 { 31 | return NewCFDotValidationError(cmd, fmt.Errorf("missing spec argument")) 32 | } 33 | 34 | spec, err := ValidateCreateTaskArguments(args) 35 | if err != nil { 36 | return NewCFDotValidationError(cmd, err) 37 | } 38 | 39 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 40 | if err != nil { 41 | return NewCFDotError(cmd, err) 42 | } 43 | 44 | err = CreateTask(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, spec) 45 | if err != nil { 46 | return NewCFDotError(cmd, err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func ValidateCreateTaskArguments(args []string) ([]byte, error) { 53 | var spec []byte 54 | var err error 55 | var task *models.Task 56 | 57 | argValue := args[0] 58 | if strings.HasPrefix(argValue, "@") { 59 | _, err := os.Stat(argValue[1:]) 60 | if err != nil { 61 | return nil, err 62 | } 63 | spec, err = os.ReadFile(argValue[1:]) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | } else { 69 | spec = []byte(argValue) 70 | } 71 | err = json.Unmarshal([]byte(spec), &task) 72 | if err != nil { 73 | return nil, fmt.Errorf("Invalid JSON: %s", err.Error()) 74 | } 75 | return spec, nil 76 | } 77 | 78 | func CreateTask(stdout, stderr io.Writer, bbsClient bbs.Client, spec []byte) error { 79 | traceID := trace.GenerateTraceID() 80 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("create-task"), traceID) 81 | 82 | var task *models.Task 83 | err := json.Unmarshal(spec, &task) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | err = bbsClient.DesireTask(logger, traceID, task.TaskGuid, task.Domain, task.TaskDefinition) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /commands/create_task_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "code.cloudfoundry.org/bbs/fake_bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/models/test/model_helpers" 10 | "code.cloudfoundry.org/cfdot/commands" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | "github.com/onsi/gomega/gbytes" 15 | "github.com/openzipkin/zipkin-go/model" 16 | ) 17 | 18 | var _ = Describe("CreateTask", func() { 19 | 20 | var ( 21 | fakeBBSClient *fake_bbs.FakeClient 22 | stdout, stderr *gbytes.Buffer 23 | expectedTask *models.Task 24 | spec []byte 25 | ) 26 | 27 | BeforeEach(func() { 28 | fakeBBSClient = &fake_bbs.FakeClient{} 29 | stdout = gbytes.NewBuffer() 30 | stderr = gbytes.NewBuffer() 31 | 32 | expectedTask = &models.Task{ 33 | TaskGuid: "task-guid", 34 | Domain: "domain", 35 | TaskDefinition: model_helpers.NewValidTaskDefinition(), 36 | } 37 | var err error 38 | spec, err = json.Marshal(expectedTask) 39 | Expect(err).NotTo(HaveOccurred()) 40 | }) 41 | 42 | It("creates the task", func() { 43 | err := commands.CreateTask(stdout, stderr, fakeBBSClient, spec) 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | Expect(fakeBBSClient.DesireTaskCallCount()).To(Equal(1)) 47 | _, traceID, guid, domain, taskDefinition := fakeBBSClient.DesireTaskArgsForCall(0) 48 | Expect(guid).To(Equal(expectedTask.TaskGuid)) 49 | Expect(domain).To(Equal(expectedTask.Domain)) 50 | Expect(taskDefinition).To(Equal(expectedTask.TaskDefinition)) 51 | 52 | _, err = model.TraceIDFromHex(traceID) 53 | Expect(err).NotTo(HaveOccurred()) 54 | }) 55 | 56 | Context("when a file is passed as an argument", func() { 57 | var filename string 58 | 59 | BeforeEach(func() { 60 | f, err := os.CreateTemp(os.TempDir(), "spec_file") 61 | Expect(err).NotTo(HaveOccurred()) 62 | defer f.Close() 63 | _, err = f.Write(spec) 64 | Expect(err).NotTo(HaveOccurred()) 65 | filename = f.Name() 66 | }) 67 | 68 | It("validates the input file successfully", func() { 69 | args := []string{"@" + filename} 70 | actualSpec, err := commands.ValidateCreateTaskArguments(args) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(actualSpec).To(Equal(spec)) 73 | }) 74 | 75 | }) 76 | 77 | Context("when the bbs errors", func() { 78 | BeforeEach(func() { 79 | fakeBBSClient.DesireTaskReturns(models.ErrUnknownError) 80 | }) 81 | 82 | It("fails with a relevant error", func() { 83 | err := commands.CreateTask(stdout, stderr, fakeBBSClient, []byte("{}")) 84 | Expect(err).To(HaveOccurred()) 85 | Expect(err).To(Equal(models.ErrUnknownError)) 86 | }) 87 | }) 88 | 89 | }) 90 | -------------------------------------------------------------------------------- /commands/delete_desired_lrp.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | 6 | "code.cloudfoundry.org/bbs" 7 | "code.cloudfoundry.org/bbs/trace" 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var deleteDesiredLRPCmd = &cobra.Command{ 13 | Use: "delete-desired-lrp PROCESS_GUID", 14 | Short: "Delete a desired LRP", 15 | Long: "Delete a desired LRP with the given process guid.", 16 | RunE: deleteDesiredLRP, 17 | } 18 | 19 | func init() { 20 | AddBBSAndTimeoutFlags(deleteDesiredLRPCmd) 21 | RootCmd.AddCommand(deleteDesiredLRPCmd) 22 | } 23 | 24 | func deleteDesiredLRP(cmd *cobra.Command, args []string) error { 25 | processGuid, err := ValidateDeleteDesiredLRPArguments(args) 26 | if err != nil { 27 | return NewCFDotValidationError(cmd, err) 28 | } 29 | 30 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 31 | if err != nil { 32 | return NewCFDotError(cmd, err) 33 | } 34 | 35 | err = DeleteDesiredLRP(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, processGuid) 36 | if err != nil { 37 | return NewCFDotError(cmd, err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func ValidateDeleteDesiredLRPArguments(args []string) (string, error) { 44 | if len(args) == 0 { 45 | return "", errMissingArguments 46 | } 47 | 48 | if len(args) > 1 { 49 | return "", errExtraArguments 50 | } 51 | 52 | if args[0] == "" { 53 | return "", errInvalidProcessGuid 54 | } 55 | 56 | return args[0], nil 57 | } 58 | 59 | func DeleteDesiredLRP(stdout, stderr io.Writer, bbsClient bbs.Client, processGuid string) error { 60 | traceID := trace.GenerateTraceID() 61 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("delete-desired-lrp"), traceID) 62 | err := bbsClient.RemoveDesiredLRP(logger, traceID, processGuid) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /commands/delete_desired_lrp_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/bbs/fake_bbs" 5 | "code.cloudfoundry.org/bbs/models" 6 | "code.cloudfoundry.org/cfdot/commands" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/openzipkin/zipkin-go/model" 12 | ) 13 | 14 | var _ = Describe("DeleteDesiredLRP", func() { 15 | var ( 16 | fakeBBSClient *fake_bbs.FakeClient 17 | returnedError error 18 | stdout, stderr *gbytes.Buffer 19 | processGuid string 20 | ) 21 | 22 | BeforeEach(func() { 23 | fakeBBSClient = &fake_bbs.FakeClient{} 24 | stdout = gbytes.NewBuffer() 25 | stderr = gbytes.NewBuffer() 26 | 27 | fakeBBSClient.RemoveDesiredLRPReturns(returnedError) 28 | }) 29 | 30 | It("deletes the desired lrp", func() { 31 | err := commands.DeleteDesiredLRP(stdout, stderr, fakeBBSClient, processGuid) 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | Expect(fakeBBSClient.RemoveDesiredLRPCallCount()).To(Equal(1)) 35 | _, traceID, lrp := fakeBBSClient.RemoveDesiredLRPArgsForCall(0) 36 | 37 | _, err = model.TraceIDFromHex(traceID) 38 | Expect(err).NotTo(HaveOccurred()) 39 | Expect(lrp).To(Equal(processGuid)) 40 | 41 | }) 42 | 43 | Context("when the bbs errors", func() { 44 | BeforeEach(func() { 45 | fakeBBSClient.RemoveDesiredLRPReturns(models.ErrUnknownError) 46 | }) 47 | 48 | It("fails with a relevant error", func() { 49 | err := commands.DeleteDesiredLRP(stdout, stderr, fakeBBSClient, "the-process-guid") 50 | Expect(err).To(HaveOccurred()) 51 | Expect(err).To(Equal(models.ErrUnknownError)) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /commands/delete_task.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | 6 | "code.cloudfoundry.org/bbs" 7 | "code.cloudfoundry.org/bbs/trace" 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var deleteTaskCmd = &cobra.Command{ 13 | Use: "delete-task TASK_GUID", 14 | Short: "Delete a Task", 15 | Long: "Delete a Task with the given task guid.", 16 | RunE: deleteTask, 17 | } 18 | 19 | func init() { 20 | AddBBSAndTimeoutFlags(deleteTaskCmd) 21 | RootCmd.AddCommand(deleteTaskCmd) 22 | } 23 | 24 | func deleteTask(cmd *cobra.Command, args []string) error { 25 | taskGuid, err := ValidateDeleteTaskArguments(args) 26 | if err != nil { 27 | return NewCFDotValidationError(cmd, err) 28 | } 29 | 30 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 31 | if err != nil { 32 | return NewCFDotError(cmd, err) 33 | } 34 | 35 | err = DeleteTask(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, taskGuid) 36 | if err != nil { 37 | return NewCFDotError(cmd, err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func ValidateDeleteTaskArguments(args []string) (string, error) { 44 | if len(args) == 0 { 45 | return "", errMissingArguments 46 | } 47 | 48 | if len(args) > 1 { 49 | return "", errExtraArguments 50 | } 51 | 52 | if args[0] == "" { 53 | return "", errInvalidProcessGuid 54 | } 55 | 56 | return args[0], nil 57 | } 58 | 59 | func DeleteTask(stdout, stderr io.Writer, bbsClient bbs.Client, taskGuid string) error { 60 | traceID := trace.GenerateTraceID() 61 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("delete-task"), traceID) 62 | 63 | err := bbsClient.ResolvingTask(logger, traceID, taskGuid) 64 | if err != nil { 65 | return err 66 | } 67 | err = bbsClient.DeleteTask(logger, traceID, taskGuid) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /commands/delete_task_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/bbs/fake_bbs" 5 | "code.cloudfoundry.org/bbs/models" 6 | "code.cloudfoundry.org/cfdot/commands" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/openzipkin/zipkin-go/model" 12 | ) 13 | 14 | var _ = Describe("DeleteTask", func() { 15 | var ( 16 | fakeBBSClient *fake_bbs.FakeClient 17 | returnedError error 18 | stdout, stderr *gbytes.Buffer 19 | taskGuid string 20 | ) 21 | 22 | BeforeEach(func() { 23 | fakeBBSClient = &fake_bbs.FakeClient{} 24 | stdout = gbytes.NewBuffer() 25 | stderr = gbytes.NewBuffer() 26 | 27 | fakeBBSClient.DeleteTaskReturns(returnedError) 28 | }) 29 | 30 | It("deletes the task", func() { 31 | err := commands.DeleteTask(stdout, stderr, fakeBBSClient, taskGuid) 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | Expect(fakeBBSClient.ResolvingTaskCallCount()).To(Equal(1)) 35 | _, traceID, task := fakeBBSClient.ResolvingTaskArgsForCall(0) 36 | 37 | _, err = model.TraceIDFromHex(traceID) 38 | Expect(err).NotTo(HaveOccurred()) 39 | 40 | Expect(task).To(Equal(taskGuid)) 41 | Expect(fakeBBSClient.DeleteTaskCallCount()).To(Equal(1)) 42 | _, traceID2, task := fakeBBSClient.DeleteTaskArgsForCall(0) 43 | Expect(task).To(Equal(taskGuid)) 44 | Expect(traceID2).To(Equal(traceID)) 45 | }) 46 | 47 | Context("when the bbs errors", func() { 48 | BeforeEach(func() { 49 | fakeBBSClient.DeleteTaskReturns(models.ErrUnknownError) 50 | }) 51 | 52 | It("fails with a relevant error", func() { 53 | err := commands.DeleteTask(stdout, stderr, fakeBBSClient, "the-task-guid") 54 | Expect(err).To(HaveOccurred()) 55 | Expect(err).To(Equal(models.ErrUnknownError)) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /commands/desired_lrp.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs/trace" 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var desiredLRPCmd = &cobra.Command{ 15 | Use: "desired-lrp PROCESS_GUID", 16 | Short: "Show the specified desired LRP", 17 | Long: "Show the desired LRP specified by the given process GUID", 18 | RunE: desiredLRP, 19 | } 20 | 21 | func init() { 22 | AddBBSAndTimeoutFlags(desiredLRPCmd) 23 | RootCmd.AddCommand(desiredLRPCmd) 24 | } 25 | 26 | func desiredLRP(cmd *cobra.Command, args []string) error { 27 | processGuid, err := ValidateDesiredLRPArguments(args) 28 | if err != nil { 29 | return NewCFDotValidationError(cmd, err) 30 | } 31 | 32 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 33 | if err != nil { 34 | return NewCFDotError(cmd, err) 35 | } 36 | 37 | err = DesiredLRP(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, processGuid) 38 | if err != nil { 39 | return NewCFDotError(cmd, err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func ValidateDesiredLRPArguments(args []string) (string, error) { 46 | if len(args) == 0 { 47 | return "", errMissingArguments 48 | } 49 | 50 | if len(args) > 1 { 51 | return "", errExtraArguments 52 | } 53 | 54 | if args[0] == "" { 55 | return "", errInvalidProcessGuid 56 | } 57 | 58 | return args[0], nil 59 | } 60 | 61 | func DesiredLRP(stdout, stderr io.Writer, bbsClient bbs.Client, processGuid string) error { 62 | traceID := trace.GenerateTraceID() 63 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("desired-lrp"), traceID) 64 | 65 | desiredLRP, err := bbsClient.DesiredLRPByProcessGuid(logger, traceID, processGuid) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | encoder := json.NewEncoder(stdout) 71 | return encoder.Encode(desiredLRP) 72 | } 73 | -------------------------------------------------------------------------------- /commands/desired_lrp_scheduling_infos.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // flags 15 | var ( 16 | desiredLRPSchedulingInfosDomainFlag string 17 | ) 18 | 19 | var desiredLRPSchedulingInfosCmd = &cobra.Command{ 20 | Use: "desired-lrp-scheduling-infos", 21 | Short: "List desired LRP scheduling infos", 22 | Long: "List desired LRP scheduling infos from the BBS", 23 | RunE: desiredLRPSchedulingInfos, 24 | } 25 | 26 | func init() { 27 | AddBBSAndTimeoutFlags(desiredLRPSchedulingInfosCmd) 28 | desiredLRPSchedulingInfosCmd.Flags().StringVarP(&desiredLRPSchedulingInfosDomainFlag, "domain", "d", "", "retrieve only scheduling infos for the given domain") 29 | RootCmd.AddCommand(desiredLRPSchedulingInfosCmd) 30 | } 31 | 32 | func desiredLRPSchedulingInfos(cmd *cobra.Command, args []string) error { 33 | err := ValidateConflictingShortAndLongFlag("-d", "--domain", cmd) 34 | if err != nil { 35 | return NewCFDotValidationError(cmd, err) 36 | } 37 | 38 | err = ValidateDesiredLRPSchedulingInfosArguments(args) 39 | if err != nil { 40 | return NewCFDotValidationError(cmd, err) 41 | } 42 | 43 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 44 | if err != nil { 45 | return NewCFDotError(cmd, err) 46 | } 47 | 48 | err = DesiredLRPSchedulingInfos(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, desiredLRPSchedulingInfosDomainFlag) 49 | if err != nil { 50 | return NewCFDotError(cmd, err) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func ValidateDesiredLRPSchedulingInfosArguments(args []string) error { 57 | if len(args) > 0 { 58 | return errExtraArguments 59 | } 60 | return nil 61 | } 62 | 63 | func DesiredLRPSchedulingInfos(stdout, stderr io.Writer, bbsClient bbs.Client, domain string) error { 64 | traceID := trace.GenerateTraceID() 65 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("desired-lrp-scheduling-infos"), traceID) 66 | 67 | encoder := json.NewEncoder(stdout) 68 | desiredLRPFilter := models.DesiredLRPFilter{ 69 | Domain: domain, 70 | } 71 | 72 | desiredLRPSchedulingInfos, err := bbsClient.DesiredLRPSchedulingInfos(logger, traceID, desiredLRPFilter) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | for _, info := range desiredLRPSchedulingInfos { 78 | err = encoder.Encode(info) 79 | if err != nil { 80 | logger.Error("failed-to-marshal", err) 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /commands/desired_lrp_scheduling_infos_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/openzipkin/zipkin-go/model" 14 | ) 15 | 16 | var _ = Describe("DesiredLRPSchedulingInfos", func() { 17 | var ( 18 | fakeBBSClient *fake_bbs.FakeClient 19 | schedulingInfos []*models.DesiredLRPSchedulingInfo 20 | stdout, stderr *gbytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | fakeBBSClient = &fake_bbs.FakeClient{} 25 | stdout = gbytes.NewBuffer() 26 | stderr = gbytes.NewBuffer() 27 | 28 | schedulingInfos = []*models.DesiredLRPSchedulingInfo{ 29 | { 30 | Instances: 1, 31 | }, 32 | } 33 | fakeBBSClient.DesiredLRPSchedulingInfosReturns(schedulingInfos, nil) 34 | }) 35 | 36 | It("prints a json stream of all the desired lrp scheduling infos", func() { 37 | err := commands.DesiredLRPSchedulingInfos(stdout, stderr, fakeBBSClient, "domain") 38 | Expect(err).NotTo(HaveOccurred()) 39 | 40 | Expect(fakeBBSClient.DesiredLRPSchedulingInfosCallCount()).To(Equal(1)) 41 | _, traceID, filter := fakeBBSClient.DesiredLRPSchedulingInfosArgsForCall(0) 42 | 43 | _, err = model.TraceIDFromHex(traceID) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(filter).To(Equal(models.DesiredLRPFilter{Domain: "domain"})) 46 | 47 | expectedOutput := "" 48 | for _, info := range schedulingInfos { 49 | d, err := json.Marshal(info) 50 | Expect(err).NotTo(HaveOccurred()) 51 | expectedOutput += string(d) + "\n" 52 | } 53 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 54 | }) 55 | 56 | Context("when the bbs errors", func() { 57 | BeforeEach(func() { 58 | fakeBBSClient.DesiredLRPSchedulingInfosReturns(nil, models.ErrUnknownError) 59 | }) 60 | 61 | It("fails with a relevant error", func() { 62 | err := commands.DesiredLRPSchedulingInfos(stdout, stderr, fakeBBSClient, "") 63 | Expect(err).To(HaveOccurred()) 64 | Expect(err).To(Equal(models.ErrUnknownError)) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /commands/desired_lrp_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/bbs/fake_bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/cfdot/commands" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | ) 15 | 16 | var _ = Describe("DesiredLRP Command", func() { 17 | Context("DesiredLRP", func() { 18 | var ( 19 | fakeBBSClient *fake_bbs.FakeClient 20 | stdout, stderr *gbytes.Buffer 21 | desiredLRP *models.DesiredLRP 22 | ) 23 | 24 | BeforeEach(func() { 25 | fakeBBSClient = &fake_bbs.FakeClient{} 26 | stdout = gbytes.NewBuffer() 27 | stderr = gbytes.NewBuffer() 28 | 29 | desiredLRP = &models.DesiredLRP{ 30 | ProcessGuid: "test-guid", 31 | Instances: 1, 32 | } 33 | 34 | fakeBBSClient.DesiredLRPByProcessGuidReturns(desiredLRP, nil) 35 | }) 36 | 37 | It("writes the json representation of the desired LRP to stdout", func() { 38 | err := commands.DesiredLRP(stdout, stderr, fakeBBSClient, "test-guid") 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | jsonData, err := json.Marshal(desiredLRP) 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | Expect(stdout).To(gbytes.Say(string(jsonData))) 45 | }) 46 | 47 | Context("when fetching the desired lrp fails", func() { 48 | BeforeEach(func() { 49 | fakeBBSClient.DesiredLRPByProcessGuidReturns(nil, errors.New("i failed")) 50 | }) 51 | 52 | It("returns the error", func() { 53 | err := commands.DesiredLRP(stdout, stderr, fakeBBSClient, "test-guid") 54 | Expect(err).To(HaveOccurred()) 55 | }) 56 | }) 57 | }) 58 | 59 | Context("ValidateDesiredLRPArguments", func() { 60 | It("returns the process guid", func() { 61 | processGuid, err := commands.ValidateDesiredLRPArguments([]string{"test-guid"}) 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(processGuid).To(Equal("test-guid")) 64 | }) 65 | 66 | Context("when no arguments are supplied", func() { 67 | It("returns an error", func() { 68 | _, err := commands.ValidateDesiredLRPArguments([]string{}) 69 | Expect(err).To(HaveOccurred()) 70 | }) 71 | }) 72 | 73 | Context("when too many arguments are supplied", func() { 74 | It("returns an error", func() { 75 | _, err := commands.ValidateDesiredLRPArguments([]string{"one", "two"}) 76 | Expect(err).To(HaveOccurred()) 77 | }) 78 | }) 79 | 80 | Context("when invalid arguments are supplied", func() { 81 | It("returns an error", func() { 82 | _, err := commands.ValidateDesiredLRPArguments([]string{""}) 83 | Expect(err).To(HaveOccurred()) 84 | }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /commands/desired_lrps.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // flags 15 | var ( 16 | desiredLRPsDomainFlag string 17 | ) 18 | 19 | var desiredLRPsCmd = &cobra.Command{ 20 | Use: "desired-lrps", 21 | Short: "List desired LRPs", 22 | Long: "List desired LRPs from the BBS", 23 | RunE: desiredLRPs, 24 | } 25 | 26 | func init() { 27 | AddBBSAndTimeoutFlags(desiredLRPsCmd) 28 | desiredLRPsCmd.Flags().StringVarP(&desiredLRPsDomainFlag, "domain", "d", "", "retrieve only desired lrps for the given domain") 29 | RootCmd.AddCommand(desiredLRPsCmd) 30 | } 31 | 32 | func desiredLRPs(cmd *cobra.Command, args []string) error { 33 | err := ValidateConflictingShortAndLongFlag("-d", "--domain", cmd) 34 | if err != nil { 35 | return NewCFDotValidationError(cmd, err) 36 | } 37 | 38 | err = ValidateDesiredLRPsArguments(args) 39 | if err != nil { 40 | return NewCFDotValidationError(cmd, err) 41 | } 42 | 43 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 44 | if err != nil { 45 | return NewCFDotError(cmd, err) 46 | } 47 | 48 | err = DesiredLRPs(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, desiredLRPsDomainFlag) 49 | if err != nil { 50 | return NewCFDotError(cmd, err) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func ValidateDesiredLRPsArguments(args []string) error { 57 | if len(args) > 0 { 58 | return errExtraArguments 59 | } 60 | return nil 61 | } 62 | 63 | func DesiredLRPs(stdout, stderr io.Writer, bbsClient bbs.Client, domain string) error { 64 | traceID := trace.GenerateTraceID() 65 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("desired-lrps"), traceID) 66 | 67 | desiredLRPFilter := models.DesiredLRPFilter{Domain: domain} 68 | 69 | desiredLRPs, err := bbsClient.DesiredLRPs(logger, traceID, desiredLRPFilter) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | encoder := json.NewEncoder(stdout) 75 | for _, lrp := range desiredLRPs { 76 | err = encoder.Encode(lrp) 77 | if err != nil { 78 | logger.Error("failed-to-marshal", err) 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /commands/desired_lrps_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/openzipkin/zipkin-go/model" 14 | ) 15 | 16 | var _ = Describe("DesiredLRPs", func() { 17 | var ( 18 | fakeBBSClient *fake_bbs.FakeClient 19 | desiredLrps []*models.DesiredLRP 20 | returnedError error 21 | stdout, stderr *gbytes.Buffer 22 | ) 23 | 24 | BeforeEach(func() { 25 | fakeBBSClient = &fake_bbs.FakeClient{} 26 | stdout = gbytes.NewBuffer() 27 | stderr = gbytes.NewBuffer() 28 | 29 | desiredLrps = []*models.DesiredLRP{ 30 | { 31 | Instances: 1, 32 | }, 33 | } 34 | fakeBBSClient.DesiredLRPsReturns(desiredLrps, returnedError) 35 | }) 36 | 37 | It("prints a json stream of all the desired lrps", func() { 38 | err := commands.DesiredLRPs(stdout, stderr, fakeBBSClient, "domain") 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | Expect(fakeBBSClient.DesiredLRPsCallCount()).To(Equal(1)) 42 | _, traceID, filter := fakeBBSClient.DesiredLRPsArgsForCall(0) 43 | 44 | _, err = model.TraceIDFromHex(traceID) 45 | Expect(err).NotTo(HaveOccurred()) 46 | Expect(filter).To(Equal(models.DesiredLRPFilter{Domain: "domain"})) 47 | 48 | expectedOutput := "" 49 | for _, info := range desiredLrps { 50 | d, err := json.Marshal(info) 51 | Expect(err).NotTo(HaveOccurred()) 52 | expectedOutput += string(d) + "\n" 53 | } 54 | 55 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 56 | }) 57 | 58 | Context("when the bbs errors", func() { 59 | BeforeEach(func() { 60 | fakeBBSClient.DesiredLRPsReturns(nil, models.ErrUnknownError) 61 | }) 62 | 63 | It("fails with a relevant error", func() { 64 | err := commands.DesiredLRPs(stdout, stderr, fakeBBSClient, "domain") 65 | Expect(err).To(HaveOccurred()) 66 | Expect(err).To(Equal(models.ErrUnknownError)) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /commands/domains.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/trace" 9 | 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var domainsCmd = &cobra.Command{ 15 | Use: "domains", 16 | Short: "List domains", 17 | Long: "List fresh domains from the BBS", 18 | RunE: domains, 19 | } 20 | 21 | func init() { 22 | AddBBSAndTimeoutFlags(domainsCmd) 23 | RootCmd.AddCommand(domainsCmd) 24 | } 25 | 26 | func domains(cmd *cobra.Command, args []string) error { 27 | err := ValidateDomainsArguments(args) 28 | if err != nil { 29 | return NewCFDotValidationError(cmd, err) 30 | } 31 | 32 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 33 | if err != nil { 34 | return NewCFDotError(cmd, err) 35 | } 36 | 37 | err = Domains(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient) 38 | if err != nil { 39 | return NewCFDotError(cmd, err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func ValidateDomainsArguments(args []string) error { 46 | if len(args) > 0 { 47 | return errExtraArguments 48 | } 49 | return nil 50 | } 51 | 52 | func Domains(stdout, stderr io.Writer, bbsClient bbs.Client) error { 53 | traceID := trace.GenerateTraceID() 54 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("domains"), traceID) 55 | 56 | encoder := json.NewEncoder(stdout) 57 | 58 | domains, err := bbsClient.Domains(logger, traceID) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | for _, domain := range domains { 64 | err = encoder.Encode(domain) 65 | if err != nil { 66 | logger.Error("failed-to-marshal", err) 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /commands/domains_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/bbs/fake_bbs" 5 | "code.cloudfoundry.org/bbs/models" 6 | "code.cloudfoundry.org/cfdot/commands" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | ) 12 | 13 | var _ = Describe("Domains", func() { 14 | var ( 15 | fakeBBSClient *fake_bbs.FakeClient 16 | stdout, stderr *gbytes.Buffer 17 | ) 18 | 19 | BeforeEach(func() { 20 | stdout = gbytes.NewBuffer() 21 | stderr = gbytes.NewBuffer() 22 | fakeBBSClient = &fake_bbs.FakeClient{} 23 | }) 24 | 25 | Context("when the bbs responds with domains", func() { 26 | BeforeEach(func() { 27 | fakeBBSClient.DomainsReturns([]string{"domain-1", "domain-2"}, nil) 28 | }) 29 | 30 | It("prints a json stream of all the domains", func() { 31 | err := commands.Domains(stdout, stderr, fakeBBSClient) 32 | Expect(err).NotTo(HaveOccurred()) 33 | Expect(stdout).To(gbytes.Say(`"domain-1"\n"domain-2"\n`)) 34 | }) 35 | }) 36 | 37 | Context("when the bbs responds with no domains", func() { 38 | BeforeEach(func() { 39 | fakeBBSClient.DomainsReturns([]string{}, nil) 40 | }) 41 | 42 | It("returns an empty response", func() { 43 | err := commands.Domains(stdout, stderr, fakeBBSClient) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(stdout.Contents()).To(BeEmpty()) 46 | }) 47 | }) 48 | 49 | Context("when the bbs errors", func() { 50 | BeforeEach(func() { 51 | fakeBBSClient.DomainsReturns(nil, models.ErrUnknownError) 52 | }) 53 | 54 | It("fails with a relevant error", func() { 55 | err := commands.Domains(stdout, stderr, fakeBBSClient) 56 | Expect(err).To(HaveOccurred()) 57 | Expect(err).To(Equal(models.ErrUnknownError)) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /commands/fixtures/bbsCACert.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/bbsCACert.crt -------------------------------------------------------------------------------- /commands/fixtures/bbsClient.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/bbsClient.crt -------------------------------------------------------------------------------- /commands/fixtures/bbsClient.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/bbsClient.key -------------------------------------------------------------------------------- /commands/fixtures/bbsClientBadPermissions.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/bbsClientBadPermissions.key -------------------------------------------------------------------------------- /commands/fixtures/randmoClient.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/randmoClient.key -------------------------------------------------------------------------------- /commands/fixtures/randomCACert.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/randomCACert.crt -------------------------------------------------------------------------------- /commands/fixtures/randomClient.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/randomClient.crt -------------------------------------------------------------------------------- /commands/fixtures/randomClient.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/cfdot/89695eb382a0dba95fb8845c2a921206737cf62a/commands/fixtures/randomClient.key -------------------------------------------------------------------------------- /commands/help.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var helpCmd = &cobra.Command{ 10 | Use: "help CMD", 11 | Short: "Get help on [command]", 12 | Long: "Get help on using cfdot commands", 13 | RunE: help, 14 | } 15 | 16 | func init() { 17 | RootCmd.AddCommand(helpCmd) 18 | } 19 | 20 | func help(cmd *cobra.Command, args []string) error { 21 | if len(args) == 0 || args[0] == "" { 22 | cmd.HelpFunc()(cmd, args) 23 | return nil 24 | } 25 | 26 | for _, c := range cmd.Root().Commands() { 27 | if c.Name() == args[0] { 28 | c.HelpFunc()(c, args) 29 | return nil 30 | } 31 | } 32 | 33 | return NewCFDotValidationError(cmd, fmt.Errorf("'%s' is not a valid command", args[0])) 34 | } 35 | -------------------------------------------------------------------------------- /commands/helpers/clients.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/lager/v3" 9 | "code.cloudfoundry.org/locket" 10 | locketmodels "code.cloudfoundry.org/locket/models" 11 | "code.cloudfoundry.org/rep" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const ( 16 | clientSessionCacheSize int = -1 17 | maxIdleConnsPerHost int = -1 18 | ) 19 | 20 | type TLSConfig struct { 21 | BBSUrl string 22 | LocketApiLocation string 23 | CACertFile string 24 | CertFile string 25 | KeyFile string 26 | SkipCertVerify bool 27 | Timeout int 28 | } 29 | 30 | func NewBBSClient(cmd *cobra.Command, bbsClientConfig TLSConfig) (bbs.Client, error) { 31 | var err error 32 | var client bbs.Client 33 | 34 | if !strings.HasPrefix(bbsClientConfig.BBSUrl, "https") { 35 | client, err = bbs.NewClientWithConfig(bbs.ClientConfig{ 36 | URL: bbsClientConfig.BBSUrl, 37 | Retries: 1, 38 | RequestTimeout: time.Duration(bbsClientConfig.Timeout) * time.Second, 39 | }) 40 | } else { 41 | client, err = bbs.NewClientWithConfig( 42 | bbs.ClientConfig{ 43 | URL: bbsClientConfig.BBSUrl, 44 | IsTLS: true, 45 | InsecureSkipVerify: bbsClientConfig.SkipCertVerify, 46 | CAFile: bbsClientConfig.CACertFile, 47 | CertFile: bbsClientConfig.CertFile, 48 | KeyFile: bbsClientConfig.KeyFile, 49 | ClientSessionCacheSize: clientSessionCacheSize, 50 | MaxIdleConnsPerHost: maxIdleConnsPerHost, 51 | Retries: 1, 52 | RequestTimeout: time.Duration(bbsClientConfig.Timeout) * time.Second, 53 | }, 54 | ) 55 | } 56 | 57 | return client, err 58 | } 59 | 60 | func NewRepClient(clientFactory rep.ClientFactory, address, url string) (rep.Client, error) { 61 | traceID := "" 62 | return clientFactory.CreateClient(address, traceID, url) 63 | } 64 | 65 | func NewLocketClient(logger lager.Logger, cmd *cobra.Command, locketClientConfig TLSConfig) (locketmodels.LocketClient, error) { 66 | var err error 67 | var client locketmodels.LocketClient 68 | config := locket.ClientLocketConfig{ 69 | LocketAddress: locketClientConfig.LocketApiLocation, 70 | LocketCACertFile: locketClientConfig.CACertFile, 71 | LocketClientCertFile: locketClientConfig.CertFile, 72 | LocketClientKeyFile: locketClientConfig.KeyFile, 73 | } 74 | 75 | if locketClientConfig.SkipCertVerify { 76 | client, err = locket.NewClientSkipCertVerify( 77 | logger, 78 | config, 79 | ) 80 | } else { 81 | client, err = locket.NewClient( 82 | logger, 83 | config, 84 | ) 85 | } 86 | 87 | return client, err 88 | } 89 | 90 | func (config *TLSConfig) Merge(newConfig TLSConfig) { 91 | if newConfig.BBSUrl != "" { 92 | config.BBSUrl = newConfig.BBSUrl 93 | } 94 | if newConfig.LocketApiLocation != "" { 95 | config.LocketApiLocation = newConfig.LocketApiLocation 96 | } 97 | if newConfig.Timeout != 0 { 98 | config.Timeout = newConfig.Timeout 99 | } 100 | if newConfig.KeyFile != "" { 101 | config.KeyFile = newConfig.KeyFile 102 | } 103 | if newConfig.CACertFile != "" { 104 | config.CACertFile = newConfig.CACertFile 105 | } 106 | if newConfig.CertFile != "" { 107 | config.CertFile = newConfig.CertFile 108 | } 109 | config.SkipCertVerify = config.SkipCertVerify || newConfig.SkipCertVerify 110 | } 111 | -------------------------------------------------------------------------------- /commands/helpers/package.go: -------------------------------------------------------------------------------- 1 | package helpers // import "code.cloudfoundry.org/cfdot/commands/helpers" 2 | -------------------------------------------------------------------------------- /commands/locket_flags.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | locketApiLocation string 12 | ) 13 | 14 | // errors 15 | var ( 16 | errMissingLocketUrl = errors.New("Locket API Location not set. Please specify one with the '--locketAPILocation' flag or the 'LOCKET_API_LOCATION' environment variable.") 17 | ) 18 | 19 | func AddLocketFlags(cmd *cobra.Command) { 20 | AddTLSFlags(cmd) 21 | cmd.Flags().StringVar(&locketApiLocation, "locketAPILocation", "", "Hostname:Port of Locket server to target [environment variable equivalent: LOCKET_API_LOCATION]") 22 | cmd.PreRunE = LocketPrehook 23 | } 24 | 25 | func LocketPrehook(cmd *cobra.Command, args []string) error { 26 | if err := setLocketFlags(cmd, args); err != nil { 27 | return err 28 | } 29 | return tlsPreHook(cmd, args) 30 | } 31 | 32 | func setLocketFlags(cmd *cobra.Command, args []string) error { 33 | if locketApiLocation == "" { 34 | locketApiLocation = os.Getenv("LOCKET_API_LOCATION") 35 | } 36 | 37 | Config.LocketApiLocation = locketApiLocation 38 | if Config.LocketApiLocation == "" { 39 | return NewCFDotValidationError(cmd, errMissingLocketUrl) 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /commands/locket_flags_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "os" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var _ = Describe("Locket Flags", func() { 15 | var validTLSFlags map[string]string 16 | var dummyCmd *cobra.Command 17 | var err error 18 | var output *gbytes.Buffer 19 | 20 | BeforeEach(func() { 21 | dummyCmd = &cobra.Command{ 22 | Use: "dummy", 23 | Run: func(cmd *cobra.Command, args []string) {}, 24 | } 25 | commands.AddLocketFlags(dummyCmd) 26 | output = gbytes.NewBuffer() 27 | dummyCmd.SetOut(output) 28 | 29 | validTLSFlags = map[string]string{ 30 | "--locketAPILocation": "127.0.0.1:9802", 31 | "--skipCertVerify": "false", 32 | "--caCertFile": "fixtures/bbsCACert.crt", 33 | "--clientCertFile": "fixtures/bbsClient.crt", 34 | "--clientKeyFile": "fixtures/bbsClient.key", 35 | } 36 | }) 37 | 38 | JustBeforeEach(func() { 39 | err = dummyCmd.PreRunE(dummyCmd, dummyCmd.Flags().Args()) 40 | }) 41 | 42 | Describe("locketAPILocation", func() { 43 | Context("when the --locketAPILocation isn't given", func() { 44 | BeforeEach(func() { 45 | parseFlagsErr := dummyCmd.ParseFlags(removeFlag(validTLSFlags, "--locketAPILocation")) 46 | Expect(parseFlagsErr).NotTo(HaveOccurred()) 47 | }) 48 | 49 | It("returns an error message", func() { 50 | Expect(err).To(MatchError( 51 | "Locket API Location not set. Please specify one with the '--locketAPILocation' flag or the 'LOCKET_API_LOCATION' environment variable.", 52 | )) 53 | }) 54 | 55 | It("exits with code 3", func() { 56 | argsErr, ok := err.(commands.CFDotError) 57 | Expect(ok).To(BeTrue()) 58 | Expect(argsErr.ExitCode()).To(Equal(3)) 59 | }) 60 | }) 61 | 62 | Context("when a LOCKET_API_LOCATION environment variable is specified", func() { 63 | AfterEach(func() { 64 | os.Unsetenv("LOCKET_API_LOCATION") 65 | }) 66 | 67 | Context("when the LOCKET_API_LOCATION is valid", func() { 68 | BeforeEach(func() { 69 | parseFlagsErr := dummyCmd.ParseFlags(removeFlag(validTLSFlags, "--locketAPILocation")) 70 | Expect(parseFlagsErr).NotTo(HaveOccurred()) 71 | os.Setenv("LOCKET_API_LOCATION", "example.com:9083") 72 | }) 73 | 74 | It("does not error", func() { 75 | Expect(err).NotTo(HaveOccurred()) 76 | }) 77 | }) 78 | 79 | Context("when the --locketAPILocation flag is also specified", func() { 80 | BeforeEach(func() { 81 | parseFlagsErr := dummyCmd.ParseFlags(buildArgList(validTLSFlags)) 82 | Expect(parseFlagsErr).NotTo(HaveOccurred()) 83 | os.Setenv("LOCKET_API_LOCATION", "broken url") 84 | }) 85 | 86 | It("uses the value from the flag instead of the environment variable", func() { 87 | Expect(err).NotTo(HaveOccurred()) 88 | }) 89 | }) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /commands/locks.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "code.cloudfoundry.org/locket/models" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var locksCmd = &cobra.Command{ 15 | Use: "locks", 16 | Short: "List Locket locks", 17 | Long: "List locks from Locket", 18 | RunE: locks, 19 | } 20 | 21 | func init() { 22 | AddLocketFlags(locksCmd) 23 | RootCmd.AddCommand(locksCmd) 24 | } 25 | 26 | func locks(cmd *cobra.Command, args []string) error { 27 | err := ValidateLocksArguments(args) 28 | if err != nil { 29 | return NewCFDotValidationError(cmd, err) 30 | } 31 | 32 | logger := globalLogger.Session("locket-client") 33 | locketClient, err := helpers.NewLocketClient(logger, cmd, Config) 34 | if err != nil { 35 | return NewCFDotComponentError(cmd, err) 36 | } 37 | 38 | err = Locks( 39 | cmd.OutOrStdout(), 40 | cmd.OutOrStderr(), 41 | locketClient, 42 | ) 43 | if err != nil { 44 | return NewCFDotComponentError(cmd, err) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func ValidateLocksArguments(args []string) error { 51 | if len(args) > 0 { 52 | return errExtraArguments 53 | } 54 | return nil 55 | } 56 | 57 | func Locks(stdout, stderr io.Writer, locketClient models.LocketClient) error { 58 | logger := globalLogger.Session("locks") 59 | 60 | encoder := json.NewEncoder(stdout) 61 | 62 | req := &models.FetchAllRequest{TypeCode: models.LOCK} 63 | resp, err := locketClient.FetchAll(context.Background(), req) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, lock := range resp.Resources { 69 | err = encoder.Encode(lock) 70 | if err != nil { 71 | logger.Error("failed-to-marshal", err) 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /commands/locks_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/cfdot/commands" 8 | "code.cloudfoundry.org/locket/models" 9 | "code.cloudfoundry.org/locket/models/modelsfakes" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | ) 15 | 16 | var _ = Describe("Locks", func() { 17 | var ( 18 | fakeLocketClient *modelsfakes.FakeLocketClient 19 | stdout, stderr *gbytes.Buffer 20 | ) 21 | 22 | BeforeEach(func() { 23 | stdout = gbytes.NewBuffer() 24 | stderr = gbytes.NewBuffer() 25 | fakeLocketClient = &modelsfakes.FakeLocketClient{} 26 | }) 27 | 28 | Context("when the locket responds with locks", func() { 29 | var ( 30 | resources *models.FetchAllResponse 31 | ) 32 | BeforeEach(func() { 33 | resources = &models.FetchAllResponse{ 34 | Resources: []*models.Resource{ 35 | &models.Resource{ 36 | Key: "key", 37 | Owner: "owner", 38 | Value: "value", 39 | Type: "type", 40 | }, 41 | }, 42 | } 43 | fakeLocketClient.FetchAllReturns(resources, nil) 44 | }) 45 | 46 | It("prints a json stream of all the locks", func() { 47 | err := commands.Locks(stdout, stderr, fakeLocketClient) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | Expect(fakeLocketClient.FetchAllCallCount()).To(Equal(1)) 51 | 52 | _, req, _ := fakeLocketClient.FetchAllArgsForCall(0) 53 | Expect(req).To(Equal(&models.FetchAllRequest{TypeCode: models.LOCK})) 54 | 55 | d, err := json.Marshal(resources.Resources[0]) 56 | Expect(err).NotTo(HaveOccurred()) 57 | expectedOutput := string(d) + "\n" 58 | 59 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 60 | }) 61 | }) 62 | 63 | Context("when the locket errors", func() { 64 | JustBeforeEach(func() { 65 | fakeLocketClient.FetchAllReturns(nil, errors.New("boom")) 66 | }) 67 | 68 | It("fails with a relevant error", func() { 69 | err := commands.Locks(stdout, stderr, fakeLocketClient) 70 | Expect(err).To(HaveOccurred()) 71 | Expect(err).To(Equal(errors.New("boom"))) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /commands/package.go: -------------------------------------------------------------------------------- 1 | package commands // import "code.cloudfoundry.org/cfdot/commands" 2 | -------------------------------------------------------------------------------- /commands/presences.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "code.cloudfoundry.org/locket/models" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var presencesCmd = &cobra.Command{ 15 | Use: "presences", 16 | Short: "List Locket presences", 17 | Long: "List presences registered in Locket", 18 | RunE: presences, 19 | } 20 | 21 | func init() { 22 | AddLocketFlags(presencesCmd) 23 | RootCmd.AddCommand(presencesCmd) 24 | } 25 | 26 | func presences(cmd *cobra.Command, args []string) error { 27 | err := ValidatePresencesArguments(args) 28 | if err != nil { 29 | return NewCFDotValidationError(cmd, err) 30 | } 31 | 32 | logger := globalLogger.Session("locket-client") 33 | locketClient, err := helpers.NewLocketClient(logger, cmd, Config) 34 | if err != nil { 35 | return NewCFDotComponentError(cmd, err) 36 | } 37 | 38 | err = Presences( 39 | cmd.OutOrStdout(), 40 | cmd.OutOrStderr(), 41 | locketClient, 42 | ) 43 | if err != nil { 44 | return NewCFDotComponentError(cmd, err) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func ValidatePresencesArguments(args []string) error { 51 | if len(args) > 0 { 52 | return errExtraArguments 53 | } 54 | return nil 55 | } 56 | 57 | func Presences(stdout, stderr io.Writer, locketClient models.LocketClient) error { 58 | logger := globalLogger.Session("presences") 59 | 60 | encoder := json.NewEncoder(stdout) 61 | 62 | req := &models.FetchAllRequest{TypeCode: models.PRESENCE} 63 | resp, err := locketClient.FetchAll(context.Background(), req) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, presence := range resp.Resources { 69 | err = encoder.Encode(presence) 70 | if err != nil { 71 | logger.Error("failed-to-marshal", err) 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /commands/presences_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/cfdot/commands" 8 | "code.cloudfoundry.org/locket/models" 9 | "code.cloudfoundry.org/locket/models/modelsfakes" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | ) 15 | 16 | var _ = Describe("Presences", func() { 17 | var ( 18 | fakeLocketClient *modelsfakes.FakeLocketClient 19 | stdout, stderr *gbytes.Buffer 20 | ) 21 | 22 | BeforeEach(func() { 23 | stdout = gbytes.NewBuffer() 24 | stderr = gbytes.NewBuffer() 25 | fakeLocketClient = &modelsfakes.FakeLocketClient{} 26 | }) 27 | 28 | Context("when the locket responds with presences", func() { 29 | var ( 30 | resources *models.FetchAllResponse 31 | ) 32 | BeforeEach(func() { 33 | resources = &models.FetchAllResponse{ 34 | Resources: []*models.Resource{ 35 | &models.Resource{ 36 | Key: "key", 37 | Owner: "owner", 38 | Value: "value", 39 | TypeCode: models.PRESENCE, 40 | }, 41 | }, 42 | } 43 | fakeLocketClient.FetchAllReturns(resources, nil) 44 | }) 45 | 46 | It("prints a json stream of all the locks", func() { 47 | err := commands.Presences(stdout, stderr, fakeLocketClient) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | Expect(fakeLocketClient.FetchAllCallCount()).To(Equal(1)) 51 | 52 | _, req, _ := fakeLocketClient.FetchAllArgsForCall(0) 53 | Expect(req).To(Equal(&models.FetchAllRequest{TypeCode: models.PRESENCE})) 54 | 55 | d, err := json.Marshal(resources.Resources[0]) 56 | Expect(err).NotTo(HaveOccurred()) 57 | expectedOutput := string(d) + "\n" 58 | 59 | Expect(string(stdout.Contents())).To(Equal(expectedOutput)) 60 | }) 61 | }) 62 | 63 | Context("when the locket errors", func() { 64 | JustBeforeEach(func() { 65 | fakeLocketClient.FetchAllReturns(nil, errors.New("boom")) 66 | }) 67 | 68 | It("fails with a relevant error", func() { 69 | err := commands.Presences(stdout, stderr, fakeLocketClient) 70 | Expect(err).To(HaveOccurred()) 71 | Expect(err).To(Equal(errors.New("boom"))) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /commands/release_lock.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "code.cloudfoundry.org/cfdot/commands/helpers" 9 | "code.cloudfoundry.org/locket/models" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var releaseLockCmd = &cobra.Command{ 14 | Use: "release-lock", 15 | Short: "Release Locket lock", 16 | Long: "Releases a Locket lock with the given key and owner", 17 | RunE: releaseLock, 18 | } 19 | 20 | func init() { 21 | AddLocketFlags(releaseLockCmd) 22 | releaseLockCmd.Flags().StringVarP(&lockKey, "key", "k", "", "the key of the lock being releaseed") 23 | releaseLockCmd.Flags().StringVarP(&lockOwner, "owner", "o", "", "the lock owner") 24 | RootCmd.AddCommand(releaseLockCmd) 25 | } 26 | 27 | func releaseLock(cmd *cobra.Command, args []string) error { 28 | err := ValidateReleaseLocksArguments(cmd, args, lockKey, lockOwner) 29 | if err != nil { 30 | return NewCFDotValidationError(cmd, err) 31 | } 32 | 33 | logger := globalLogger.Session("locket-client") 34 | locketClient, err := helpers.NewLocketClient(logger, cmd, Config) 35 | if err != nil { 36 | return NewCFDotComponentError(cmd, err) 37 | } 38 | 39 | err = ReleaseLock( 40 | cmd.OutOrStdout(), 41 | cmd.OutOrStderr(), 42 | locketClient, 43 | lockKey, 44 | lockOwner, 45 | ) 46 | if err != nil { 47 | return NewCFDotComponentError(cmd, err) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func ValidateReleaseLocksArguments(cmd *cobra.Command, args []string, lockKey, lockOwner string) error { 54 | if len(args) > 0 { 55 | return errExtraArguments 56 | } 57 | 58 | var err error 59 | 60 | err = ValidateConflictingShortAndLongFlag("-k", "--key", cmd) 61 | if err != nil { 62 | return NewCFDotValidationError(cmd, err) 63 | } 64 | 65 | err = ValidateConflictingShortAndLongFlag("-o", "--owner", cmd) 66 | if err != nil { 67 | return NewCFDotValidationError(cmd, err) 68 | } 69 | 70 | if lockKey == "" { 71 | return NewCFDotValidationError(cmd, errors.New("key cannot be empty")) 72 | } 73 | 74 | if lockOwner == "" { 75 | return NewCFDotValidationError(cmd, errors.New("owner cannot be empty")) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func ReleaseLock( 82 | stdout, stderr io.Writer, 83 | locketClient models.LocketClient, 84 | lockKey, lockOwner string, 85 | ) error { 86 | logger := globalLogger.Session("release-lock") 87 | 88 | req := &models.ReleaseRequest{ 89 | Resource: &models.Resource{ 90 | Key: lockKey, 91 | Owner: lockOwner, 92 | }, 93 | } 94 | _, err := locketClient.Release(context.Background(), req) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | logger.Info("completed") 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /commands/release_lock_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | "code.cloudfoundry.org/locket/models" 8 | "code.cloudfoundry.org/locket/models/modelsfakes" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | ) 14 | 15 | var _ = Describe("ReleaseLock", func() { 16 | var ( 17 | fakeLocketClient *modelsfakes.FakeLocketClient 18 | stdout, stderr *gbytes.Buffer 19 | ) 20 | 21 | BeforeEach(func() { 22 | stdout = gbytes.NewBuffer() 23 | stderr = gbytes.NewBuffer() 24 | fakeLocketClient = &modelsfakes.FakeLocketClient{} 25 | }) 26 | 27 | Context("when there is no error", func() { 28 | BeforeEach(func() { 29 | response := &models.ReleaseResponse{} 30 | fakeLocketClient.ReleaseReturns(response, nil) 31 | }) 32 | 33 | It("should release the lock successfully", func() { 34 | err := commands.ReleaseLock( 35 | stdout, stderr, fakeLocketClient, "key", "owner") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | Expect(fakeLocketClient.ReleaseCallCount()).To(Equal(1)) 39 | 40 | _, req, _ := fakeLocketClient.ReleaseArgsForCall(0) 41 | Expect(req).To(Equal(&models.ReleaseRequest{ 42 | Resource: &models.Resource{ 43 | Key: "key", 44 | Owner: "owner", 45 | }, 46 | })) 47 | }) 48 | }) 49 | 50 | Context("when there is an error", func() { 51 | BeforeEach(func() { 52 | fakeLocketClient.ReleaseReturns(nil, errors.New("random-error")) 53 | }) 54 | 55 | It("should return an error", func() { 56 | err := commands.ReleaseLock( 57 | stdout, stderr, fakeLocketClient, "key", "owner") 58 | Expect(err).To(HaveOccurred()) 59 | Expect(err).To(Equal(errors.New("random-error"))) 60 | }) 61 | }) 62 | 63 | Context("Validations", func() { 64 | var ( 65 | key string 66 | owner string 67 | ) 68 | 69 | BeforeEach(func() { 70 | key = "key" 71 | owner = "owner" 72 | }) 73 | 74 | It("should not allow for any arguments", func() { 75 | err := commands.ValidateReleaseLocksArguments(nil, []string{"1"}, key, owner) 76 | Expect(err).To(HaveOccurred()) 77 | Expect(err).To(MatchError(errors.New("Too many arguments specified"))) 78 | }) 79 | 80 | It("should not allow for missing key", func() { 81 | key = "" 82 | err := commands.ValidateReleaseLocksArguments(nil, []string{}, key, owner) 83 | Expect(err).To(HaveOccurred()) 84 | Expect(err.Error()).To(Equal("key cannot be empty")) 85 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 86 | }) 87 | 88 | It("should not allow for missing owner", func() { 89 | owner = "" 90 | err := commands.ValidateReleaseLocksArguments(nil, []string{}, key, owner) 91 | Expect(err).To(HaveOccurred()) 92 | Expect(err.Error()).To(Equal("owner cannot be empty")) 93 | Expect(err.(commands.CFDotError).ExitCode()).To(Equal(3)) 94 | }) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /commands/retire_actual_lrp.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | "strconv" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var retireActualLRPCmd = &cobra.Command{ 15 | Use: "retire-actual-lrp PROCESS_GUID INDEX", 16 | Short: "Retire actual LRP by index and process guid", 17 | Long: "Retire actual LRP by index and process guid", 18 | RunE: retireActualLRP, 19 | } 20 | 21 | func init() { 22 | AddBBSAndTimeoutFlags(retireActualLRPCmd) 23 | RootCmd.AddCommand(retireActualLRPCmd) 24 | } 25 | 26 | func retireActualLRP(cmd *cobra.Command, args []string) error { 27 | processGuid, index, err := ValidateRetireActualLRPArgs(args) 28 | if err != nil { 29 | return NewCFDotValidationError(cmd, err) 30 | } 31 | 32 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 33 | if err != nil { 34 | return NewCFDotError(cmd, err) 35 | } 36 | 37 | err = RetireActualLRP(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, processGuid, int32(index)) 38 | if err != nil { 39 | return NewCFDotError(cmd, err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func ValidateRetireActualLRPArgs(args []string) (string, int, error) { 46 | if len(args) < 2 { 47 | return "", 0, errMissingArguments 48 | } 49 | 50 | if len(args) > 2 { 51 | return "", 0, errExtraArguments 52 | } 53 | 54 | if args[0] == "" { 55 | return "", 0, errInvalidProcessGuid 56 | } 57 | 58 | index, err := strconv.Atoi(args[1]) 59 | if err != nil || index < 0 { 60 | return "", 0, errInvalidIndex 61 | } 62 | 63 | return args[0], index, nil 64 | } 65 | 66 | func RetireActualLRP(stdout, stderr io.Writer, bbsClient bbs.Client, processGuid string, index int32) error { 67 | traceID := trace.GenerateTraceID() 68 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("retire-actual-lrp"), traceID) 69 | 70 | desiredLRP, err := bbsClient.DesiredLRPByProcessGuid(logger, traceID, processGuid) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | actualLRPKey := models.ActualLRPKey{ProcessGuid: processGuid, Index: index, Domain: desiredLRP.Domain} 76 | err = bbsClient.RetireActualLRP(logger, traceID, &actualLRPKey) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/lager/v3" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var globalLogger = lager.NewLogger("cfdot") 11 | 12 | var RootCmd = &cobra.Command{ 13 | Use: "cfdot", 14 | Short: "Diego operator tooling", 15 | Long: "A command-line tool to interact with a Cloud Foundry Diego deployment", 16 | } 17 | 18 | var ( 19 | errMissingArguments = errors.New("Missing arguments") 20 | errExtraArguments = errors.New("Too many arguments specified") 21 | errInvalidProcessGuid = errors.New("Process guid should be non empty string") 22 | errInvalidIndex = errors.New("Index must be a non-negative integer") 23 | ) 24 | -------------------------------------------------------------------------------- /commands/set_domain.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | 8 | "code.cloudfoundry.org/bbs" 9 | "code.cloudfoundry.org/bbs/trace" 10 | 11 | "code.cloudfoundry.org/cfdot/commands/helpers" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | // errors 17 | errMissingDomain = errors.New("No domain given") 18 | errNegativeTTL = errors.New("ttl is negative") 19 | 20 | // flags 21 | setDomainTTLFlag time.Duration 22 | ) 23 | 24 | var setDomainCmd = &cobra.Command{ 25 | Use: "set-domain DOMAIN", 26 | Short: "Set domain", 27 | Long: "Mark a domain as fresh for ttl seconds, where 0 or non-specified means keep fresh permanently", 28 | RunE: setDomain, 29 | } 30 | 31 | func init() { 32 | AddBBSAndTimeoutFlags(setDomainCmd) 33 | setDomainCmd.Flags().DurationVarP(&setDomainTTLFlag, "ttl", "t", 0*time.Second, "ttl of domain") 34 | RootCmd.AddCommand(setDomainCmd) 35 | } 36 | 37 | func setDomain(cmd *cobra.Command, args []string) error { 38 | domain, err := ValidateSetDomainArgs(args) 39 | if err != nil { 40 | return NewCFDotValidationError(cmd, err) 41 | } 42 | 43 | if setDomainTTLFlag < 0 { 44 | return NewCFDotValidationError(cmd, errNegativeTTL) 45 | } 46 | 47 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 48 | if err != nil { 49 | return NewCFDotError(cmd, err) 50 | } 51 | 52 | err = SetDomain(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, domain, setDomainTTLFlag) 53 | if err != nil { 54 | return NewCFDotError(cmd, err) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func ValidateSetDomainArgs(args []string) (string, error) { 61 | if len(args) < 1 { 62 | return "", errMissingArguments 63 | } 64 | 65 | if len(args) > 1 { 66 | return "", errExtraArguments 67 | } 68 | 69 | if args[0] == "" { 70 | return "", errMissingDomain 71 | } 72 | 73 | return args[0], nil 74 | } 75 | 76 | func SetDomain(stdout, stderr io.Writer, bbsClient bbs.Client, domain string, ttlDuration time.Duration) error { 77 | traceID := trace.GenerateTraceID() 78 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("set-domain"), traceID) 79 | 80 | err := bbsClient.UpsertDomain(logger, traceID, domain, ttlDuration) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /commands/set_domain_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "time" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/openzipkin/zipkin-go/model" 14 | ) 15 | 16 | var _ = Describe("Set Domain", func() { 17 | Context("ValidateSetDomainArgs", func() { 18 | It("returns the domain", func() { 19 | domain, err := commands.ValidateSetDomainArgs([]string{"domain"}) 20 | Expect(err).NotTo(HaveOccurred()) 21 | Expect(domain).To(Equal("domain")) 22 | }) 23 | 24 | Context("when no arguments are specified", func() { 25 | It("returns an error", func() { 26 | _, err := commands.ValidateSetDomainArgs([]string{}) 27 | Expect(err).To(HaveOccurred()) 28 | }) 29 | }) 30 | 31 | Context("when too many arguments are specified", func() { 32 | It("returns an error", func() { 33 | _, err := commands.ValidateSetDomainArgs([]string{"domain1", "domain2"}) 34 | Expect(err).To(HaveOccurred()) 35 | }) 36 | }) 37 | 38 | Context("when the domain is empty", func() { 39 | It("returns an error", func() { 40 | _, err := commands.ValidateSetDomainArgs([]string{""}) 41 | Expect(err).To(HaveOccurred()) 42 | }) 43 | }) 44 | }) 45 | 46 | Context("SetDomain", func() { 47 | var ( 48 | fakeBBSClient *fake_bbs.FakeClient 49 | stdout, stderr *gbytes.Buffer 50 | ) 51 | 52 | BeforeEach(func() { 53 | stdout = gbytes.NewBuffer() 54 | stderr = gbytes.NewBuffer() 55 | fakeBBSClient = &fake_bbs.FakeClient{} 56 | }) 57 | 58 | BeforeEach(func() { 59 | fakeBBSClient.UpsertDomainReturns(nil) 60 | }) 61 | 62 | It("prints a success message when a domain is given", func() { 63 | err := commands.SetDomain(stdout, stderr, fakeBBSClient, "anything", 5*time.Second) 64 | Expect(err).NotTo(HaveOccurred()) 65 | 66 | Expect(fakeBBSClient.UpsertDomainCallCount()).To(Equal(1)) 67 | _, traceID, domain, ttl := fakeBBSClient.UpsertDomainArgsForCall(0) 68 | 69 | _, err = model.TraceIDFromHex(traceID) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(domain).To(Equal("anything")) 72 | Expect(ttl).To(Equal(5 * time.Second)) 73 | }) 74 | 75 | Context("when the bbs errors", func() { 76 | BeforeEach(func() { 77 | fakeBBSClient.UpsertDomainReturns(models.ErrUnknownError) 78 | }) 79 | 80 | It("fails with a relevant error", func() { 81 | err := commands.SetDomain(stdout, stderr, fakeBBSClient, "anything", 0) 82 | Expect(err).To(HaveOccurred()) 83 | Expect(err).To(Equal(models.ErrUnknownError)) 84 | }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /commands/task.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/trace" 9 | "code.cloudfoundry.org/cfdot/commands/helpers" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var taskCmd = &cobra.Command{ 14 | Use: "task TASK_GUID", 15 | Short: "Display task", 16 | Long: "Retrieve the specified task", 17 | RunE: task, 18 | } 19 | 20 | func init() { 21 | AddBBSAndTimeoutFlags(taskCmd) 22 | RootCmd.AddCommand(taskCmd) 23 | } 24 | 25 | func task(cmd *cobra.Command, args []string) error { 26 | guid, err := ValidateTaskArgs(args) 27 | if err != nil { 28 | return NewCFDotValidationError(cmd, err) 29 | } 30 | 31 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 32 | if err != nil { 33 | return NewCFDotError(cmd, err) 34 | } 35 | 36 | if err := TaskByGuid(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, guid); err != nil { 37 | return NewCFDotError(cmd, err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func TaskByGuid(stdout, _ io.Writer, bbsClient bbs.Client, taskGuid string) error { 44 | traceID := trace.GenerateTraceID() 45 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("task-by-guid"), traceID) 46 | 47 | task, err := bbsClient.TaskByGuid(logger, traceID, taskGuid) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | encoder := json.NewEncoder(stdout) 53 | err = encoder.Encode(task) 54 | if err != nil { 55 | logger.Error("failed-to-marshal", err) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func ValidateTaskArgs(args []string) (string, error) { 62 | if len(args) == 0 || args[0] == "" { 63 | return "", errMissingArguments 64 | } 65 | 66 | if len(args) > 1 { 67 | return "", errExtraArguments 68 | } 69 | 70 | return args[0], nil 71 | } 72 | -------------------------------------------------------------------------------- /commands/task_events.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/cfdot/commands/helpers" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | taskEventsCellIdFlag string 15 | ) 16 | 17 | var taskEventsCmd = &cobra.Command{ 18 | Use: "task-events", 19 | Short: "Subscribe to BBS Task events", 20 | Long: "Subscribe to BBS Task events", 21 | RunE: taskEvents, 22 | } 23 | 24 | type TaskEvent struct { 25 | Type string `json:"type"` 26 | Data interface{} `json:"data"` 27 | } 28 | 29 | func init() { 30 | AddBBSFlags(taskEventsCmd) 31 | RootCmd.AddCommand(taskEventsCmd) 32 | } 33 | 34 | func taskEvents(cmd *cobra.Command, args []string) error { 35 | err := validateLRPEventsArguments(args) 36 | if err != nil { 37 | return NewCFDotValidationError(cmd, err) 38 | } 39 | 40 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 41 | if err != nil { 42 | return NewCFDotError(cmd, err) 43 | } 44 | 45 | err = TaskEvents(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, taskEventsCellIdFlag) 46 | if err != nil { 47 | return NewCFDotError(cmd, err) 48 | } 49 | return nil 50 | } 51 | 52 | func TaskEvents(stdout, stderr io.Writer, bbsClient bbs.Client, cellID string) error { 53 | logger := globalLogger.Session("lrp-events") 54 | 55 | es, err := bbsClient.SubscribeToTaskEvents(logger) 56 | if err != nil { 57 | return models.ConvertError(err) 58 | } 59 | defer es.Close() 60 | encoder := json.NewEncoder(stdout) 61 | 62 | var taskEvents LRPEvent 63 | for { 64 | event, err := es.Next() 65 | switch err { 66 | case nil: 67 | taskEvents.Type = event.EventType() 68 | taskEvents.Data = event 69 | err = encoder.Encode(taskEvents) 70 | if err != nil { 71 | logger.Error("failed-to-marshal", err) 72 | } 73 | case io.EOF: 74 | return nil 75 | default: 76 | return err 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /commands/task_events_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | 9 | "code.cloudfoundry.org/bbs/events/eventfakes" 10 | "code.cloudfoundry.org/bbs/fake_bbs" 11 | "code.cloudfoundry.org/bbs/models" 12 | "code.cloudfoundry.org/cfdot/commands" 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/gbytes" 16 | ) 17 | 18 | var _ = Describe("Task Events", func() { 19 | var ( 20 | fakeBBSClient *fake_bbs.FakeClient 21 | fakeEventSource *eventfakes.FakeEventSource 22 | stdout, stderr *gbytes.Buffer 23 | task *models.Task 24 | ) 25 | 26 | BeforeEach(func() { 27 | stdout = gbytes.NewBuffer() 28 | stderr = gbytes.NewBuffer() 29 | fakeBBSClient = &fake_bbs.FakeClient{} 30 | fakeEventSource = &eventfakes.FakeEventSource{} 31 | fakeBBSClient.SubscribeToTaskEventsReturns(fakeEventSource, nil) 32 | 33 | task = &models.Task{ 34 | TaskGuid: "some-task", 35 | } 36 | count := 0 37 | fakeEventSource.NextStub = func() (models.Event, error) { 38 | count += 1 39 | if count > 2 { 40 | return nil, io.EOF 41 | } 42 | return models.NewTaskCreatedEvent(task), nil 43 | } 44 | }) 45 | 46 | It("prints a JSON object", func() { 47 | event := models.NewTaskCreatedEvent(task) 48 | 49 | taskEvent := commands.TaskEvent{ 50 | Type: event.EventType(), 51 | Data: event, 52 | } 53 | data, err := json.Marshal(taskEvent) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | expectedLines := []string{string(data), string(data)} 57 | 58 | err = commands.TaskEvents(stdout, stderr, fakeBBSClient, "") 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | stdoutData := stdout.Contents() 62 | lines := bytes.SplitN(stdoutData, []byte{'\n'}, 3) 63 | Expect(lines).To(HaveLen(3)) 64 | 65 | for i := 0; i < 2; i++ { 66 | Expect(string(lines[i])).To(Equal(expectedLines[i])) 67 | } 68 | }) 69 | 70 | It("closes the event stream", func() { 71 | err := commands.TaskEvents(stdout, stderr, fakeBBSClient, "") 72 | Expect(err).NotTo(HaveOccurred()) 73 | 74 | Expect(fakeEventSource.CloseCallCount()).To(Equal(1)) 75 | }) 76 | 77 | Context("when failing to subscribe to events", func() { 78 | BeforeEach(func() { 79 | fakeBBSClient.SubscribeToTaskEventsReturns(nil, errors.New("failed to connect")) 80 | }) 81 | 82 | It("returns an error", func() { 83 | err := commands.TaskEvents(stdout, stderr, fakeBBSClient, "") 84 | Expect(err).To(HaveOccurred()) 85 | Expect(err.Error()).To(ContainSubstring("failed to connect")) 86 | }) 87 | }) 88 | 89 | Context("when failing to receive an event", func() { 90 | BeforeEach(func() { 91 | fakeEventSource.NextStub = nil 92 | fakeEventSource.NextReturns(nil, errors.New("boom")) 93 | }) 94 | 95 | It("returns an error", func() { 96 | err := commands.TaskEvents(stdout, stderr, fakeBBSClient, "") 97 | Expect(err).To(HaveOccurred()) 98 | Expect(err.Error()).To(ContainSubstring("boom")) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /commands/task_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "code.cloudfoundry.org/bbs/fake_bbs" 7 | "code.cloudfoundry.org/bbs/models" 8 | "code.cloudfoundry.org/cfdot/commands" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/openzipkin/zipkin-go/model" 14 | ) 15 | 16 | var _ = Describe("Task", func() { 17 | Describe("TaskByGuid", func() { 18 | var ( 19 | fakeBBSClient *fake_bbs.FakeClient 20 | stdout, stderr *gbytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | stdout = gbytes.NewBuffer() 25 | stderr = gbytes.NewBuffer() 26 | fakeBBSClient = &fake_bbs.FakeClient{} 27 | }) 28 | 29 | Context("when the bbs responds with a task", func() { 30 | It("prints the task as json", func() { 31 | var ( 32 | taskGuid = "task-guid" 33 | task = &models.Task{TaskGuid: taskGuid} 34 | ) 35 | 36 | fakeBBSClient.TaskByGuidReturns(task, nil) 37 | 38 | err := commands.TaskByGuid(stdout, stderr, fakeBBSClient, taskGuid) 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | taskJSON, err := json.Marshal(task) 42 | Expect(err).NotTo(HaveOccurred()) 43 | Expect(stdout.Contents()).To(MatchJSON(taskJSON)) 44 | 45 | _, traceID, guid := fakeBBSClient.TaskByGuidArgsForCall(0) 46 | 47 | _, err = model.TraceIDFromHex(traceID) 48 | Expect(err).NotTo(HaveOccurred()) 49 | Expect(guid).To(Equal(taskGuid)) 50 | }) 51 | }) 52 | 53 | Context("when the bbs client errors", func() { 54 | It("returns an error back", func() { 55 | fakeBBSClient.TaskByGuidReturns(nil, models.ErrResourceNotFound) 56 | 57 | err := commands.TaskByGuid(stdout, stderr, fakeBBSClient, "broken") 58 | Expect(err).To(HaveOccurred()) 59 | Expect(err).To(Equal(models.ErrResourceNotFound)) 60 | }) 61 | }) 62 | }) 63 | 64 | Describe("ValidateTaskArgs", func() { 65 | Context("when one argument is passed", func() { 66 | It("returns the argument and no error", func() { 67 | guid, err := commands.ValidateTaskArgs([]string{"guid"}) 68 | Expect(err).NotTo(HaveOccurred()) 69 | Expect(guid).To(Equal("guid")) 70 | }) 71 | }) 72 | 73 | Context("when no arguments are passed", func() { 74 | It("returns an error", func() { 75 | _, err := commands.ValidateTaskArgs([]string{}) 76 | Expect(err).To(HaveOccurred()) 77 | }) 78 | }) 79 | 80 | Context("when two arguments are passed", func() { 81 | It("returns an error", func() { 82 | _, err := commands.ValidateTaskArgs([]string{"guid1", "guid2"}) 83 | Expect(err).To(HaveOccurred()) 84 | }) 85 | }) 86 | 87 | Context("when one empty argument is passed", func() { 88 | It("returns an error", func() { 89 | _, err := commands.ValidateTaskArgs([]string{""}) 90 | Expect(err).To(HaveOccurred()) 91 | }) 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /commands/tasks.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "code.cloudfoundry.org/bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/bbs/trace" 10 | "code.cloudfoundry.org/cfdot/commands/helpers" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var tasksCmd = &cobra.Command{ 15 | Use: "tasks", 16 | Short: "List tasks in BBS", 17 | Long: "List all tasks in BBS", 18 | RunE: tasks, 19 | } 20 | 21 | // flags 22 | var tasksDomainFlag, tasksCellIdFlag string 23 | 24 | func init() { 25 | AddBBSAndTimeoutFlags(tasksCmd) 26 | tasksCmd.Flags().StringVarP(&tasksDomainFlag, "domain", "d", "", "retrieve only tasks for the given domain") 27 | tasksCmd.Flags().StringVarP(&tasksCellIdFlag, "cell-id", "c", "", "retrieve only tasks for the given cell-id") 28 | RootCmd.AddCommand(tasksCmd) 29 | } 30 | 31 | func tasks(cmd *cobra.Command, args []string) error { 32 | err := ValidateConflictingShortAndLongFlag("-d", "--domain", cmd) 33 | if err != nil { 34 | return NewCFDotValidationError(cmd, err) 35 | } 36 | 37 | err = ValidateConflictingShortAndLongFlag("-c", "--cell-id", cmd) 38 | if err != nil { 39 | return NewCFDotValidationError(cmd, err) 40 | } 41 | 42 | err = ValidateTasksArgs(args) 43 | if err != nil { 44 | return NewCFDotValidationError(cmd, err) 45 | } 46 | 47 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 48 | if err != nil { 49 | return NewCFDotError(cmd, err) 50 | } 51 | 52 | err = Tasks(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, tasksDomainFlag, tasksCellIdFlag) 53 | if err != nil { 54 | return NewCFDotError(cmd, err) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func Tasks(stdout, _ io.Writer, bbsClient bbs.Client, domain, cellID string) error { 61 | var tasks []*models.Task 62 | var err error 63 | 64 | traceID := trace.GenerateTraceID() 65 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("tasks"), traceID) 66 | tasks, err = bbsClient.TasksWithFilter(logger, traceID, models.TaskFilter{Domain: domain, CellID: cellID}) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | encoder := json.NewEncoder(stdout) 72 | for _, task := range tasks { 73 | err = encoder.Encode(task) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func ValidateTasksArgs(args []string) error { 83 | if len(args) > 0 { 84 | return errExtraArguments 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /commands/timeout_flag.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "code.cloudfoundry.org/cfdot/commands/helpers" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | timeoutConfig helpers.TLSConfig 13 | timeoutPreHooks = []func(cmd *cobra.Command, args []string) error{} 14 | ) 15 | 16 | func AddBBSAndTimeoutFlags(cmd *cobra.Command) { 17 | AddBBSFlags(cmd) 18 | cmd.Flags().IntVar(&timeoutConfig.Timeout, "timeout", 0, "timeout for BBS requests in seconds [environment variable equivalent: CFDOT_TIMEOUT]") 19 | timeoutPreHooks = append(timeoutPreHooks, cmd.PreRunE) 20 | cmd.PreRunE = TimeoutPrehook 21 | } 22 | 23 | func TimeoutPrehook(cmd *cobra.Command, args []string) error { 24 | var err error 25 | for _, f := range timeoutPreHooks { 26 | if f == nil { 27 | continue 28 | } 29 | err = f(cmd, args) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | timeoutConfig.Merge(Config) 36 | err = setTimeoutFlag(cmd, args) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | Config = timeoutConfig 42 | return nil 43 | } 44 | 45 | func setTimeoutFlag(cmd *cobra.Command, args []string) error { 46 | if timeoutConfig.Timeout == 0 && os.Getenv("CFDOT_TIMEOUT") != "" { 47 | timeout, err := strconv.ParseInt(os.Getenv("CFDOT_TIMEOUT"), 10, 16) 48 | if err != nil { 49 | return err 50 | } 51 | timeoutConfig.Timeout = int(timeout) 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /commands/timeout_flag_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "os" 5 | 6 | "code.cloudfoundry.org/cfdot/commands" 7 | "github.com/onsi/gomega/gbytes" 8 | "github.com/spf13/cobra" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Timeout Flag", func() { 15 | var validFlags map[string]string 16 | var dummyCmd *cobra.Command 17 | var err error 18 | var output *gbytes.Buffer 19 | 20 | BeforeEach(func() { 21 | dummyCmd = &cobra.Command{ 22 | Use: "dummy", 23 | Run: func(cmd *cobra.Command, args []string) {}, 24 | } 25 | commands.AddBBSAndTimeoutFlags(dummyCmd) 26 | output = gbytes.NewBuffer() 27 | dummyCmd.SetOut(output) 28 | 29 | validFlags = map[string]string{ 30 | "--bbsURL": "https://example.com", 31 | "--skipCertVerify": "false", 32 | "--caCertFile": "fixtures/bbsCACert.crt", 33 | "--clientCertFile": "fixtures/bbsClient.crt", 34 | "--clientKeyFile": "fixtures/bbsClient.key", 35 | } 36 | }) 37 | 38 | JustBeforeEach(func() { 39 | err = dummyCmd.PreRunE(dummyCmd, dummyCmd.Flags().Args()) 40 | }) 41 | 42 | Context("when flags are passed in as arguments", func() { 43 | BeforeEach(func() { 44 | validFlags["--timeout"] = "10" 45 | parseFlagsErr := dummyCmd.ParseFlags(buildArgList(validFlags)) 46 | Expect(parseFlagsErr).NotTo(HaveOccurred()) 47 | }) 48 | 49 | It("should set the flag in the configuration", func() { 50 | Expect(err).NotTo(HaveOccurred()) 51 | Expect(commands.Config.Timeout).To(Equal(10)) 52 | }) 53 | }) 54 | 55 | Context("with env vars", func() { 56 | BeforeEach(func() { 57 | parseFlagsErr := dummyCmd.ParseFlags(buildArgList(validFlags)) 58 | Expect(parseFlagsErr).NotTo(HaveOccurred()) 59 | }) 60 | 61 | AfterEach(func() { 62 | os.Unsetenv("CFDOT_TIMEOUT") 63 | }) 64 | 65 | Context("when set through env var", func() { 66 | BeforeEach(func() { 67 | os.Setenv("CFDOT_TIMEOUT", "20") 68 | }) 69 | 70 | It("should set the flag in the configuration", func() { 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(commands.Config.Timeout).To(Equal(20)) 73 | }) 74 | }) 75 | 76 | Context("when invalid env var is set", func() { 77 | BeforeEach(func() { 78 | os.Setenv("CFDOT_TIMEOUT", "random thing") 79 | }) 80 | 81 | It("errors when set to an invalid env var", func() { 82 | Expect(err).To(HaveOccurred()) 83 | Expect(commands.Config.Timeout).To(Equal(0)) 84 | }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /commands/tls_flags.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "code.cloudfoundry.org/cfdot/commands/helpers" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // errors 15 | var ( 16 | errMissingCACertFile = errors.New("--caCertFile must be specified if using HTTPS and --skipCertVerify is not set") 17 | errMissingClientCertAndKeyFiles = errors.New("--clientCertFile and --clientKeyFile must both be specified for TLS connections.") 18 | ) 19 | 20 | var ( 21 | Config helpers.TLSConfig 22 | ) 23 | 24 | func AddTLSFlags(cmd *cobra.Command) { 25 | cmd.Flags().BoolVar(&Config.SkipCertVerify, "skipCertVerify", false, "when set to true, skips all SSL/TLS certificate verification [environment variable equivalent: SKIP_CERT_VERIFY]") 26 | cmd.Flags().StringVar(&Config.CACertFile, "caCertFile", "", "path the Certificate Authority (CA) file to use when verifying TLS keypairs [environment variable equivalent: CA_CERT_FILE]") 27 | cmd.Flags().StringVar(&Config.CertFile, "clientCertFile", "", "path to the TLS client certificate to use during mutual-auth TLS [environment variable equivalent: CLIENT_CERT_FILE]") 28 | cmd.Flags().StringVar(&Config.KeyFile, "clientKeyFile", "", "path to the TLS client private key file to use during mutual-auth TLS [environment variable equivalent: CLIENT_KEY_FILE]") 29 | 30 | cmd.PreRunE = tlsPreHook 31 | } 32 | 33 | func tlsPreHook(cmd *cobra.Command, args []string) error { 34 | var err, returnErr error 35 | 36 | // Only look at the environment variable if the flag has not been set. 37 | if !cmd.Flags().Lookup("skipCertVerify").Changed && os.Getenv("SKIP_CERT_VERIFY") != "" { 38 | Config.SkipCertVerify, err = strconv.ParseBool(os.Getenv("SKIP_CERT_VERIFY")) 39 | if err != nil { 40 | returnErr = NewCFDotValidationError( 41 | cmd, 42 | fmt.Errorf( 43 | "The value '%s' is not a valid value for SKIP_CERT_VERIFY. Please specify one of the following valid boolean values: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False", 44 | os.Getenv("SKIP_CERT_VERIFY")), 45 | ) 46 | return returnErr 47 | } 48 | } 49 | 50 | if Config.CACertFile == "" { 51 | Config.CACertFile = os.Getenv("CA_CERT_FILE") 52 | } 53 | 54 | if Config.CertFile == "" { 55 | Config.CertFile = os.Getenv("CLIENT_CERT_FILE") 56 | } 57 | 58 | if Config.KeyFile == "" { 59 | Config.KeyFile = os.Getenv("CLIENT_KEY_FILE") 60 | } 61 | 62 | if !Config.SkipCertVerify { 63 | if Config.CACertFile == "" { 64 | returnErr = NewCFDotValidationError(cmd, errMissingCACertFile) 65 | return returnErr 66 | } 67 | 68 | err := validateReadableFile(cmd, Config.CACertFile, "CA cert") 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | 74 | if (Config.KeyFile == "") || (Config.CertFile == "") { 75 | returnErr = NewCFDotValidationError(cmd, errMissingClientCertAndKeyFiles) 76 | return returnErr 77 | } 78 | 79 | if Config.KeyFile != "" { 80 | err := validateReadableFile(cmd, Config.KeyFile, "key") 81 | 82 | if err != nil { 83 | return err 84 | } 85 | } 86 | 87 | if Config.CertFile != "" { 88 | err := validateReadableFile(cmd, Config.CertFile, "cert") 89 | 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /commands/update_desired_lrp.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "code.cloudfoundry.org/bbs" 11 | "code.cloudfoundry.org/bbs/models" 12 | "code.cloudfoundry.org/bbs/trace" 13 | "code.cloudfoundry.org/cfdot/commands/helpers" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var updateDesiredLRPCmd = &cobra.Command{ 18 | Use: "update-desired-lrp process-guid (SPEC|@FILE)", 19 | Short: "Update a desired LRP", 20 | Long: "Update a desired LRP for a process-guid with the given spec. Spec can either be json encoded update to a desired-lrp, e.g. '{\"instances\":\"4\"}', or a file containing json encoded update to a desired-lrp, e.g. @/path/to/spec/file", 21 | RunE: updateDesiredLRP, 22 | } 23 | 24 | func init() { 25 | AddBBSAndTimeoutFlags(updateDesiredLRPCmd) 26 | RootCmd.AddCommand(updateDesiredLRPCmd) 27 | } 28 | 29 | func updateDesiredLRP(cmd *cobra.Command, args []string) error { 30 | if len(args) != 2 { 31 | return NewCFDotValidationError(cmd, fmt.Errorf("Missing arguments")) 32 | } 33 | 34 | processGuid, spec, err := ValidateUpdateDesiredLRPArguments(args) 35 | if err != nil { 36 | return NewCFDotValidationError(cmd, err) 37 | } 38 | 39 | bbsClient, err := helpers.NewBBSClient(cmd, Config) 40 | if err != nil { 41 | return NewCFDotError(cmd, err) 42 | } 43 | 44 | err = UpdateDesiredLRP(cmd.OutOrStdout(), cmd.OutOrStderr(), bbsClient, processGuid, spec) 45 | if err != nil { 46 | return NewCFDotError(cmd, err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func ValidateUpdateDesiredLRPArguments(args []string) (string, []byte, error) { 53 | var desiredLRP *models.DesiredLRPUpdate 54 | var err error 55 | var spec []byte 56 | processGuid := args[0] 57 | argValue := args[1] 58 | if strings.HasPrefix(argValue, "@") { 59 | _, err := os.Stat(argValue[1:]) 60 | if err != nil { 61 | println(err.Error()) 62 | return "", nil, err 63 | } 64 | spec, err = os.ReadFile(argValue[1:]) 65 | if err != nil { 66 | return "", nil, err 67 | } 68 | 69 | } else { 70 | spec = []byte(argValue) 71 | } 72 | err = json.Unmarshal([]byte(spec), &desiredLRP) 73 | if err != nil { 74 | return "", nil, fmt.Errorf("Invalid JSON: %s", err.Error()) 75 | } 76 | return processGuid, spec, nil 77 | } 78 | 79 | func UpdateDesiredLRP(stdout, stderr io.Writer, bbsClient bbs.Client, processGuid string, spec []byte) error { 80 | traceID := trace.GenerateTraceID() 81 | logger := trace.LoggerWithTraceInfo(globalLogger.Session("update-desired-lrp"), traceID) 82 | 83 | var desiredLRP *models.DesiredLRPUpdate 84 | err := json.Unmarshal(spec, &desiredLRP) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = bbsClient.UpdateDesiredLRP(logger, traceID, processGuid, desiredLRP) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /commands/update_desired_lrp_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "code.cloudfoundry.org/bbs/fake_bbs" 8 | "code.cloudfoundry.org/bbs/models" 9 | "code.cloudfoundry.org/cfdot/commands" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | "github.com/openzipkin/zipkin-go/model" 15 | ) 16 | 17 | var _ = Describe("UpdateDesiredLRP", func() { 18 | var ( 19 | fakeBBSClient *fake_bbs.FakeClient 20 | stdout, stderr *gbytes.Buffer 21 | updatedDesiredLRP *models.DesiredLRPUpdate 22 | processGuid string 23 | spec []byte 24 | ) 25 | 26 | BeforeEach(func() { 27 | fakeBBSClient = &fake_bbs.FakeClient{} 28 | stdout = gbytes.NewBuffer() 29 | stderr = gbytes.NewBuffer() 30 | processGuid = "some-process-guid" 31 | initialDesiredLRP := &models.DesiredLRP{ 32 | ProcessGuid: processGuid, 33 | Instances: 1, 34 | } 35 | 36 | var err error 37 | initialSpec, err := json.Marshal(initialDesiredLRP) 38 | Expect(err).NotTo(HaveOccurred()) 39 | err = commands.CreateDesiredLRP(stdout, stderr, fakeBBSClient, initialSpec) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | updatedInstanceCount := int32(4) 43 | dlu := models.DesiredLRPUpdate{} 44 | dlu.SetInstances(updatedInstanceCount) 45 | updatedDesiredLRP = &dlu 46 | 47 | spec, err = json.Marshal(updatedDesiredLRP) 48 | Expect(err).NotTo(HaveOccurred()) 49 | }) 50 | 51 | It("updates the desired lrp", func() { 52 | err := commands.UpdateDesiredLRP(stdout, stderr, fakeBBSClient, processGuid, spec) 53 | Expect(err).NotTo(HaveOccurred()) 54 | 55 | Expect(fakeBBSClient.UpdateDesiredLRPCallCount()).To(Equal(1)) 56 | _, traceID, guid, lrp := fakeBBSClient.UpdateDesiredLRPArgsForCall(0) 57 | 58 | _, err = model.TraceIDFromHex(traceID) 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(lrp).To(Equal(updatedDesiredLRP)) 61 | Expect(guid).To(Equal(processGuid)) 62 | }) 63 | 64 | Context("when a file is passed as an argument", func() { 65 | var filename string 66 | 67 | BeforeEach(func() { 68 | f, err := os.CreateTemp(os.TempDir(), "spec_file") 69 | Expect(err).NotTo(HaveOccurred()) 70 | defer f.Close() 71 | _, err = f.Write(spec) 72 | Expect(err).NotTo(HaveOccurred()) 73 | filename = f.Name() 74 | }) 75 | 76 | It("validates the input file successfully", func() { 77 | args := []string{processGuid, "@" + filename} 78 | actualProcessGuid, actualSpec, err := commands.ValidateUpdateDesiredLRPArguments(args) 79 | Expect(err).NotTo(HaveOccurred()) 80 | Expect(actualSpec).To(Equal(spec)) 81 | Expect(actualProcessGuid).To(Equal(processGuid)) 82 | }) 83 | }) 84 | 85 | Context("when the bbs errors", func() { 86 | BeforeEach(func() { 87 | fakeBBSClient.UpdateDesiredLRPReturns(models.ErrUnknownError) 88 | }) 89 | 90 | It("fails with a relevant error", func() { 91 | err := commands.UpdateDesiredLRP(stdout, stderr, fakeBBSClient, processGuid, []byte("{}")) 92 | Expect(err).To(MatchError(models.ErrUnknownError)) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /commands/validators.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func ValidateConflictingShortAndLongFlag(short string, long string, cmd *cobra.Command) error { 11 | errorConflictingShortAndLongFlagPassed := fmt.Errorf("Only one of %s and %s should be passed", short, long) 12 | 13 | if contains(os.Args, short) && contains(os.Args, long) { 14 | return NewCFDotValidationError(cmd, errorConflictingShortAndLongFlagPassed) 15 | } 16 | 17 | return nil 18 | } 19 | 20 | func contains(s []string, e string) bool { 21 | for _, a := range s { 22 | if a == e { 23 | return true 24 | } 25 | } 26 | 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /docs/010-current-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Current Commands 3 | expires_at : never 4 | tags: [diego-release, cfdot] 5 | --- 6 | 7 | ## Current Commands 8 | 9 | ```bash 10 | $ cfdot --help 11 | A command-line tool to interact with a Cloud Foundry Diego deployment 12 | 13 | Usage: 14 | cfdot [command] 15 | 16 | Available Commands: 17 | actual-lrps List actual LRPs 18 | cancel-task Cancel task 19 | cell Show the specified cell presence 20 | cell-state Show the specified cell state 21 | cell-states Show cell states for all cells 22 | cells List registered cell presences 23 | claim-lock Claim Locket lock 24 | claim-presence Claim Locket presence 25 | create-desired-lrp Create a desired LRP 26 | create-task Create a Task 27 | delete-desired-lrp Delete a desired LRP 28 | delete-task Delete a Task 29 | desired-lrp Show the specified desired LRP 30 | desired-lrp-scheduling-infos List desired LRP scheduling infos 31 | desired-lrps List desired LRPs 32 | domains List domains 33 | help Get help on [command] 34 | locks List Locket locks 35 | lrp-events Subscribe to BBS LRP events 36 | presences List Locket presences 37 | release-lock Release Locket lock 38 | retire-actual-lrp Retire actual LRP by index and process guid 39 | set-domain Set domain 40 | task Display task 41 | task-events Subscribe to BBS Task events 42 | tasks List tasks in BBS 43 | update-desired-lrp Update a desired LRP 44 | 45 | Flags: 46 | -h, --help help for cfdot 47 | 48 | Use "cfdot [command] --help" for more information about a command. 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/020-running-from-a-BOSH-deployed-vm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Running from a BOSH deployed VM 3 | expires_at : never 4 | tags: [diego-release, cfdot] 5 | --- 6 | 7 | ## Running from a BOSH-deployed VM 8 | 9 | `cfdot` is most useful in the context of a running Diego deployment. If you 10 | use [cf-deployment](https://github.com/cloudfoundry/cf-deployment/tree/main) , 11 | `cfdot` is already available on the BOSH-deployed Diego VMs. To use it: 12 | 13 | ```bash 14 | bosh ssh / 15 | cfdot 16 | ``` 17 | 18 | The `cfdot` pre-start script installs the `setup` script into `/etc/profile.d`. 19 | This `setup` script does 3 things: 20 | 21 | - Exports environment variables to target the BBS API in the deployment. 22 | - Puts the `cfdot` binary on the `PATH`. 23 | - Puts a `jq` binary on the `PATH`. 24 | -------------------------------------------------------------------------------- /docs/030-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | expires_at : never 4 | tags: [diego-release, cfdot] 5 | --- 6 | 7 | ## Examples 8 | 9 | ```bash 10 | # count the total number of desired instances 11 | $ cfdot desired-lrp-scheduling-infos | jq '.instances' | jq -s 'add' 12 | 568 13 | 14 | # show instance counts by state 15 | $ cfdot actual-lrps | jq -s -r 'group_by(.state)[] | .[0].state + ": " + (length | tostring)' 16 | CRASHED: 36 17 | RUNNING: 531 18 | UNCLAIMED: 1 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/040-building-from-source.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building from Source 3 | expires_at : never 4 | tags: [diego-release, cfdot] 5 | --- 6 | 7 | ## Building from Source 8 | 9 | `cfdot` requires the [Diego BBS client library](https://github.com/cloudfoundry/bbs). 10 | If you have already cloned [diego-release](https://github.com/cloudfoundry/diego-release), 11 | you can run the following commands using that diego-release directory as your 12 | GOPATH. Alternatively, run these commands with any other GOPATH and `go get` 13 | will automatically fetch the latest BBS code from diego-release. 14 | 15 | ```bash 16 | # Get cfdot and required dependencies 17 | go get code.cloudfoundry.org/cfdot 18 | cd src/code.cloudfoundry.org/cfdot 19 | 20 | # Build for Linux 21 | GOOS=linux go build . 22 | 23 | # Build for Mac 24 | GOOS=darwin go build . 25 | 26 | # Build for Windows 27 | GOOS=windows go build . 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/050-design-tenets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Tenets 3 | expires_at : never 4 | tags: [diego-release, cfdot] 5 | --- 6 | 7 | ## Design Tenets 8 | 9 | - Execution is stateless: configuration is specified either as flags or as environment variables. 10 | - Conform to UNIX conventions of successful output on stdout and error messages on stderr. 11 | - For BBS API commands, output is a stream of JSON values, one per line, optimal for processing with `jq` and suitable for processing with `bash` and other line-based UNIX utilities. 12 | -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | ../scripts/generate-cfdot-documentation -------------------------------------------------------------------------------- /integration/actual_lrp_groups_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs/models" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("actual-lrp-groups", func() { 17 | itValidatesBBSFlags("actual-lrp-groups") 18 | itHasNoArgs("actual-lrp-groups", false) 19 | 20 | Context("when no filters are passed", func() { 21 | var ( 22 | serverTimeout int 23 | ) 24 | 25 | BeforeEach(func() { 26 | serverTimeout = 0 27 | }) 28 | 29 | JustBeforeEach(func() { 30 | bbsServer.AppendHandlers( 31 | ghttp.CombineHandlers( 32 | ghttp.VerifyRequest("POST", "/v1/actual_lrp_groups/list"), 33 | func(w http.ResponseWriter, req *http.Request) { 34 | time.Sleep(time.Duration(serverTimeout) * time.Second) 35 | }, 36 | //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method 37 | ghttp.RespondWithProto(200, &models.ActualLRPGroupsResponse{ 38 | //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method 39 | ActualLrpGroups: []*models.ActualLRPGroup{ 40 | { 41 | Instance: &models.ActualLRP{ 42 | State: "running", 43 | }, 44 | }, 45 | }, 46 | }), 47 | ), 48 | ) 49 | }) 50 | 51 | It("returns the json encoding of the actual lrp", func() { 52 | sess := RunCFDot("actual-lrp-groups") 53 | Eventually(sess).Should(gexec.Exit(0)) 54 | Expect(sess.Out).To(gbytes.Say(`"state":"running"`)) 55 | }) 56 | 57 | Context("when timeout flag is present", func() { 58 | Context("when request exceeds timeout", func() { 59 | BeforeEach(func() { 60 | serverTimeout = 2 61 | }) 62 | 63 | It("exits with code 4 and a timeout message", func() { 64 | sess := RunCFDot("actual-lrp-groups", "--timeout", "1") 65 | Eventually(sess, 2).Should(gexec.Exit(4)) 66 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 67 | }) 68 | }) 69 | 70 | Context("when request is within the timeout", func() { 71 | It("exits with status code of 0", func() { 72 | sess := RunCFDot("actual-lrp-groups", "--timeout", "1") 73 | Eventually(sess).Should(gexec.Exit(0)) 74 | Expect(sess.Out).To(gbytes.Say(`"state":"running"`)) 75 | }) 76 | }) 77 | }) 78 | }) 79 | 80 | Context("when passing filters", func() { 81 | BeforeEach(func() { 82 | bbsServer.AppendHandlers( 83 | ghttp.CombineHandlers( 84 | ghttp.VerifyRequest("POST", "/v1/actual_lrp_groups/list"), 85 | //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method 86 | ghttp.VerifyProtoRepresenting(&models.ActualLRPGroupsRequest{ 87 | Domain: "cf-apps", 88 | CellId: "cell_z1-0", 89 | }), 90 | //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method 91 | ghttp.RespondWithProto(200, &models.ActualLRPGroupsResponse{ 92 | //lint:ignore SA1019 - calling deprecated model while unit testing deprecated method 93 | ActualLrpGroups: []*models.ActualLRPGroup{ 94 | { 95 | Instance: &models.ActualLRP{ 96 | State: "running", 97 | }, 98 | }, 99 | }, 100 | }), 101 | ), 102 | ) 103 | }) 104 | 105 | It("exits with status code of 0", func() { 106 | sess := RunCFDot("actual-lrp-groups", 107 | "-d", "cf-apps", 108 | "-c", "cell_z1-0", 109 | ) 110 | Eventually(sess).Should(gexec.Exit(0)) 111 | }) 112 | 113 | It("exits with status code of 0", func() { 114 | sess := RunCFDot("actual-lrp-groups", 115 | "-d", "cf-apps", 116 | "--domain", "cf-apps", 117 | "--cell-id", "cell_z1-0", 118 | ) 119 | Eventually(sess).Should(gexec.Exit(0)) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /integration/actual_lrps_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs/models" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/onsi/gomega/gexec" 12 | "github.com/onsi/gomega/ghttp" 13 | ) 14 | 15 | var _ = Describe("actual-lrps", func() { 16 | itValidatesBBSFlags("actual-lrps") 17 | itHasNoArgs("actual-lrps", false) 18 | 19 | Context("when no filters are passed", func() { 20 | var ( 21 | serverTimeout int 22 | ) 23 | 24 | BeforeEach(func() { 25 | serverTimeout = 0 26 | }) 27 | 28 | JustBeforeEach(func() { 29 | bbsServer.AppendHandlers( 30 | ghttp.CombineHandlers( 31 | ghttp.VerifyRequest("POST", "/v1/actual_lrps/list"), 32 | ghttp.VerifyProtoRepresenting(&models.ActualLRPsRequest{}), 33 | func(w http.ResponseWriter, req *http.Request) { 34 | time.Sleep(time.Duration(serverTimeout) * time.Second) 35 | }, 36 | ghttp.RespondWithProto(200, &models.ActualLRPsResponse{ 37 | ActualLrps: []*models.ActualLRP{ 38 | &models.ActualLRP{ 39 | State: "running", 40 | }, 41 | }, 42 | }), 43 | ), 44 | ) 45 | }) 46 | 47 | It("returns the json encoding of the actual lrp", func() { 48 | sess := RunCFDot("actual-lrps") 49 | Eventually(sess).Should(gexec.Exit(0)) 50 | Expect(sess.Out).To(gbytes.Say(`"state":"running"`)) 51 | }) 52 | 53 | Context("when timeout flag is present", func() { 54 | Context("when request exceeds timeout", func() { 55 | BeforeEach(func() { 56 | serverTimeout = 2 57 | }) 58 | 59 | It("exits with code 4 and a timeout message", func() { 60 | sess := RunCFDot("actual-lrps", "--timeout", "1") 61 | Eventually(sess, 2).Should(gexec.Exit(4)) 62 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 63 | }) 64 | }) 65 | 66 | Context("when request is within the timeout", func() { 67 | It("exits with status code of 0", func() { 68 | sess := RunCFDot("actual-lrps", "--timeout", "1") 69 | Eventually(sess).Should(gexec.Exit(0)) 70 | Expect(sess.Out).To(gbytes.Say(`"state":"running"`)) 71 | }) 72 | }) 73 | }) 74 | }) 75 | 76 | Context("when passing filters", func() { 77 | BeforeEach(func() { 78 | alr := models.ActualLRPsRequest{ 79 | Domain: "cf-apps", 80 | CellId: "cell_z1-0", 81 | ProcessGuid: "pg-0", 82 | } 83 | alr.SetIndex(int32(1)) 84 | 85 | bbsServer.AppendHandlers( 86 | ghttp.CombineHandlers( 87 | ghttp.VerifyRequest("POST", "/v1/actual_lrps/list"), 88 | ghttp.VerifyProtoRepresenting(&alr), 89 | ghttp.RespondWithProto(200, &models.ActualLRPsResponse{ 90 | ActualLrps: []*models.ActualLRP{ 91 | &models.ActualLRP{ 92 | State: "running", 93 | }, 94 | }, 95 | }), 96 | ), 97 | ) 98 | }) 99 | 100 | It("exits with status code of 0", func() { 101 | sess := RunCFDot("actual-lrps", 102 | "-d", "cf-apps", 103 | "-c", "cell_z1-0", 104 | "-p", "pg-0", 105 | "-i", "1", 106 | ) 107 | Eventually(sess).Should(gexec.Exit(0)) 108 | }) 109 | 110 | It("exits with status code of 0", func() { 111 | sess := RunCFDot("actual-lrps", 112 | "-d", "cf-apps", 113 | "--domain", "cf-apps", 114 | "--cell-id", "cell_z1-0", 115 | "--process-guid", "pg-0", 116 | "--index", "1", 117 | ) 118 | Eventually(sess).Should(gexec.Exit(0)) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /integration/cancel_task_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs/models" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/onsi/gomega/gexec" 12 | "github.com/onsi/gomega/ghttp" 13 | ) 14 | 15 | var _ = Describe("cancel-task", func() { 16 | itValidatesBBSFlags("cancel-task", "task-guid") 17 | 18 | Context("when the server responds ok for canceling task", func() { 19 | var ( 20 | serverTimeout int 21 | ) 22 | 23 | BeforeEach(func() { 24 | serverTimeout = 0 25 | 26 | }) 27 | JustBeforeEach(func() { 28 | bbsServer.AppendHandlers( 29 | ghttp.CombineHandlers( 30 | ghttp.VerifyRequest("POST", "/v1/tasks/cancel"), 31 | func(w http.ResponseWriter, req *http.Request) { 32 | time.Sleep(time.Duration(serverTimeout) * time.Second) 33 | }, 34 | ghttp.VerifyProtoRepresenting(&models.TaskGuidRequest{TaskGuid: "task-guid"}), 35 | ghttp.RespondWith(200, nil), 36 | ), 37 | ) 38 | }) 39 | 40 | It("exits 0", func() { 41 | sess := RunCFDot("cancel-task", "task-guid") 42 | Eventually(sess).Should(gexec.Exit(0)) 43 | Expect(len(bbsServer.ReceivedRequests())).To(Equal(1)) 44 | }) 45 | 46 | Context("when timeout flag is present", func() { 47 | Context("when request exceeds timeout", func() { 48 | BeforeEach(func() { 49 | serverTimeout = 2 50 | }) 51 | 52 | It("exits with code 4 and a timeout message", func() { 53 | sess := RunCFDot("cancel-task", "task-guid", "--timeout", "1") 54 | Eventually(sess, 2).Should(gexec.Exit(4)) 55 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 56 | }) 57 | }) 58 | 59 | Context("when request is within the timeout", func() { 60 | It("exits with status code of 0", func() { 61 | sess := RunCFDot("cancel-task", "task-guid", "--timeout", "1") 62 | Eventually(sess).Should(gexec.Exit(0)) 63 | Expect(len(bbsServer.ReceivedRequests())).To(Equal(1)) 64 | }) 65 | }) 66 | }) 67 | }) 68 | 69 | Context("when the server responds with error", func() { 70 | It("exits with status code 4", func() { 71 | bbsServer.RouteToHandler( 72 | "POST", 73 | "/v1/tasks/cancel", 74 | ghttp.RespondWithProto(200, &models.TaskResponse{ 75 | Error: models.ErrUnknownError, 76 | }), 77 | ) 78 | 79 | sess := RunCFDot("cancel-task", "task-guid") 80 | Eventually(sess).Should(gexec.Exit(4)) 81 | 82 | Expect(sess.Err).To(gbytes.Say("UnknownError")) 83 | }) 84 | }) 85 | 86 | Context("when passed invalid arguments", func() { 87 | It("fails with no arugments and prints the usage", func() { 88 | sess := RunCFDot("cancel-task") 89 | Eventually(sess).Should(gexec.Exit(3)) 90 | Expect(sess.Err).To(gbytes.Say("Error: Missing arguments")) 91 | Expect(sess.Err).To(gbytes.Say("cfdot cancel-task TASK_GUID \\[flags\\]")) 92 | }) 93 | 94 | It("fails with 2+ arguments", func() { 95 | sess := RunCFDot("cancel-task", "arg1", "arg2") 96 | Eventually(sess).Should(gexec.Exit(3)) 97 | Expect(sess.Err).To(gbytes.Say("Error: Too many arguments specified")) 98 | Expect(sess.Err).To(gbytes.Say("cfdot cancel-task TASK_GUID \\[flags\\]")) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /integration/cell_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "code.cloudfoundry.org/bbs/models" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | "github.com/onsi/gomega/ghttp" 15 | ) 16 | 17 | var _ = Describe("cell", func() { 18 | itValidatesBBSFlags("cell") 19 | 20 | Context("when cell command is called", func() { 21 | var ( 22 | presence *models.CellPresence 23 | serverTimeout int 24 | ) 25 | 26 | BeforeEach(func() { 27 | presence = &models.CellPresence{ 28 | CellId: "cell-1", 29 | RepAddress: "rep-1", 30 | } 31 | serverTimeout = 0 32 | }) 33 | 34 | JustBeforeEach(func() { 35 | bbsServer.AppendHandlers( 36 | ghttp.CombineHandlers( 37 | ghttp.VerifyRequest("POST", "/v1/cells/list.r1"), 38 | func(w http.ResponseWriter, req *http.Request) { 39 | time.Sleep(time.Duration(serverTimeout) * time.Second) 40 | }, 41 | ghttp.RespondWithProto(200, &models.CellsResponse{ 42 | Cells: []*models.CellPresence{presence}, 43 | }), 44 | ), 45 | ) 46 | }) 47 | 48 | It("returns the json encoding of the cell presences", func() { 49 | sess := RunCFDot("cell", "cell-1") 50 | Eventually(sess).Should(gexec.Exit(0)) 51 | 52 | jsonData, err := json.Marshal(presence) 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(sess.Out).To(gbytes.Say(string(jsonData))) 55 | }) 56 | 57 | Context("when the cell does not exist", func() { 58 | It("exits with status code of 5", func() { 59 | sess := RunCFDot("cell", "cell-id-dsafasdklfjasdlkf") 60 | Eventually(sess).Should(gexec.Exit(5)) 61 | }) 62 | }) 63 | 64 | Context("when timeout flag is present", func() { 65 | var sess *gexec.Session 66 | 67 | BeforeEach(func() { 68 | sess = RunCFDot("--timeout", "1", "cell", "cell-1") 69 | }) 70 | 71 | Context("when request exceeds timeout", func() { 72 | BeforeEach(func() { 73 | serverTimeout = 2 74 | }) 75 | 76 | It("exits with code 4 and a timeout message", func() { 77 | Eventually(sess, 2).Should(gexec.Exit(4)) 78 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 79 | }) 80 | }) 81 | 82 | Context("when request is within the timeout", func() { 83 | It("exits with status code of 0", func() { 84 | Eventually(sess).Should(gexec.Exit(0)) 85 | 86 | jsonData, err := json.Marshal(presence) 87 | Expect(err).NotTo(HaveOccurred()) 88 | Expect(sess.Out).To(gbytes.Say(string(jsonData))) 89 | }) 90 | }) 91 | }) 92 | }) 93 | 94 | Context("when cell command is called with extra arguments", func() { 95 | It("exits with status code of 3", func() { 96 | sess := RunCFDot("cell", "cell-id", "extra-argument") 97 | Eventually(sess).Should(gexec.Exit(3)) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /integration/cells_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs/models" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("cells", func() { 17 | var serverTimeout int 18 | 19 | itValidatesBBSFlags("cells") 20 | itHasNoArgs("cells", false) 21 | 22 | Context("when cells command is called", func() { 23 | BeforeEach(func() { 24 | serverTimeout = 0 25 | }) 26 | 27 | JustBeforeEach(func() { 28 | bbsServer.AppendHandlers( 29 | ghttp.CombineHandlers( 30 | ghttp.VerifyRequest("POST", "/v1/cells/list.r1"), 31 | func(w http.ResponseWriter, req *http.Request) { 32 | time.Sleep(time.Duration(serverTimeout) * time.Second) 33 | }, 34 | ghttp.RespondWithProto(200, &models.CellsResponse{ 35 | Cells: []*models.CellPresence{ 36 | { 37 | CellId: "cell-1", 38 | RepAddress: "rep-1", 39 | Zone: "zone1", 40 | Capacity: &models.CellCapacity{ 41 | MemoryMb: 1024, 42 | DiskMb: 1024, 43 | Containers: 10, 44 | }, 45 | RootfsProviders: []*models.Provider{ 46 | { 47 | Name: "rootfs1", 48 | }, 49 | }, 50 | RepUrl: "http://rep1.com", 51 | }, 52 | }, 53 | }), 54 | ), 55 | ) 56 | }) 57 | 58 | It("returns the json encoding of the cell presences", func() { 59 | sess := RunCFDot("cells") 60 | Eventually(sess).Should(gexec.Exit(0)) 61 | Expect(sess.Out).To(gbytes.Say(`"rep_url":"http://rep1.com"`)) 62 | }) 63 | 64 | Context("when timeout flag is present", func() { 65 | var sess *gexec.Session 66 | 67 | BeforeEach(func() { 68 | sess = RunCFDot("--timeout", "1", "cells") 69 | }) 70 | 71 | Context("when request exceeds timeout", func() { 72 | BeforeEach(func() { 73 | serverTimeout = 2 74 | }) 75 | 76 | It("exits with code 4 and a timeout message", func() { 77 | Eventually(sess, 2).Should(gexec.Exit(4)) 78 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 79 | }) 80 | }) 81 | 82 | Context("when request is within the timeout", func() { 83 | It("exits with status code of 0", func() { 84 | Eventually(sess).Should(gexec.Exit(0)) 85 | Expect(sess.Out).To(gbytes.Say(`"rep_url":"http://rep1.com"`)) 86 | }) 87 | }) 88 | }) 89 | }) 90 | 91 | Context("when cells command is called with extra arguments", func() { 92 | It("exits with status code of 3", func() { 93 | sess := RunCFDot("cells", "extra-args") 94 | Eventually(sess).Should(gexec.Exit(3)) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /integration/claim_presence_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/onsi/gomega/gexec" 12 | ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2" 13 | ) 14 | 15 | var _ = Describe("claim-presence", func() { 16 | itValidatesLocketFlags("claim-presence") 17 | 18 | BeforeEach(func() { 19 | os.Setenv("CA_CERT_FILE", locketCACertFile) 20 | os.Setenv("CLIENT_CERT_FILE", locketClientCertFile) 21 | os.Setenv("CLIENT_KEY_FILE", locketClientKeyFile) 22 | }) 23 | 24 | AfterEach(func() { 25 | os.Unsetenv("CA_CERT_FILE") 26 | os.Unsetenv("CLIENT_CERT_FILE") 27 | os.Unsetenv("CLIENT_KEY_FILE") 28 | }) 29 | 30 | It("claim-lock exists successfully with code 0", func() { 31 | cfdotCmd := exec.Command(cfdotPath, 32 | "--locketAPILocation", locketAPILocation, 33 | "claim-presence", 34 | "--key", "test-key", 35 | "--owner", "test-owner", 36 | "--ttl", "60", 37 | ) 38 | 39 | sess, err := gexec.Start(cfdotCmd, GinkgoWriter, GinkgoWriter) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | Eventually(sess).Should(gexec.Exit(0)) 43 | }) 44 | 45 | Context("when the server is down", func() { 46 | BeforeEach(func() { 47 | ginkgomon.Interrupt(locketProcess) 48 | }) 49 | 50 | It("claim-lock fails with a relevant error message", func() { 51 | cfdotCmd := exec.Command(cfdotPath, 52 | "--locketAPILocation", locketAPILocation, 53 | "claim-presence", 54 | "--key", "test-key", 55 | "--owner", "test-owner", 56 | "--ttl", "60", 57 | ) 58 | 59 | sess, err := gexec.Start(cfdotCmd, GinkgoWriter, GinkgoWriter) 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | Eventually(sess, 11*time.Second).Should(gexec.Exit(4)) 63 | Expect(sess.Err).To(gbytes.Say("context deadline exceeded")) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /integration/delete_desired_lrp_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "code.cloudfoundry.org/bbs/models" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("delete-desired-lrp", func() { 17 | itValidatesBBSFlags("delete-desired-lrp") 18 | 19 | Context("when a set of invalid arguments is passed", func() { 20 | Context("when two arguments are passed", func() { 21 | It("exits with status 3 and prints the usage and the error", func() { 22 | sess := RunCFDot("delete-desired-lrp", "arg1", "arg2") 23 | Eventually(sess).Should(gexec.Exit(3)) 24 | Expect(sess.Err).To(gbytes.Say(`Error:`)) 25 | Expect(sess.Err).To(gbytes.Say("cfdot delete-desired-lrp PROCESS_GUID .*")) 26 | }) 27 | }) 28 | 29 | Context("when no arguments are passed", func() { 30 | It("exits with status 3 and prints the usage and the error", func() { 31 | sess := RunCFDot("delete-desired-lrp") 32 | Eventually(sess).Should(gexec.Exit(3)) 33 | Expect(sess.Err).To(gbytes.Say(`Error:`)) 34 | Expect(sess.Err).To(gbytes.Say("cfdot delete-desired-lrp PROCESS_GUID .*")) 35 | }) 36 | }) 37 | }) 38 | 39 | Context("when bbs responds with 200 status code", func() { 40 | const processGuid = "process-guid" 41 | var ( 42 | serverTimeout int 43 | ) 44 | 45 | BeforeEach(func() { 46 | serverTimeout = 0 47 | }) 48 | 49 | JustBeforeEach(func() { 50 | bbsServer.AppendHandlers( 51 | ghttp.CombineHandlers( 52 | ghttp.VerifyRequest("POST", "/v1/desired_lrp/remove"), 53 | func(w http.ResponseWriter, req *http.Request) { 54 | time.Sleep(time.Duration(serverTimeout) * time.Second) 55 | }, 56 | ghttp.VerifyProtoRepresenting(&models.RemoveDesiredLRPRequest{ 57 | ProcessGuid: processGuid, 58 | }), 59 | ghttp.RespondWithProto(200, &models.DesiredLRPLifecycleResponse{}), 60 | ), 61 | ) 62 | }) 63 | 64 | It("exits with status code of 0", func() { 65 | sess := RunCFDot("delete-desired-lrp", processGuid) 66 | Eventually(sess).Should(gexec.Exit(0)) 67 | }) 68 | 69 | Context("when timeout flag is present", func() { 70 | var sess *gexec.Session 71 | 72 | BeforeEach(func() { 73 | sess = RunCFDot("delete-desired-lrp", "--timeout", "1", processGuid) 74 | }) 75 | 76 | Context("when request exceeds timeout", func() { 77 | BeforeEach(func() { 78 | serverTimeout = 2 79 | }) 80 | 81 | It("exits with code 4 and a timeout message", func() { 82 | Eventually(sess, 2).Should(gexec.Exit(4)) 83 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 84 | }) 85 | }) 86 | 87 | Context("when request is within the timeout", func() { 88 | It("exits with status code of 0", func() { 89 | Eventually(sess).Should(gexec.Exit(0)) 90 | }) 91 | }) 92 | }) 93 | }) 94 | 95 | Context("when bbs responds with non-200 status code", func() { 96 | BeforeEach(func() { 97 | bbsServer.AppendHandlers( 98 | ghttp.CombineHandlers( 99 | ghttp.VerifyRequest("POST", "/v1/desired_lrp/remove"), 100 | ghttp.RespondWithProto(500, &models.DesiredLRPLifecycleResponse{ 101 | Error: &models.Error{ 102 | Type: models.Error_Deadlock, 103 | Message: "deadlock detected", 104 | }, 105 | }), 106 | ), 107 | ) 108 | }) 109 | 110 | It("exits with status code 4 and prints the error", func() { 111 | sess := RunCFDot("delete-desired-lrp", "any-process-guid") 112 | Eventually(sess).Should(gexec.Exit(4)) 113 | Expect(sess.Err).To(gbytes.Say("deadlock")) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /integration/desired_lrp_scheduling_infos_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "code.cloudfoundry.org/bbs/models" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("desired-lrp-scheduling-infos", func() { 17 | itValidatesBBSFlags("desired-lrp-scheduling-infos") 18 | itHasNoArgs("desired-lrp-scheduling-infos", false) 19 | 20 | Context("when extra arguments are passed", func() { 21 | It("exits with status code of 3 and prints the usage", func() { 22 | sess := RunCFDot("desired-lrp-scheduling-infos", "extra-arg") 23 | Eventually(sess).Should(gexec.Exit(3)) 24 | Expect(sess.Err).To(gbytes.Say("cfdot desired-lrp-scheduling-infos \\[flags\\]")) 25 | }) 26 | }) 27 | 28 | Context("when no filters are passed", func() { 29 | var ( 30 | serverTimeout int 31 | ) 32 | 33 | BeforeEach(func() { 34 | serverTimeout = 0 35 | }) 36 | 37 | JustBeforeEach(func() { 38 | bbsServer.AppendHandlers( 39 | ghttp.CombineHandlers( 40 | ghttp.VerifyRequest("POST", "/v1/desired_lrp_scheduling_infos/list"), 41 | func(w http.ResponseWriter, req *http.Request) { 42 | time.Sleep(time.Duration(serverTimeout) * time.Second) 43 | }, 44 | ghttp.RespondWithProto(200, &models.DesiredLRPSchedulingInfosResponse{ 45 | DesiredLrpSchedulingInfos: []*models.DesiredLRPSchedulingInfo{ 46 | { 47 | Instances: 1, 48 | }, 49 | }, 50 | }), 51 | ), 52 | ) 53 | }) 54 | 55 | It("exits with 0 and returns the json encoding of the desired lrp scheduling info", func() { 56 | sess := RunCFDot("desired-lrp-scheduling-infos") 57 | Eventually(sess).Should(gexec.Exit(0)) 58 | Expect(sess.Out).To(gbytes.Say(`"instances":1`)) 59 | }) 60 | 61 | Context("when timeout flag is present", func() { 62 | var sess *gexec.Session 63 | 64 | BeforeEach(func() { 65 | sess = RunCFDot("desired-lrp-scheduling-infos", "--timeout", "1") 66 | }) 67 | 68 | Context("when request exceeds timeout", func() { 69 | BeforeEach(func() { 70 | serverTimeout = 2 71 | }) 72 | 73 | It("exits with code 4 and a timeout message", func() { 74 | Eventually(sess, 2).Should(gexec.Exit(4)) 75 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 76 | }) 77 | }) 78 | 79 | Context("when request is within the timeout", func() { 80 | It("exits with status code of 0", func() { 81 | Eventually(sess).Should(gexec.Exit(0)) 82 | Expect(sess.Out).To(gbytes.Say(`"instances":1`)) 83 | }) 84 | }) 85 | }) 86 | }) 87 | 88 | Context("when passing filters", func() { 89 | BeforeEach(func() { 90 | bbsServer.AppendHandlers( 91 | ghttp.CombineHandlers( 92 | ghttp.VerifyRequest("POST", "/v1/desired_lrp_scheduling_infos/list"), 93 | ghttp.VerifyProtoRepresenting(&models.DesiredLRPsRequest{ 94 | Domain: "cf-apps", 95 | }), 96 | ghttp.RespondWithProto(200, &models.DesiredLRPSchedulingInfosResponse{ 97 | DesiredLrpSchedulingInfos: []*models.DesiredLRPSchedulingInfo{ 98 | { 99 | Instances: 1, 100 | }, 101 | }, 102 | }), 103 | ), 104 | ) 105 | }) 106 | 107 | It("exits with status code of 0", func() { 108 | sess := RunCFDot("desired-lrp-scheduling-infos", "-d", "cf-apps") 109 | Eventually(sess).Should(gexec.Exit(0)) 110 | }) 111 | 112 | It("exits with status code of 0", func() { 113 | sess := RunCFDot("desired-lrp-scheduling-infos", "--domain", "cf-apps") 114 | Eventually(sess).Should(gexec.Exit(0)) 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /integration/desired_lrps_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "code.cloudfoundry.org/bbs/models" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("desired-lrps", func() { 17 | itValidatesBBSFlags("desired-lrps") 18 | itHasNoArgs("desired-lrps", false) 19 | 20 | Context("when no filters are passed", func() { 21 | var ( 22 | serverTimeout int 23 | ) 24 | 25 | BeforeEach(func() { 26 | serverTimeout = 0 27 | }) 28 | 29 | JustBeforeEach(func() { 30 | bbsServer.AppendHandlers( 31 | ghttp.CombineHandlers( 32 | ghttp.VerifyRequest("POST", "/v1/desired_lrps/list.r3"), 33 | func(w http.ResponseWriter, req *http.Request) { 34 | time.Sleep(time.Duration(serverTimeout) * time.Second) 35 | }, 36 | ghttp.RespondWithProto(200, &models.DesiredLRPsResponse{ 37 | DesiredLrps: []*models.DesiredLRP{ 38 | { 39 | Instances: 1, 40 | }, 41 | }, 42 | }), 43 | ), 44 | ) 45 | }) 46 | 47 | It("returns the json encoding of the desired lrp scheduling info", func() { 48 | sess := RunCFDot("desired-lrps") 49 | Eventually(sess).Should(gexec.Exit(0)) 50 | Expect(sess.Out).To(gbytes.Say(`"instances":1`)) 51 | }) 52 | 53 | Context("when timeout flag is present", func() { 54 | Context("when request exceeds timeout", func() { 55 | BeforeEach(func() { 56 | serverTimeout = 2 57 | }) 58 | 59 | It("exits with code 4 and a timeout message", func() { 60 | sess := RunCFDot("desired-lrps", "--timeout", "1") 61 | Eventually(sess, 2).Should(gexec.Exit(4)) 62 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 63 | }) 64 | }) 65 | 66 | Context("when request is within the timeout", func() { 67 | It("exits with status code of 0", func() { 68 | sess := RunCFDot("desired-lrps", "--timeout", "1") 69 | Eventually(sess).Should(gexec.Exit(0)) 70 | Expect(sess.Out).To(gbytes.Say(`"instances":1`)) 71 | }) 72 | }) 73 | }) 74 | }) 75 | 76 | Context("when passing filters", func() { 77 | BeforeEach(func() { 78 | bbsServer.AppendHandlers( 79 | ghttp.CombineHandlers( 80 | ghttp.VerifyRequest("POST", "/v1/desired_lrps/list.r3"), 81 | ghttp.VerifyProtoRepresenting(&models.DesiredLRPsRequest{ 82 | Domain: "cf-apps", 83 | }), 84 | ghttp.RespondWithProto(200, &models.DesiredLRPsResponse{ 85 | DesiredLrps: []*models.DesiredLRP{ 86 | { 87 | Instances: 1, 88 | }, 89 | }, 90 | }), 91 | ), 92 | ) 93 | }) 94 | 95 | Context("when -d is used as a filter flag", func() { 96 | It("exits with a status code of 0", func() { 97 | sess := RunCFDot("desired-lrps", "-d", "cf-apps") 98 | Eventually(sess).Should(gexec.Exit(0)) 99 | }) 100 | }) 101 | 102 | Context("when --domain is used as a filter flag", func() { 103 | It("exits with a status code of 0", func() { 104 | sess := RunCFDot("desired-lrps", "--domain", "cf-apps") 105 | Eventually(sess).Should(gexec.Exit(0)) 106 | }) 107 | }) 108 | 109 | Context("when --domain and -d are supplied as filter flags", func() { 110 | It("exits with a status code of 3", func() { 111 | sess := RunCFDot("desired-lrps", "--domain", "cf-apps", "-d", "cf-apps") 112 | Eventually(sess).Should(gexec.Exit(3)) 113 | }) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /integration/fixtures/locketCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhsb2Nr 3 | ZXRDQTAeFw0yMzEyMDUyMjMwNTNaFw0zMzEyMDUyMjQwNTFaMBMxETAPBgNVBAMT 4 | CGxvY2tldENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA62VKvjcD 5 | I1BlQ04CG2+GWRugtawawSlhuGDwbEhNGZKYyD0MKxUQHZaoc5woJ7ugFJAbNWsj 6 | +75WY4fX/BC+Z/nXUhu1hPYKhgqi9/HkWVbhb1cCL+DPezP9DNAu7+OoSVkx/ZW8 7 | VPznkXr7rZg/e9wxSZQnMCGD/8yITQhrOwf6VtdpBuvKT+tIN7irjpZgLVyoTV4r 8 | z/4sglOpZCnu9BzMahQFaTYxqVEJnwNGhehM1Yhtmv9eCOzmiE7+2WB8QoKqOwdA 9 | 0OfVxokuqn13jMtwLKli7fjmkbm8CSh8AEgI0y8V6w+vrzHcFZ6QqlRzK0aZtkY1 10 | 7PIcmq9XPKtRkzioWrXpgC7aLnO+ZYDX2s+AohKHYh8Axmi1vxSGGpjiekC3INrI 11 | smeWJdNN1aIyifubrleN2Pf+RqmRp9w3qDqLeWwTSCkrJ/yj/aXtNaZwqVD9XpUX 12 | FSCNzm5J76z0hI/7oGtlx5HmozJAH/H4z1VGYnpmtVuvuw+sUxSoXKID6SB/6VHz 13 | X6UcmltD797PTyqxhFvILHKx2UrFxPeqq+wu7sJJ4Uly+E6VxiXZDIvP4pAUB/a0 14 | ggJal8XRYuJ8Ud7mHpwc9PGmyTcw3dBaQqNu1HKgWYNtq0TGATAIfTZBWMTHAWkI 15 | de7H/LpPv+ZyZ5yOvLpJpLLLPjnggZCZVG0CAwEAAaNFMEMwDgYDVR0PAQH/BAQD 16 | AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFCHejwxnOMplfw2LM3r/ 17 | ZOwjzJhyMA0GCSqGSIb3DQEBCwUAA4ICAQC1G3MZYMtFAnPgUTNbxWJFCdkp802C 18 | sfdbITcTO4ECei/f/LL1sBOrRHlkvTLBTVjhZHHnfoUUIDAyRfiSZ1uXh+mFUipP 19 | kR+xl/1JdI/IQmNXLSWD3GEFA86v7SBht5eI9fpxYbFQczSi3NRtoJ6QZSZzBoi5 20 | jUV2CjRX1SRBP7iNQpRFCM/2sZAwVJYOk9XgUybJPMXyPiIY3g/iXI7T16bDQ5aT 21 | eJw1GaU8qqmLECN2Hqqkfa/R5svF+d5PdSRPWDiBN0iHOPmzCOaMnB9I4TzikY2s 22 | kNlTJkzoK4xow1OWycmpQHoPt4OQMCMmolC4WW4CCk1MlwCu32erHvRBxFmySkS5 23 | vhJb947xJ6/9tJrJlSRwwVbgnVRJkYRlUOKX74h10JIUiauLHB3UGDieuqce/jYp 24 | X6ZVnCkzoiMz2FoOK+Fm7DLcSWWZucXDBSewVgo7JF4aTwDiAXPhRJjp85OCH+vj 25 | b5bmQkQ4hV/BCEuGibmE95iGiLN15Q72yR4THRK0wSxfIY8CVLVHT6P9gKYn2TWb 26 | qEn9QFLuQlGcx0yiCp8jCfeL62oH3xcGAWQKfEige/JuIx+yWHoh4Obck1Ze2aRc 27 | mjs3t/XXEqMzhQ0Rnd9wOI9Gg8iP3FkjSYLmFWEjygNoAS3EbU6wC4nnI+NKj+tf 28 | ppoCiPcoWujQfw== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /integration/fixtures/locketClient.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEOTCCAiGgAwIBAgIRAJjVVcggnFomZDpih3ShMEYwDQYJKoZIhvcNAQELBQAw 3 | EzERMA8GA1UEAxMIbG9ja2V0Q0EwHhcNMjMxMjA1MjIzMDU0WhcNMzMxMjA1MjI0 4 | MDUwWjAXMRUwEwYDVQQDEwxsb2NrZXRDbGllbnQwggEiMA0GCSqGSIb3DQEBAQUA 5 | A4IBDwAwggEKAoIBAQC2007jtBUYT0TYL1/ENPcC8Iybj1QGdfP0h+xEY5jUfZfh 6 | gjelDp9fe1RwdW4EgLLDXBAiHrUnrkeBl1FY5WHnWHOfpUR7NqKg/zHRsX/my692 7 | ig23kcbnksZ/xBSFeKNZdHrJgRw8EfFAvmSFHjNCuY3bVVig7gum7UuMFpg5Ohjy 8 | qcYxRulVLWcuD4ZBP5jZI3D8qqhRUGiWQwXpGQL79qGUe91xiOxEOxT01CttyABV 9 | SANh5xizBaNNHwyKGQx7KRsy4CC67oc7yukyI2ovlS/gDRWi/NDkyI4lklVS5y76 10 | Jlf51w9R7uyxem82057ih3RMfUJBH9C98y96xgTdAgMBAAGjgYMwgYAwDgYDVR0P 11 | AQH/BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4E 12 | FgQUD1K/7YqfI+Ixn3AG7opdUq5Z3XIwHwYDVR0jBBgwFoAUId6PDGc4ymV/DYsz 13 | ev9k7CPMmHIwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAVYz0 14 | rcMXJM0LOzzjCorxxdY3SZcJwh973YDLfGiCnIyd4P+iRX9eKSKFkOPZAp4i4j9g 15 | CCYkVHVceFB4ZuB1rXBhzYAcIA++qOeJIxEmOw9jUgmT7I0gWOKR0piPahxBL3dx 16 | e4ZnSV+7RamG55JqRWgnw+W4X+L+pUvv+hbfjR6tBWh63tJkCV5ejVKGL3Mqx/fS 17 | nMf2V3tmAyIsJLrqbyHeSSG/db/EskLyOSPx8WdMdx8WaQRNhUf6cxBduIwb+GFB 18 | K2At+VkEWhYy1ruylccZoCBl+uoKaDPe2rRirpFSLiBwt12ysnEpZgkXdBaMn2ay 19 | cjPTCoGYuoQP8f3ugmvE10PuAk8C10IipcrAVe4zp9/JmjKpzSttiznq1r4odFt0 20 | i+N3vB/pY3Oc8YUXOjaVrLZLYXGOOXnf7GiLpfEZdkKFWrn0bfFlfr+2fDxZrPZ1 21 | yM5zXEM6UKDxqGNtXwYAMPhuU1sixUJI/Yi3Noh8ck4dJJt9dWJKPCv0GpXF/E6b 22 | KhLJwWsDCn2l0VAn4+F82fX93+6PsVPiUkoRgcTavtc01v8NUhmmYvE7qdOVHgFO 23 | gpOkPHkDQ8fQ5Kmp7kjNxg8amDaQmPx5auWmDcpEJjwV98iloGHV8fyUXrGHbnMz 24 | h6Gp84Ifdh7hFVfI8gMETFyHtfVICdfiL7skkno= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /integration/fixtures/locketClient.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAttNO47QVGE9E2C9fxDT3AvCMm49UBnXz9IfsRGOY1H2X4YI3 3 | pQ6fX3tUcHVuBICyw1wQIh61J65HgZdRWOVh51hzn6VEezaioP8x0bF/5suvdooN 4 | t5HG55LGf8QUhXijWXR6yYEcPBHxQL5khR4zQrmN21VYoO4Lpu1LjBaYOToY8qnG 5 | MUbpVS1nLg+GQT+Y2SNw/KqoUVBolkMF6RkC+/ahlHvdcYjsRDsU9NQrbcgAVUgD 6 | YecYswWjTR8MihkMeykbMuAguu6HO8rpMiNqL5Uv4A0VovzQ5MiOJZJVUucu+iZX 7 | +dcPUe7ssXpvNtOe4od0TH1CQR/QvfMvesYE3QIDAQABAoIBAG4PZVWEypI43iv6 8 | 0P8QF4eogiz/qsi852DVFrHQ92csOGukdZUi6WSUnS+aMeH6DCO5X2j67Xl23Esy 9 | aeFHLZoU0H5SApjeOicLmdUvstygz/9feoSA+Knd3St7QhNbnZKzoVt/Ix0ye/bK 10 | 5z6QALHxfGmGxnUS+ObDwZAS5g7EWm5M5bQ+QIoJyuUuFAyakgQK8h9lzZhY2L+k 11 | mZKH86gGbcVcIogDd/nWvqGHWmwshOO13cZqNICbEzO5KhQD8Vlix1bF9RSlnhyc 12 | ZtzZRME7pgr9qR9CNI3JGITtfygUlUa8+DirMPKkDYZ/5yj+ZfdgcgytjArOoBWy 13 | NgKKVw0CgYEAxvP6k4FBN42VJOEbxYHFXqqDPl3WelogOJ2MDBMulZvMPjQ8Ps+x 14 | KLHyEN9/I6MveT2/1K2lhRTRw80znP1ZSePUIXjZM7rpXAasA6f4NfrLY9PEyZmK 15 | 57vfmZ5Ugi5+FIAkfg7v45QOjJhafpZNa2eMd5GIz61wGGWDD/Oak88CgYEA6z99 16 | 4IoQPXRKTcQwdcQm3Mx9i/zOPg6BgOpSCtJ/z0V1GCP2WX5oaS5ukddGJUXUiPwy 17 | TrEW/DEO8mi++5NUGMC+AQDh0VJbkV1Y9hPvy7BdE5hX+RrbzTgpKMMvL4ZrBzxD 18 | 7SBoC8x7i76IZCwvIQjNT22rkWO8oHv7OVrSy5MCgYB0Rx4ZXb7fGAmmD0bq7UiM 19 | Zn3kUnXK9qfETxgN6pwcZn09kcvwLfTtwuqmP+mPy40wgA2A4KhPmnZqyKC0zUGF 20 | NPnDQl03pbNDzMZ2MJKaAeUjiB6IcQvFGoG5x9a+ayvXEXnCjzpXOS4zr8ucFn5T 21 | Gqb3LwDV2BkeuR2bkhWy8wKBgCXDE1mvz9uNhz/kdkLxMavPx3dpJVIg0fj3pCPI 22 | lmuXIqy1WR4UhH4dzfHrNH9XLj6u/QRUuPPSjCutVLrdc2plxhozIgyPw9MZwOX/ 23 | XUa9ELPMiJKcFRxkH3dDnZdrZFLNAbpWsVzzfgF4nDw4O3h6CTrux9OWxvib7j/O 24 | rHexAoGAHMZajiJvMRSCXTOvqjAi8VDw4EiEF99LQMwgk/AyRcflviXzLPqpB5Yo 25 | 2WAW4I3yCOPnce2tyroCUlzKpA/RxnGs5Rp21DkIE1BZnn9/JVlrpL0PS3CI23u1 26 | Lpfh8XoPJ7iSwPZnQ4+D8wzP9B5wMGI/yNHf1RLGoAN+Ad1VN48= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /integration/fixtures/locketServer.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEQzCCAiugAwIBAgIQaCoaVe7gp6fE5chtcZvwEzANBgkqhkiG9w0BAQsFADAT 3 | MREwDwYDVQQDEwhsb2NrZXRDQTAeFw0yMzEyMDUyMjMwNTNaFw0zMzEyMDUyMjQw 4 | NTBaMBcxFTATBgNVBAMTDGxvY2tldFNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBALRC/FnRmd8n00sriR7YL76x6J599VrtmhjM14oMMGjqEaEu 6 | PhA1eLqyCdFmofgvcbh0Mjk3U7hR2F8W3L0c8qe0pQZifpM5+pzcePsnazaUoQzn 7 | Qu/zkointhbQbulfq3VGhmiN12RUOZtB6dF+thHWZhHOSkXJDszdyWw9AjPF4EJj 8 | ZlfDDBjlxPVZwXYZC68Sv65svSe/KjbFKiY6V/924MGDKVXotFA3/HnvfYHmQKWK 9 | mqovAbOB8PmoufTj7Jf+FHPf2QGZW4I3dcMZBHBGseQQaNBKgpIYnsloPaCmXyCG 10 | O0dzb/svt4gDU5nJDsGjDYtAqCP0+fBhJca6QhcCAwEAAaOBjjCBizAOBgNVHQ8B 11 | Af8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW 12 | BBStXA5axCfpzZwNCttESIfkgAmO4jAfBgNVHSMEGDAWgBQh3o8MZzjKZX8NizN6 13 | /2TsI8yYcjAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEL 14 | BQADggIBAB+LENAWmPegE0sZDe25suFHSDWuEbFkUvzJu0+U7GN4tWI0Gs1Z5HAD 15 | 5HjIjlPoBxcK8dFIAK7LaUz0woyYPT4UghHLNaeYddZZDb/RWbrqtBqamh2himOM 16 | H0XH+1oWPjaSr9T29gzZR8cg99FP6B8Ev/bhwjcAZjOBdb/h/G0blSy8FtA9M5+i 17 | 4sswmQ2ScgCVFHGVVEtD/gCB6a50IPyh2ZfEM95ucNjimtuMg+I1omXHbda9bwlt 18 | xe03JAvkFM/zDqZuyBLVXMaBgLXxN0WFxGLVYLH2fq39cq13bBhOx2CQveJNm8xv 19 | 4ImdH8wIt3obqKuFdkf+grYtLdd4NOQtF3IUOtpaQwS7mHRlzC/FmiYrYRIiVyBK 20 | EU8WJRi9ES43r7Lpa9y/slWgbSSBpOA32blmyQKfIRyFBdw3VWdEkDqJgQMqh6aH 21 | 7Z6UpCZLZNscHJln6WkC28Co3xfWbumyYxJKlZ/rjytbKWxeRBE7hWIq+bmqY8gc 22 | 7MMIBMHZxhVwjpSMXU5YxrHZY3Z7hmaMvi+zm+B/n7xDtCQpWV5x8ykq/WUKp1NH 23 | yB4BU5dTcYlNrMuGWSTqn47PlAzy0so0EYx/qgGYPQqctbolxb2nXdbMGTbiXqwr 24 | KBIoVrcNnYAteKo0yfQ3NpKEslyt1O0T3DMIT0z/F8mEpHZcp7Iv 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /integration/fixtures/locketServer.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtEL8WdGZ3yfTSyuJHtgvvrHonn31Wu2aGMzXigwwaOoRoS4+ 3 | EDV4urIJ0Wah+C9xuHQyOTdTuFHYXxbcvRzyp7SlBmJ+kzn6nNx4+ydrNpShDOdC 4 | 7/OSiKe2FtBu6V+rdUaGaI3XZFQ5m0Hp0X62EdZmEc5KRckOzN3JbD0CM8XgQmNm 5 | V8MMGOXE9VnBdhkLrxK/rmy9J78qNsUqJjpX/3bgwYMpVei0UDf8ee99geZApYqa 6 | qi8Bs4Hw+ai59OPsl/4Uc9/ZAZlbgjd1wxkEcEax5BBo0EqCkhieyWg9oKZfIIY7 7 | R3Nv+y+3iANTmckOwaMNi0CoI/T58GElxrpCFwIDAQABAoIBAQCBmjOzJDa8MfcB 8 | cbCpg4d4VJNJ+IfCM1h6gjRppW/czoWUXBn1L2y41i6m5mKHfR0HIEvHxValC/tE 9 | Crz0Hu770q5fk8srcSEVTJKTCHlWCbBtBYyj1nzBUctiRcOoeMdejzEt1OVK1h+A 10 | 1/gTTkX7Nym1nZra576DXuZlsst1ipdWWMG9C836vOurGgo8Yzeu6WgKjvUalwO6 11 | wwVAFS9KrgDa7wu36UffJtUyzwi/1oI0fZPIFtoc4A9vIiX0YK0gW8tn1AbPJZNM 12 | 1YEfCtL4+Is+MeGrt59Fmyn9hSBa9/MLGSVecA9mhPPXsr2BEzD4QjVLqzK8y7wq 13 | nPstsnVhAoGBANec6ACdKCOx+SoiT4rOUsnCYzoIcHi/UH7K3cWtQ8juORGQrcnp 14 | gfSsEx1ds12WZexkLg1GekP6wtXO4t+XyfqAhHMa+N79efx3bZsmbbdxZblh6sCb 15 | LGnOIAHVfcbLMu5MKXko8xuMwIJJw+KMuxhaUeKVsfwA6fUdr0G12pNxAoGBANYG 16 | 6ZWr1y6VUU9ETIRCPploKB1GMf23LEJ6Vd47akaBFawnQWv39c63Ltxq3b0ZTO6l 17 | eN4uR4T0A8XVoj6JErNfUn3o1nexScNP9S7LTogEF3q5SVYucxoy11Dso/A8HiAd 18 | qJtJM+domlKduwVNv1+H3C4o8iTamgGRFaSSUtoHAoGACNOwn83PRd7UX4g2D22w 19 | 7/eYTljoGdljNz2g5KXaP5CZH4H3y2tW2ahtw+cBH9S3aX0UcZCwErZiYZRtr0+s 20 | gifexEOXarGf29kb0J07IqZuzq8WiDoSEGbS4qBV3NyWwjC2BfLKOReDDhJtBYpT 21 | YoTYE1mjhriW22UStrL2NtECgYAM8gYW43n0e1ubq8nlBgy7Qq2Kd8B2VCn9K2Ee 22 | PAHFmfExpH40hZ78jd/rvRsEYQ9iL+gLqEjzJb9ErBNQLQJQjydyLi8qtjJ7+c4e 23 | snVhjL3O0n45FD4qC4Eyh92ynjBESQb1vYvCJ3WOIbQ6MeWtZY5PzHI5AG/oQFpK 24 | xf706QKBgGvRnHUXvCUtV/4mPuraCbVWSnxRaCTmk2ZykooEUk7gTVN/dUTibNei 25 | s16GwHF1iTaD+6zSIxidvTyWJr6A2b+nrjEnWv8DHvYvBuoHkMfnTtjQALUW0ds2 26 | dxt9iJ0xl+S+h/kYFm7BnCCQ3kkgtyUESMbSXeY6Gai9LfJRJEFq 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /integration/fixtures/regenerate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | this_dir="$(cd $(dirname $0) && pwd)" 6 | 7 | pushd "$this_dir" 8 | 9 | rm -rf out 10 | certstrap init --common-name "locketCA" --passphrase "" --expires "10 years" 11 | 12 | certstrap request-cert --common-name "locketServer" --ip "127.0.0.1" --domain "localhost" --passphrase "" 13 | certstrap sign locketServer --CA "locketCA" --expires "10 years" 14 | 15 | certstrap request-cert --common-name "locketClient" --ip "127.0.0.1" --passphrase "" 16 | certstrap sign locketClient --CA "locketCA" --expires "10 years" 17 | 18 | mv -f out/locketServer.crt ./locketServer.crt 19 | mv -f out/locketServer.key ./locketServer.key 20 | mv -f out/locketClient.crt ./locketClient.crt 21 | mv -f out/locketClient.key ./locketClient.key 22 | mv -f out/locketCA.crt ./locketCA.crt 23 | 24 | rm -rf out 25 | 26 | 27 | 28 | popd 29 | -------------------------------------------------------------------------------- /integration/package.go: -------------------------------------------------------------------------------- 1 | package integration // import "code.cloudfoundry.org/cfdot/integration" 2 | -------------------------------------------------------------------------------- /integration/presences_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "time" 8 | 9 | "code.cloudfoundry.org/lager/v3/lagertest" 10 | "code.cloudfoundry.org/locket" 11 | "code.cloudfoundry.org/locket/models" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/gbytes" 16 | "github.com/onsi/gomega/gexec" 17 | ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2" 18 | ) 19 | 20 | var _ = Describe("Presences", func() { 21 | itValidatesLocketFlags("presences") 22 | itHasNoArgs("presences", true) 23 | 24 | var ( 25 | logger *lagertest.TestLogger 26 | ) 27 | 28 | BeforeEach(func() { 29 | os.Setenv("CA_CERT_FILE", locketCACertFile) 30 | os.Setenv("CLIENT_CERT_FILE", locketClientCertFile) 31 | os.Setenv("CLIENT_KEY_FILE", locketClientKeyFile) 32 | 33 | logger = lagertest.NewTestLogger("locket") 34 | }) 35 | 36 | AfterEach(func() { 37 | os.Unsetenv("CA_CERT_FILE") 38 | os.Unsetenv("CLIENT_CERT_FILE") 39 | os.Unsetenv("CLIENT_KEY_FILE") 40 | }) 41 | 42 | Context("when the server responds with presences", func() { 43 | BeforeEach(func() { 44 | locketConfig := locket.ClientLocketConfig{ 45 | LocketAddress: locketAPILocation, 46 | LocketCACertFile: locketCACertFile, 47 | LocketClientCertFile: locketClientCertFile, 48 | LocketClientKeyFile: locketClientKeyFile, 49 | } 50 | locketClient, err := locket.NewClient(logger, locketConfig) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | req := &models.LockRequest{ 54 | Resource: &models.Resource{ 55 | Key: "key", 56 | Owner: "owner", 57 | Value: "value", 58 | TypeCode: models.PRESENCE, 59 | }, 60 | TtlInSeconds: 10, 61 | } 62 | 63 | _, err = locketClient.Lock(context.Background(), req) 64 | Expect(err).NotTo(HaveOccurred()) 65 | }) 66 | 67 | It("locks prints a json stream of all the locks", func() { 68 | cfdotCmd := exec.Command(cfdotPath, "--locketAPILocation", locketAPILocation, "presences") 69 | 70 | sess, err := gexec.Start(cfdotCmd, GinkgoWriter, GinkgoWriter) 71 | Expect(err).NotTo(HaveOccurred()) 72 | 73 | Eventually(sess).Should(gexec.Exit(0)) 74 | Expect(sess.Out).To(gbytes.Say(`"key":"key","owner":"owner"`)) 75 | }) 76 | }) 77 | 78 | Context("when the server is down", func() { 79 | BeforeEach(func() { 80 | ginkgomon.Interrupt(locketProcess) 81 | }) 82 | 83 | It("presences fails with a relevant error message", func() { 84 | cfdotCmd := exec.Command(cfdotPath, "--locketAPILocation", locketAPILocation, "presences") 85 | 86 | sess, err := gexec.Start(cfdotCmd, GinkgoWriter, GinkgoWriter) 87 | Expect(err).NotTo(HaveOccurred()) 88 | 89 | Eventually(sess, 11*time.Second).Should(gexec.Exit(4)) 90 | Expect(sess.Err).To(gbytes.Say("context deadline exceeded")) 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /integration/retire_actual_lrp_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "code.cloudfoundry.org/bbs/models" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("retire-actual-lrp", func() { 17 | itValidatesBBSFlags("retire-actual-lrp", "test-guid", "1") 18 | 19 | Context("when the bbs returns everything successfully", func() { 20 | var ( 21 | serverTimeout int 22 | ) 23 | 24 | BeforeEach(func() { 25 | serverTimeout = 0 26 | }) 27 | 28 | JustBeforeEach(func() { 29 | bbsServer.AppendHandlers( 30 | ghttp.CombineHandlers( 31 | ghttp.VerifyRequest("POST", "/v1/desired_lrps/get_by_process_guid.r3"), 32 | func(w http.ResponseWriter, req *http.Request) { 33 | time.Sleep(time.Duration(serverTimeout) * time.Second) 34 | }, 35 | ghttp.RespondWithProto(200, &models.DesiredLRPResponse{ 36 | DesiredLrp: &models.DesiredLRP{ 37 | Domain: "test-domain", 38 | }, 39 | }), 40 | ), 41 | ghttp.CombineHandlers( 42 | ghttp.VerifyRequest("POST", "/v1/actual_lrps/retire"), 43 | ghttp.RespondWithProto(200, &models.ActualLRPLifecycleResponse{}), 44 | ), 45 | ) 46 | }) 47 | 48 | It("exits with exit code 0", func() { 49 | session := RunCFDot("retire-actual-lrp", "test-process-guid", "1") 50 | Eventually(session).Should(gexec.Exit(0)) 51 | }) 52 | 53 | Context("when timeout flag is present", func() { 54 | Context("when request exceeds timeout", func() { 55 | BeforeEach(func() { 56 | serverTimeout = 2 57 | }) 58 | 59 | It("exits with code 4 and a timeout message", func() { 60 | session := RunCFDot("retire-actual-lrp", "--timeout", "1", "test-process-guid", "1") 61 | Eventually(session, 2).Should(gexec.Exit(4)) 62 | Expect(session.Err).To(gbytes.Say(`Timeout exceeded`)) 63 | }) 64 | }) 65 | 66 | Context("when request is within the timeout", func() { 67 | It("exits with status code of 0", func() { 68 | session := RunCFDot("retire-actual-lrp", "--timeout", "1", "test-process-guid", "1") 69 | Eventually(session).Should(gexec.Exit(0)) 70 | }) 71 | }) 72 | }) 73 | }) 74 | 75 | Context("when the bbs returns an error", func() { 76 | BeforeEach(func() { 77 | bbsServer.AppendHandlers( 78 | ghttp.CombineHandlers( 79 | ghttp.VerifyRequest("POST", "/v1/desired_lrps/get_by_process_guid.r3"), 80 | ghttp.RespondWithProto(200, &models.DesiredLRPResponse{ 81 | Error: &models.Error{ 82 | Type: models.Error_Deadlock, 83 | Message: "the request failed due to deadlock", 84 | }, 85 | }), 86 | ), 87 | ) 88 | }) 89 | 90 | It("exits with exit code 4", func() { 91 | session := RunCFDot("retire-actual-lrp", "test-process-guid", "1") 92 | Eventually(session).Should(gexec.Exit(4)) 93 | }) 94 | }) 95 | 96 | Context("when invalid arguments are passed", func() { 97 | It("exits with exit code 3", func() { 98 | session := RunCFDot("retire-actual-lrp", "test-process-guid", "a") 99 | Eventually(session).Should(gexec.Exit(3)) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /integration/set_domain_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "net/http" 5 | "os/exec" 6 | "time" 7 | 8 | "code.cloudfoundry.org/bbs/models" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | "github.com/onsi/gomega/ghttp" 15 | ) 16 | 17 | var _ = Describe("set-domain", func() { 18 | itValidatesBBSFlags("set-domain", "domain1") 19 | 20 | Context("when the server responds for set-domain", func() { 21 | var ( 22 | serverTimeout int 23 | ) 24 | 25 | BeforeEach(func() { 26 | serverTimeout = 0 27 | }) 28 | 29 | JustBeforeEach(func() { 30 | bbsServer.AppendHandlers( 31 | ghttp.CombineHandlers( 32 | ghttp.VerifyRequest("POST", "/v1/domains/upsert"), 33 | func(w http.ResponseWriter, req *http.Request) { 34 | time.Sleep(time.Duration(serverTimeout) * time.Second) 35 | }, 36 | ghttp.RespondWithProto(200, &models.UpsertDomainResponse{}), 37 | ), 38 | ) 39 | }) 40 | 41 | It("set-domain works with a TTL not specified", func() { 42 | sess := RunCFDot("set-domain", "any-domain") 43 | Eventually(sess).Should(gexec.Exit(0)) 44 | }) 45 | 46 | It("set-domain works with a TTL specified", func() { 47 | sess := RunCFDot("set-domain", "any-domain", "--ttl", "40s") 48 | Eventually(sess).Should(gexec.Exit(0)) 49 | }) 50 | 51 | It("set-domain prints to stderr when no domain specified", func() { 52 | sess := RunCFDot("set-domain", "", "--ttl", "40s") 53 | Eventually(sess).Should(gexec.Exit(3)) 54 | Expect(sess.Err).To(gbytes.Say(`No domain given`)) 55 | Expect(sess.Err).To(gbytes.Say(`Usage`)) 56 | }) 57 | 58 | It("set-domain prints to stderr for negative TTL", func() { 59 | sess := RunCFDot("set-domain", "any-domain", "--ttl", "-40s") 60 | Eventually(sess).Should(gexec.Exit(3)) 61 | Expect(sess.Err).To(gbytes.Say(`ttl is negative`)) 62 | Expect(sess.Err).To(gbytes.Say(`Usage:`)) 63 | }) 64 | 65 | It("set-domain prints to stderr for non-numeric TTL", func() { 66 | cfdotCmd := exec.Command(cfdotPath, "--bbsURL", bbsServer.URL(), "set-domain", "any-domain", "-t", "asdf") 67 | 68 | sess, err := gexec.Start(cfdotCmd, GinkgoWriter, GinkgoWriter) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | Eventually(sess).Should(gexec.Exit(3)) 72 | Expect(sess.Err).To(gbytes.Say(`invalid duration`)) 73 | Expect(sess.Err).To(gbytes.Say(`Usage:`)) 74 | }) 75 | 76 | Context("when timeout flag is present", func() { 77 | Context("when request exceeds timeout", func() { 78 | BeforeEach(func() { 79 | serverTimeout = 2 80 | }) 81 | 82 | It("exits with code 4 and a timeout message", func() { 83 | sess := RunCFDot("--timeout", "1", "set-domain", "any-domain") 84 | Eventually(sess, 2).Should(gexec.Exit(4)) 85 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 86 | }) 87 | }) 88 | 89 | Context("when request is within the timeout", func() { 90 | It("exits with status code of 0", func() { 91 | sess := RunCFDot("--timeout", "1", "set-domain", "any-domain") 92 | Eventually(sess).Should(gexec.Exit(0)) 93 | }) 94 | }) 95 | }) 96 | }) 97 | 98 | Context("when the server does not respond for set-domain", func() { 99 | BeforeEach(func() { 100 | bbsServer.RouteToHandler("POST", "/v1/domains/upsert", 101 | ghttp.RespondWith(500, []byte{})) 102 | }) 103 | 104 | It("set-domain fails with a relevant error message", func() { 105 | sess := RunCFDot("set-domain", "any-domain") 106 | Eventually(sess, 2*time.Second).Should(gexec.Exit(4)) 107 | Expect(sess.Err).To(gbytes.Say("Invalid Response with status code: 500")) 108 | }) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /integration/task_events_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/bbs/events" 5 | "code.cloudfoundry.org/bbs/models" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | "github.com/onsi/gomega/gexec" 11 | "github.com/onsi/gomega/ghttp" 12 | ) 13 | 14 | var _ = Describe("task-events", func() { 15 | itValidatesBBSFlags("task-events") 16 | itHasNoArgs("task-events", false) 17 | 18 | Context("when the server responds with events", func() { 19 | BeforeEach(func() { 20 | task := models.Task{TaskGuid: "some-guid"} 21 | taskEvent := models.NewTaskRemovedEvent(&task) 22 | sseEvent, err := events.NewEventFromModelEvent(1, taskEvent) 23 | Expect(err).ToNot(HaveOccurred()) 24 | 25 | bbsServer.AppendHandlers( 26 | ghttp.CombineHandlers( 27 | ghttp.VerifyRequest("POST", "/v1/events/tasks.r1"), 28 | ghttp.RespondWith(200, sseEvent.Encode()), 29 | ), 30 | ) 31 | }) 32 | 33 | It("prints out the event stream", func() { 34 | sess := RunCFDot("task-events") 35 | Eventually(sess).Should(gexec.Exit(0)) 36 | Expect(sess.Out).To(gbytes.Say("some-guid")) 37 | }) 38 | }) 39 | 40 | Context("when there is a BBS error", func() { 41 | BeforeEach(func() { 42 | bbsServer.AppendHandlers( 43 | ghttp.CombineHandlers( 44 | ghttp.VerifyRequest("POST", "/v1/events/tasks.r1"), 45 | ghttp.RespondWith(418, ""), 46 | ), 47 | ) 48 | }) 49 | 50 | It("responds with a status code 4", func() { 51 | sess := RunCFDot("task-events") 52 | Eventually(sess).Should(gexec.Exit(4)) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /integration/task_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "code.cloudfoundry.org/bbs/models" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("task", func() { 17 | itValidatesBBSFlags("task", "task-guid") 18 | 19 | Context("when the server responds for task", func() { 20 | var task = &models.Task{ 21 | TaskGuid: "task-guid", 22 | } 23 | 24 | var ( 25 | serverTimeout int 26 | ) 27 | 28 | BeforeEach(func() { 29 | serverTimeout = 0 30 | }) 31 | 32 | JustBeforeEach(func() { 33 | bbsServer.AppendHandlers( 34 | ghttp.CombineHandlers( 35 | ghttp.VerifyRequest("POST", "/v1/tasks/get_by_task_guid.r3"), 36 | func(w http.ResponseWriter, req *http.Request) { 37 | time.Sleep(time.Duration(serverTimeout) * time.Second) 38 | }, 39 | ghttp.VerifyProtoRepresenting(&models.TaskByGuidRequest{TaskGuid: "task-guid"}), 40 | ghttp.RespondWithProto(200, &models.TaskResponse{Task: task}), 41 | ), 42 | ) 43 | }) 44 | 45 | It("task prints a json representation of the task", func() { 46 | sess := RunCFDot("task", "task-guid") 47 | Eventually(sess).Should(gexec.Exit(0)) 48 | 49 | taskJSON, err := json.Marshal(task) 50 | Expect(err).NotTo(HaveOccurred()) 51 | Expect(sess.Out.Contents()).To(MatchJSON(taskJSON)) 52 | }) 53 | 54 | Context("when timeout flag is present", func() { 55 | Context("when request exceeds timeout", func() { 56 | BeforeEach(func() { 57 | serverTimeout = 2 58 | }) 59 | 60 | It("exits with code 4 and a timeout message", func() { 61 | sess := RunCFDot("task", "task-guid", "--timeout", "1") 62 | Eventually(sess, 2).Should(gexec.Exit(4)) 63 | Expect(sess.Err).To(gbytes.Say(`Timeout exceeded`)) 64 | }) 65 | }) 66 | 67 | Context("when request is within the timeout", func() { 68 | It("exits with status code of 0", func() { 69 | sess := RunCFDot("task", "task-guid", "--timeout", "1") 70 | Eventually(sess).Should(gexec.Exit(0)) 71 | }) 72 | }) 73 | }) 74 | }) 75 | 76 | Context("when the server responds with error", func() { 77 | It("exits with status code 4", func() { 78 | bbsServer.RouteToHandler( 79 | "POST", 80 | "/v1/tasks/get_by_task_guid.r3", 81 | ghttp.RespondWithProto(200, &models.TaskResponse{ 82 | Error: models.ErrUnknownError, 83 | }), 84 | ) 85 | 86 | sess := RunCFDot("task", "task-guid") 87 | Eventually(sess).Should(gexec.Exit(4)) 88 | 89 | Expect(sess.Err).To(gbytes.Say("UnknownError")) 90 | }) 91 | }) 92 | 93 | Context("validates that exactly one guid is passed in", func() { 94 | It("fails with no arguments and prints the usage", func() { 95 | sess := RunCFDot("task") 96 | Eventually(sess).Should(gexec.Exit(3)) 97 | Expect(sess.Err).To(gbytes.Say("Error: Missing arguments")) 98 | Expect(sess.Err).To(gbytes.Say("cfdot task TASK_GUID \\[flags\\]")) 99 | }) 100 | 101 | It("fails with 2+ arguments", func() { 102 | sess := RunCFDot("task", "task-guid", "arg1", "arg2") 103 | Eventually(sess).Should(gexec.Exit(3)) 104 | Expect(sess.Err).To(gbytes.Say("Error: Too many arguments specified")) 105 | Expect(sess.Err).To(gbytes.Say("cfdot task TASK_GUID \\[flags\\]")) 106 | }) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "code.cloudfoundry.org/cfdot/commands" 8 | ) 9 | 10 | func main() { 11 | if err := commands.RootCmd.Execute(); err != nil { 12 | if cfDotError, ok := err.(commands.CFDotError); ok { 13 | os.Exit(cfDotError.ExitCode()) 14 | } 15 | 16 | if strings.Contains(err.Error(), "invalid argument") { 17 | os.Exit(3) 18 | } 19 | 20 | os.Exit(-1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package main // import "code.cloudfoundry.org/cfdot" 2 | -------------------------------------------------------------------------------- /scripts/generate-cfdot-documentation: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | gitrepo=$(cd $(dirname $0)/.. && pwd) 6 | 7 | echo "Generating updated README.md..." 8 | erb $gitrepo/README.md.erb > README.md 9 | 10 | echo "Staging updated README.md..." 11 | git add $gitrepo/README.md 12 | -------------------------------------------------------------------------------- /scripts/install-git-hooks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RELEASE=$(cd $(dirname $0)/.. && pwd) 4 | PRE_COMMIT=${RELEASE}/.git/hooks/pre-commit 5 | 6 | if [ ! -f ${PRE_COMMIT} ]; then 7 | echo "#!/bin/bash" > ${PRE_COMMIT} 8 | chmod +x ${PRE_COMMIT} 9 | fi 10 | 11 | if grep -q 'git-hooks/pre-commit' ${PRE_COMMIT}; then 12 | echo "pre-commit already installed." 13 | else 14 | echo >> ${PRE_COMMIT} 15 | echo './git-hooks/pre-commit' >> ${PRE_COMMIT} 16 | echo "pre-commit installed." 17 | fi 18 | --------------------------------------------------------------------------------