├── .codecov.yml ├── .fsoc-shell-aliases ├── .githooks └── pre-commit ├── .github ├── CODEOWNERS ├── dependabot.yaml ├── settings.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── config.go ├── config │ ├── config.go │ ├── create.go │ ├── delete.go │ ├── get.go │ ├── list.go │ ├── set-fields.go │ ├── set.go │ ├── set_test.go │ ├── show-fields.go │ └── use.go ├── gendocs.go ├── gendocs │ └── gendocs.go ├── iamrole.go ├── iamrole │ ├── iamrole.go │ ├── list.go │ ├── permissions.go │ └── principals.go ├── iamrolebinding.go ├── iamrolebinding │ ├── add.go │ ├── iamrolebinding.go │ ├── list.go │ ├── remove.go │ └── types.go ├── knowledge.go ├── knowledge │ ├── create.go │ ├── delete.go │ ├── edit.go │ ├── get.go │ ├── knowledge.go │ ├── layerid.go │ ├── types.go │ └── update.go ├── login.go ├── login │ └── login.go ├── logs.go ├── logs │ ├── levels.go │ ├── logs.go │ ├── query_template.go │ └── row_format.go ├── melt.go ├── melt │ ├── geometry-test.go │ ├── melt.go │ ├── meltini.go │ ├── model.go │ └── send.go ├── optimize.go ├── optimize │ ├── common.go │ ├── completion.go │ ├── configure.go │ ├── configure_test.go │ ├── events.go │ ├── events_interactive.go │ ├── management.go │ ├── optimize.go │ ├── report.go │ ├── report_test.go │ ├── server_report.go │ ├── servo.go │ ├── status.go │ ├── testdata │ │ ├── configure_test_report.json │ │ ├── configure_test_workload.json │ │ └── report_test.json │ └── types.go ├── provisioning.go ├── provisioning │ ├── api_version.go │ ├── lookup.go │ └── provisioning.go ├── proxy.go ├── proxy │ └── proxy.go ├── root.go ├── solution.go ├── solution │ ├── bump.go │ ├── check.go │ ├── delete.go │ ├── describe.go │ ├── download.go │ ├── embed-isolate.go │ ├── extend-dashui.go │ ├── extend-fmm.go │ ├── extend.go │ ├── fix.go │ ├── fork-legacy.go │ ├── fork.go │ ├── init.go │ ├── isolate.go │ ├── list.go │ ├── package.go │ ├── processor.go │ ├── push.go │ ├── show.go │ ├── solution.go │ ├── status.go │ ├── subscribe.go │ ├── tag.go │ ├── test.go │ ├── types-dashui.go │ ├── types-fmm.go │ ├── types-solution-test.go │ ├── types.go │ ├── unsubscribe.go │ ├── unzip.go │ ├── upload.go │ ├── validate.go │ └── zap.go ├── uql.go ├── uql │ ├── api_version.go │ ├── backend.go │ ├── client.go │ ├── error.go │ ├── execute.go │ ├── execute_test.go │ ├── flat_table.go │ ├── flat_table_test.go │ ├── json.go │ ├── json_test.go │ ├── response.go │ ├── response_test.go │ └── uql.go ├── version.go └── version │ ├── update.go │ ├── version.go │ ├── version_check.go │ ├── verutil.go │ └── verutil_test.go ├── cmdkit ├── cmdkit.go ├── editor │ ├── editor.go │ └── run.go ├── fetch_and_print.go ├── interrupt │ └── interrupt.go └── term │ └── term.go ├── config ├── config.go ├── config_test.go ├── default.go ├── error.go ├── manage.go ├── subsystems.go ├── types.go └── validator.go ├── developer_guide ├── README.md ├── building.md ├── core_services.md ├── domain_config.md ├── new_command_group.md └── testing.md ├── go.mod ├── go.sum ├── logfilter └── cli_logger.go ├── main.go ├── output ├── fixtures │ ├── output_json.txt │ ├── output_table.txt │ ├── output_text.txt │ └── output_yaml.txt ├── output.go └── output_test.go ├── platform ├── api │ ├── abbrev_test.go │ ├── call.go │ ├── call_test.go │ ├── collection.go │ ├── context.go │ ├── error.go │ ├── local.go │ ├── login.go │ ├── login_test.go │ ├── oauth.go │ ├── problem.go │ ├── proxy.go │ ├── service_principal.go │ ├── tenant.go │ ├── tenant_test.go │ ├── types.go │ ├── user.go │ ├── version.go │ └── version_test.go └── melt │ ├── exporter.go │ ├── exporter_test.go │ ├── metric_test.go │ └── types.go └── test ├── config.go └── io.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: main 3 | # only use the latest copy on main branch 4 | strict_yaml_branch: main 5 | 6 | coverage: 7 | precision: 2 8 | round: down 9 | range: "80...100" 10 | status: 11 | project: 12 | default: 13 | enabled: yes 14 | target: 90% 15 | patch: 16 | default: 17 | enabled: yes 18 | target: 95% 19 | -------------------------------------------------------------------------------- /.fsoc-shell-aliases: -------------------------------------------------------------------------------- 1 | # To include these file in shell profile, copy it to your home directory and add the 2 | # following line to your shell profile (~/.bashrc, ~/.zshrc, etc.): 3 | # . ~/.fsoc-shell-aliases 4 | alias fcg='fsoc config get' 5 | alias fcl='fsoc config list' 6 | alias fcup='fsoc config use --profile' 7 | alias fsb='fsoc solution bump' 8 | alias fsc='fsoc solution check' 9 | alias fsde='fsoc solution describe' 10 | alias fsd='fsoc solution download' 11 | alias fsl='fsoc solution list ' 12 | alias fsp='fsoc solution push' 13 | alias fspw='fsoc solution push --wait' 14 | alias fss='fsoc solution status' 15 | alias fssu='fsoc solution subscribe' 16 | alias fsun='fsoc solution unsubscribe' 17 | alias fsv='fsoc solution validate' 18 | alias fu='fsoc uql' 19 | alias fuy='fsoc uql -o yaml' 20 | alias fopc='fsoc optimize configure' 21 | alias fopd='fsoc optimize delete' 22 | alias fope='fsoc optimize events' 23 | alias foprc='fsoc optimize recommendations' 24 | alias foprp='fsoc optimize report' 25 | alias fops='fsoc optimize status' 26 | alias fmp='fsoc melt push' 27 | alias fkc='fsoc knowledge create' 28 | alias fkd='fsoc knowledge delete' 29 | alias fkg='fsoc knowledge get ' 30 | alias fkgt='fsoc knowledge get-type' 31 | alias fku='fsoc knowledge update' 32 | alias fke='fsoc knowledge edit' 33 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -e 3 | make pre-commit 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | #################################################### 2 | # 3 | # List of approvers for fsoc project 4 | # 5 | ##################################################### 6 | # 7 | # Learn about CODEOWNERS file format: 8 | # https://help.github.com/en/articles/about-code-owners 9 | # 10 | 11 | # These owners will be the default owners for everything in 12 | # the repo. Unless a later match takes precedence, 13 | # the following users/teams will be requested for 14 | # review when someone opens a pull request. 15 | * @pnickolov 16 | # * @cisco-open/fsoc-maintainers 17 | 18 | # TODO: assign code owners per subsystem 19 | 20 | # Enforces admin protections for repo configuration via probot settings app. 21 | # ref: https://github.com/probot/settings#security-implications 22 | .github/settings.yml @cisco-open/fsoc-admins 23 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | go-dependencies: 9 | patterns: 10 | - "*" 11 | open-pull-requests-limit: 5 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | groups: 18 | github: 19 | patterns: 20 | - "actions/*" 21 | - "github/*" 22 | open-pull-requests-limit: 3 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '38 5 * * 5' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: [ 'go' ] 39 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 40 | # Use only 'java' to analyze code written in Java, Kotlin or both 41 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 42 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 43 | 44 | steps: 45 | - name: Harden Runner 46 | uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 47 | with: 48 | egress-policy: audit 49 | 50 | - name: Checkout repository 51 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release fsoc binaries 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-20.04 14 | 15 | permissions: 16 | id-token: write 17 | packages: write 18 | contents: write 19 | 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 23 | with: 24 | egress-policy: audit 25 | 26 | - name: Checkout 27 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Setup Go 32 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 33 | with: 34 | go-version: '1.21' 35 | check-latest: true 36 | 37 | - name: Install tools 38 | run: make install-tools 39 | 40 | - name: Set version environment variables 41 | run: make print-version-info >> $GITHUB_ENV 42 | 43 | - name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 45 | with: 46 | version: 'v1.24.0' 47 | args: release --clean --timeout 5m 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLISHER_TOKEN }} 51 | GIT_BRANCH: ${{ github.ref_name }} 52 | BUILD_IS_DEV: 'false' 53 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: scorecard 2 | 3 | on: 4 | push: 5 | branches: 6 | # Run on pushes to default branch 7 | - main 8 | schedule: 9 | # Run weekly on Saturdays 10 | - cron: "30 1 * * 6" 11 | # Run when branch protection rules change 12 | branch_protection_rule: 13 | # Run the workflow manually 14 | workflow_dispatch: 15 | 16 | # Declare default permissions as read-only 17 | permissions: read-all 18 | 19 | jobs: 20 | run-scorecard: 21 | # Call reusable workflow file 22 | uses: cisco-ospo/.github/.github/workflows/_scorecard.yml@main 23 | permissions: 24 | id-token: write 25 | security-events: write 26 | secrets: inherit 27 | with: 28 | # Publish results of Scorecard analysis 29 | publish-results: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # executable from local "go build" 2 | fsoc 3 | fsoc.exe 4 | 5 | # executables from Makefile build (installed tools) 6 | bin/ 7 | 8 | # goreleaser files 9 | dist/ 10 | builds/ 11 | 12 | # Ignore Gradle project-specific cache directory 13 | .gradle 14 | 15 | # Ignore Gradle build output directory (make crosscompile) 16 | build 17 | 18 | .idea 19 | 20 | .trunk 21 | 22 | # .vscode configs 23 | .vscode/ 24 | 25 | # --- General golang ignores 26 | 27 | # Binaries for programs and plugins 28 | *.exe 29 | *.exe~ 30 | *.dll 31 | *.so 32 | *.dylib 33 | 34 | # Test binary, built with `go test -c` 35 | *.test 36 | 37 | # Output of the go coverage tool, specifically when used with LiteIDE 38 | *.out 39 | coverage.txt 40 | 41 | # Dependency directories (remove the comment below to include it) 42 | # vendor/ 43 | 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # GoRelease Build Configuration 2 | # Refer https://goreleaser.com/customization/templates template variables. 3 | # Refer https://goreleaser.com/customization/ for the fields. 4 | project_name: fsoc 5 | dist: builds 6 | builds: 7 | - id: fsoc-binaries 8 | goos: 9 | - darwin 10 | - linux 11 | - windows 12 | goarch: 13 | - amd64 14 | - arm64 15 | ignore: 16 | - goos: windows 17 | goarch: arm64 18 | binary: fsoc-{{.Os}}-{{.Arch}} 19 | no_unique_dist_dir: true 20 | ldflags: 21 | - -s 22 | - -w 23 | - -X {{ .Env.VERSION_PKG_PATH }}.defVersion={{ .Version }} 24 | - -X {{ .Env.VERSION_PKG_PATH }}.defGitHash={{ .Env.GIT_HASH }} 25 | - -X {{ .Env.VERSION_PKG_PATH }}.defGitBranch={{ .Env.GIT_BRANCH }} 26 | - -X {{ .Env.VERSION_PKG_PATH }}.defBuildHost={{ .Env.BUILD_HOST }} 27 | - -X {{ .Env.VERSION_PKG_PATH }}.defIsDev={{ .Env.BUILD_IS_DEV }} 28 | - -X {{ .Env.VERSION_PKG_PATH }}.defGitDirty={{ .Env.GIT_DIRTY }} 29 | - -X {{ .Env.VERSION_PKG_PATH }}.defGitTimestamp={{ .Env.GIT_TIMESTAMP }} 30 | - -X {{ .Env.VERSION_PKG_PATH }}.defBuildTimestamp={{ .Env.BUILD_TIMESTAMP }} 31 | flags: 32 | - -trimpath 33 | env: 34 | - CGO_ENABLED=0 35 | 36 | archives: 37 | - id: fsoc-binary-archives 38 | name_template: 'fsoc-{{ .Os }}-{{ .Arch }}' 39 | format: binary 40 | builds: 41 | - fsoc-binaries 42 | - id: fsoc-archives 43 | name_template: 'fsoc-{{ .Os }}-{{ .Arch }}' 44 | format: tar.gz 45 | builds: 46 | - fsoc-binaries 47 | format_overrides: 48 | - goos: windows 49 | format: zip 50 | 51 | brews: 52 | - name: fsoc 53 | ids: 54 | - fsoc-archives 55 | homepage: https://github.com/cisco-open/fsoc 56 | description: "Cisco Observability Platform Developer's Control Tool" 57 | license: "Apache-2.0" 58 | commit_author: 59 | name: cisco-service 60 | email: 111539563+cisco-service@users.noreply.github.com 61 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 62 | folder: Formula 63 | repository: 64 | owner: cisco-open 65 | name: homebrew-tap 66 | branch: "{{ .ProjectName }}/{{ .Tag }}" 67 | token: "{{ .Env.TAP_GITHUB_TOKEN }}" 68 | pull_request: 69 | enabled: true 70 | base: 71 | owner: cisco-open 72 | name: homebrew-tap 73 | branch: main 74 | install: | 75 | Dir.glob("fsoc-*-*") do |f| 76 | bin.install f => "fsoc" 77 | end 78 | test: | 79 | system "#{bin}/fsoc", "version" 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to fsoc 2 | 3 | We are working to set up the contribution guidelines and policies for this project. 4 | 5 | While at this time we are not accepting contributions, if there is a bugfix or a suggestion or you just need help, please open an issue in this repository. 6 | 7 | The [developer guide](developer_guide/README.md) covers fsoc's design and how to extend it. -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/config" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(config.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | // Package config provides access to fsoc configuration, both to obtain the current 16 | // configuration and to incrementally or fully modify the configuration. 17 | // The fsoc configuration has two dimension: a config file and a context within the config file. 18 | // Each config file contains one or more contexts plus a setting indicating which of them is the current one. 19 | package config 20 | 21 | import ( 22 | "github.com/spf13/cobra" 23 | 24 | cfg "github.com/cisco-open/fsoc/config" 25 | ) 26 | 27 | // Package registration function for the config root command 28 | func NewSubCmd() *cobra.Command { 29 | // cmd represents the config sub command root 30 | var cmd = &cobra.Command{ 31 | Use: "config SUBCOMMAND [options]", 32 | Short: "Configure fsoc", 33 | Long: `View and modify fsoc config files and contexts`, 34 | Example: ` fsoc config list 35 | fsoc config set auth=oauth url=https://mytenant.observe.appdynamics.com 36 | fsoc config set auth=service-principal secret-file=my-svc-principal.json --profile ci 37 | fsoc config get -o yaml 38 | fsoc config use ci 39 | fsoc config delete ci`, 40 | TraverseChildren: true, 41 | } 42 | 43 | cmd.AddCommand(newCmdConfigCreate()) 44 | cmd.AddCommand(newCmdConfigGet()) 45 | cmd.AddCommand(newCmdConfigSet()) 46 | cmd.AddCommand(newCmdConfigUse()) 47 | cmd.AddCommand(newCmdConfigList()) 48 | cmd.AddCommand(newCmdConfigDelete()) 49 | cmd.AddCommand(newCmdConfigShowFields()) 50 | 51 | return cmd 52 | } 53 | 54 | // GetAuthMethodsStringList returns the list of authentication methods as strings (for join, etc.) 55 | func GetAuthMethodsStringList() []string { 56 | return []string{ 57 | cfg.AuthMethodNone, 58 | cfg.AuthMethodOAuth, 59 | cfg.AuthMethodServicePrincipal, 60 | cfg.AuthMethodAgentPrincipal, 61 | cfg.AuthMethodJWT, 62 | cfg.AuthMethodLocal, 63 | } 64 | } 65 | 66 | func validArgsAutocomplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 67 | return cfg.ListContexts(toComplete), cobra.ShellCompDirectiveDefault 68 | } 69 | -------------------------------------------------------------------------------- /cmd/config/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | cfg "github.com/cisco-open/fsoc/config" 24 | "github.com/cisco-open/fsoc/output" 25 | ) 26 | 27 | func newCmdConfigDelete() *cobra.Command { 28 | 29 | var cmd = &cobra.Command{ 30 | Use: "delete CONTEXT_NAME", 31 | Short: "Delete a context from the fsoc config file", 32 | Long: `Delete a context from the fsoc config file`, 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: validArgsAutocomplete, 35 | Run: configDeleteContext, 36 | } 37 | 38 | return cmd 39 | } 40 | 41 | func configDeleteContext(cmd *cobra.Command, args []string) { 42 | profile := args[0] 43 | if err := cfg.DeleteContext(profile); err != nil { 44 | log.Fatalf("%v", err) 45 | } 46 | output.PrintCmdStatus(cmd, fmt.Sprintf("Deleted profile %q\n", profile)) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/config/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "slices" 19 | 20 | "github.com/apex/log" 21 | "github.com/mitchellh/mapstructure" 22 | "github.com/spf13/cobra" 23 | 24 | cfg "github.com/cisco-open/fsoc/config" 25 | "github.com/cisco-open/fsoc/output" 26 | ) 27 | 28 | const tableFieldSpec = `` + 29 | `use:.use, ` + 30 | `name:.name, ` + 31 | `auth_method:(if .auth_method == "" then "-" else .auth_method end), ` + 32 | `url:(if .url == "" then "-" else .url end), ` + 33 | `env_type:(if .env_type == null then "" else .env_type end)` 34 | 35 | func newCmdConfigList() *cobra.Command { 36 | 37 | var cmd = &cobra.Command{ 38 | Use: "list", 39 | Short: "Displays all contexts in an fsoc config file", 40 | Long: `Displays all contexts in an fsoc config file`, 41 | Args: cobra.NoArgs, 42 | RunE: configListContexts, 43 | Annotations: map[string]string{ 44 | output.TableFieldsAnnotation: tableFieldSpec, 45 | }, 46 | } 47 | 48 | return cmd 49 | } 50 | 51 | func configListContexts(cmd *cobra.Command, args []string) error { 52 | // determine if detailed human format which requires special handling 53 | outputFormat, err := cmd.Flags().GetString("output") 54 | detailView := err == nil && outputFormat == "detail" 55 | 56 | // get names of all profiles (sorted) and which ones are active/default 57 | profiles := cfg.ListAllContexts() 58 | activeProfile := cfg.GetCurrentProfileName() // whether it is the default or not 59 | defaultProfile := cfg.GetDefaultContextName() // the "current" set in the config file 60 | slices.Sort(profiles) 61 | 62 | contextList := []map[string]any{} 63 | for _, name := range profiles { 64 | context, err := cfg.GetContext(name) 65 | if err != nil { 66 | log.Warnf("(bug?) can't find listed context %q: %v; skipping", name, err) 67 | continue 68 | } 69 | 70 | // determine how the profile is used (active, default or neither) 71 | use := "" 72 | if name == activeProfile { 73 | use = "Current" 74 | } else if name == defaultProfile { 75 | use = "Default" 76 | } 77 | 78 | // display detailed view using the "get" command displayer 79 | if detailView { 80 | outputContext(cmd, context, use) // adds extra line 81 | continue 82 | } 83 | 84 | var cMap map[string]any 85 | err = mapstructure.Decode(context, &cMap) 86 | if err != nil { 87 | log.Warnf("(bug?) failed to marshal context %q to mapstructure: %v; skipping", name, err) 88 | continue 89 | } 90 | 91 | // add the use indicator and append the entry 92 | cMap["use"] = use 93 | contextList = append(contextList, cMap) 94 | } 95 | 96 | // output data (for all output formats except "detail" which is already displayed) 97 | if !detailView { 98 | output.PrintCmdOutput(cmd, struct { 99 | Items []map[string]any `json:"items"` 100 | Total int `json:"total"` 101 | }{ 102 | contextList, 103 | len(contextList), 104 | }) 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /cmd/config/set_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateUrlWithInvalidURL(t *testing.T) { 10 | url, err := validateUrl("/web") 11 | assert.Equal(t, "", url) 12 | assert.NotNil(t, err) 13 | assert.Regexp(t, "no host is provided in the url.*", err) 14 | 15 | url, err = validateUrl("h://web") 16 | assert.Equal(t, "", url) 17 | assert.NotNil(t, err) 18 | assert.Regexp(t, "the provided scheme.*is not recognized", err) 19 | } 20 | 21 | func TestValidateUrlWithDataNeedClean(t *testing.T) { 22 | url, err := validateUrl("mytenant.saas.observe.com") 23 | assert.Nil(t, err) 24 | assert.Equal(t, url, "https://mytenant.saas.observe.com") 25 | 26 | url, err = validateUrl("mytenant.saas.observe.com/index/a?b=1") 27 | assert.Nil(t, err) 28 | assert.Equal(t, url, "https://mytenant.saas.observe.com/index/a?b=1") 29 | } 30 | 31 | func TestValidateUrlWithValidData(t *testing.T) { 32 | url, err := validateUrl("http://mytenant.saas.observe.com") 33 | assert.Nil(t, err) 34 | assert.Equal(t, url, "http://mytenant.saas.observe.com") 35 | } 36 | -------------------------------------------------------------------------------- /cmd/config/use.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | cfg "github.com/cisco-open/fsoc/config" 24 | "github.com/cisco-open/fsoc/output" 25 | ) 26 | 27 | func newCmdConfigUse() *cobra.Command { 28 | 29 | var cmd = &cobra.Command{ 30 | Use: "use CONTEXT_NAME", 31 | Short: "Set the current context in an fsoc config file", 32 | Long: `Set the current context in an fsoc config file`, 33 | Args: cobra.MaximumNArgs(1), 34 | ValidArgsFunction: validArgsAutocomplete, 35 | Run: configUseContext, 36 | } 37 | 38 | return cmd 39 | } 40 | 41 | func configUseContext(cmd *cobra.Command, args []string) { 42 | var newContext string 43 | 44 | // determine which profile to use (supporting --profile for backward compatibility) 45 | if cmd.Flags().Changed("profile") { 46 | newContext, _ = cmd.Flags().GetString("profile") 47 | if len(args) > 0 { 48 | _ = cmd.Usage() 49 | log.Fatalf("The context can be specified either as an argument or as a flag but not as both") 50 | } else { 51 | log.Warn("using the --profile flag for this command is deprecated; please, use just the profile name as an argument") 52 | } 53 | } 54 | if len(args) > 0 { 55 | newContext = args[0] 56 | } 57 | if newContext == "" { // also handles empty string argument 58 | _ = cmd.Usage() 59 | log.Fatalf("Missing the context name argument") 60 | } 61 | 62 | if err := cfg.SetDefaultContextName(newContext); err != nil { 63 | log.Fatalf("%v", err) 64 | } 65 | 66 | output.PrintCmdStatus(cmd, fmt.Sprintf("Switched to context %q\n", newContext)) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/gendocs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/gendocs" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(gendocs.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/iamrole.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import "github.com/cisco-open/fsoc/cmd/iamrole" 18 | 19 | func init() { 20 | registerSubsystem(iamrole.NewSubCmd()) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/iamrole/iamrole.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrole 16 | 17 | import ( 18 | "net/url" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | // iamRoleCmd represents the role command group 25 | var iamRoleCmd = &cobra.Command{ 26 | Use: "iam-role", 27 | Short: "Manage IAM roles", 28 | Long: `Manage roles, as part of identity and access management (IAM) 29 | 30 | Roles are usually prefixed with the domain, e.g., "iam:observer". Standard roles include "iam:observer", 31 | "iam:tenantAdmin" and "iam:configManager". Solutions often define their own roles that can be bound to principals 32 | in order to access solution's functionality. 33 | 34 | Commands from this group require a principal with tenant administrator access.`, 35 | Aliases: []string{"iam-roles", "role", "roles"}, 36 | Example: ` 37 | fsoc iam-roles list 38 | fsoc roles list 39 | fsoc role principals 40 | fsoc role permissions 41 | `, 42 | //TODO: add 'create', 'update', 'delete' 43 | TraverseChildren: true, 44 | } 45 | 46 | // Package registration function for the iam-role-binding command root 47 | func NewSubCmd() *cobra.Command { 48 | cmd := iamRoleCmd 49 | 50 | cmd.AddCommand(newCmdRoleList()) 51 | cmd.AddCommand(newCmdRolePrincipals()) 52 | cmd.AddCommand(newCmdRolePermissions()) 53 | 54 | return cmd 55 | } 56 | 57 | func getIamRoleUrl(role string, subObj string) string { 58 | urlBase := "/iam/policy-admin/v1beta2/roles" // [/[]] 59 | 60 | elements := []string{} 61 | if role != "" { 62 | elements = append(elements, role) 63 | if subObj != "" { 64 | elements = append(elements, subObj) 65 | } 66 | fullUrl, err := url.JoinPath(urlBase, elements...) 67 | if err != nil { 68 | log.Fatalf("(likely bug) failred to append %v to %q: %w", elements, urlBase, err) 69 | } 70 | return fullUrl 71 | } 72 | return urlBase 73 | } 74 | -------------------------------------------------------------------------------- /cmd/iamrole/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrole 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | 20 | "github.com/cisco-open/fsoc/cmdkit" 21 | "github.com/cisco-open/fsoc/output" 22 | ) 23 | 24 | // iamRoleListCmd defines the list roles command 25 | var iamRoleListCmd = &cobra.Command{ 26 | Use: "list", 27 | Short: "List roles", 28 | Long: `List available roles. 29 | 30 | Roles can be assigned to principals using the "iam-role-binding" commands. 31 | 32 | This command requires a principal with tenant administrator access. 33 | 34 | Detail and json/yaml output include role permissions; the table view contains only role names.`, 35 | Example: ` 36 | fsoc iam-role list 37 | fsoc iam-roles list 38 | fsoc roles list -o json 39 | fsoc role list -o detail`, 40 | Args: cobra.NoArgs, 41 | Run: listRoles, 42 | Annotations: map[string]string{ 43 | output.TableFieldsAnnotation: "id:.id, name:.data.displayName, description:.data.description", 44 | output.DetailFieldsAnnotation: "id:.id, name:.data.displayName, description:.data.description, permissions:(reduce .data.permissions[].id as $o ([]; . + [$o])), scopes:.data.scopes", 45 | }, 46 | } 47 | 48 | // Package registration function for the iam-role-binding command root 49 | func newCmdRoleList() *cobra.Command { 50 | return iamRoleListCmd 51 | } 52 | 53 | func listRoles(cmd *cobra.Command, args []string) { 54 | cmdkit.FetchAndPrint(cmd, getIamRoleUrl("", ""), &cmdkit.FetchAndPrintOptions{IsCollection: true}) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/iamrole/permissions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrole 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | 20 | "github.com/cisco-open/fsoc/cmdkit" 21 | "github.com/cisco-open/fsoc/output" 22 | ) 23 | 24 | // iamRolePermissionsCmd represents the role permissions command 25 | var iamRolePermissionsCmd = &cobra.Command{ 26 | Use: "permissions ", 27 | Short: "List permissions that a role provides", 28 | Long: `List all permissions that a given role grants to principals bound to do role. 29 | 30 | This command requires a principal with tenant administrator access. 31 | 32 | The json/yaml output include the actions and resources for each permissions; the table and detail views include the permission names.`, 33 | Example: ` 34 | fsoc iam-role permissions iam:observer 35 | fsoc role permissions spacefleet:commandingOfficer 36 | fsoc role permissions iam:agent -o json`, 37 | Args: cobra.ExactArgs(1), 38 | Run: listPermissions, 39 | Annotations: map[string]string{ 40 | output.TableFieldsAnnotation: "id:.id, name:.data.displayName, description:.data.description", 41 | }, 42 | } 43 | 44 | // Package registration function for the iam-role-binding command root 45 | func newCmdRolePermissions() *cobra.Command { 46 | return iamRolePermissionsCmd 47 | } 48 | 49 | func listPermissions(cmd *cobra.Command, args []string) { 50 | cmdkit.FetchAndPrint(cmd, getIamRoleUrl(args[0], "permissions"), &cmdkit.FetchAndPrintOptions{IsCollection: true}) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/iamrole/principals.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrole 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/output" 22 | "github.com/cisco-open/fsoc/platform/api" 23 | ) 24 | 25 | // iamRolePrincipalsCmd represents the role principals list command 26 | var iamRolePrincipalsCmd = &cobra.Command{ 27 | Use: "principals ", 28 | Short: "List principals bound to a role", 29 | Long: `List all principals that are bound to a particular role. 30 | 31 | Role bindings can be managed with the "iam-role-binding" commands. 32 | 33 | This command requires a principal with tenant administrator access.`, 34 | Example: ` 35 | fsoc iam-role principals spacefleet:commandingOfficer 36 | fsoc role principals iam:agent -o json`, 37 | Args: cobra.ExactArgs(1), 38 | Run: listPrincipals, 39 | Annotations: map[string]string{ 40 | output.TableFieldsAnnotation: "id:.id, type:.type", 41 | }, 42 | } 43 | 44 | // Package registration function for the iam-role-binding command root 45 | func newCmdRolePrincipals() *cobra.Command { 46 | return iamRolePrincipalsCmd 47 | } 48 | 49 | type principalEntry struct { 50 | ID string `json:"id"` 51 | Type string `json:"type"` 52 | } 53 | 54 | type principalsResponse struct { 55 | Total uint `json:"total"` 56 | Principals []principalEntry `json:"principals"` 57 | } 58 | 59 | type principalsCollection struct { 60 | Total uint `json:"total"` 61 | Items []principalEntry `json:"items"` 62 | } 63 | 64 | func listPrincipals(cmd *cobra.Command, args []string) { 65 | // note: the API is not compliant with collections/pagination, so collect as a single request 66 | var out principalsResponse 67 | err := api.JSONGet(getIamRoleUrl(args[0], "principals"), &out, nil) 68 | if err != nil { 69 | log.Fatal(err.Error()) 70 | } 71 | 72 | // reflow into a collection structure 73 | data := principalsCollection{Total: out.Total, Items: out.Principals} 74 | output.PrintCmdOutput(cmd, data) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/iamrolebinding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import "github.com/cisco-open/fsoc/cmd/iamrolebinding" 18 | 19 | func init() { 20 | registerSubsystem(iamrolebinding.NewSubCmd()) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/iamrolebinding/add.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrolebinding 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/output" 22 | ) 23 | 24 | // iamRbAddCmd represents the role binding add command 25 | var iamRbAddCmd = &cobra.Command{ 26 | Use: "add []+", 27 | Short: "Add roles to a principal", 28 | Long: `Add one or more roles to a principal. 29 | 30 | Use the "list" command to see roles bound to the principal; use the "iam-role list" command to see available roles. 31 | 32 | This command requires a principal with tenant administrator access.`, 33 | Example: ` 34 | fsoc rb add john@example.com iam:observer spacefleet:crewMember 35 | fsoc rb add srv_1ZGdlbcm8NajPxY4o43SNv optimize:optimizationManager`, 36 | Args: cobra.MinimumNArgs(2), 37 | Run: addRoles, 38 | } 39 | 40 | // Package registration function for the iam-role-binding command root 41 | func newCmdRbAdd() *cobra.Command { 42 | return iamRbAddCmd 43 | } 44 | 45 | func addRoles(cmd *cobra.Command, args []string) { 46 | if err := patchRoles(args[0], args[1:], true); err != nil { 47 | log.Fatal(err.Error()) 48 | } 49 | 50 | output.PrintCmdStatus(cmd, "Roles added successfully.\n") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/iamrolebinding/iamrolebinding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrolebinding 16 | 17 | import ( 18 | "encoding/json" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/cisco-open/fsoc/platform/api" 24 | ) 25 | 26 | // iamRbCmd represents the role binding command group 27 | var iamRbCmd = &cobra.Command{ 28 | Use: "iam-role-binding", 29 | Short: "Manage IAM role bindings", 30 | Long: `Manage role bindings for a principal, as part of identity and access management (IAM) 31 | 32 | Principals can be user principals, service principals or agent principals. For user principals, use the email 33 | address of the user; for service and agent principal, use the principal's ID (client ID). 34 | 35 | Roles are usually prefixed with the domain, e.g., "iam:observer". Standard roles include "iam:observer", 36 | "iam:tenantAdmin" and "iam:configManager". Solutions often define their own roles that can be bound to principals 37 | in order to access solution's functionality. 38 | 39 | To see and manage roles, use the "iam-role" commands. 40 | 41 | Commands from this group require a principal with tenant administrator access.`, 42 | Aliases: []string{"iam-role-bindings", "role-binding", "role-bindings", "rb"}, 43 | Example: ` 44 | fsoc iam-role-bindings list john@example.com 45 | fsoc rb list john@example.com 46 | fsoc rb add jill@example.com iam:configManager optimize:optimizationManager 47 | fsoc rb remove jay@example.com iam:tenantAdmin 48 | `, 49 | TraverseChildren: true, 50 | } 51 | 52 | // Package registration function for the iam-role-binding command root 53 | func NewSubCmd() *cobra.Command { 54 | cmd := iamRbCmd 55 | 56 | cmd.AddCommand(newCmdRbList()) 57 | cmd.AddCommand(newCmdRbAdd()) 58 | cmd.AddCommand(newCmdRbRemove()) 59 | 60 | return cmd 61 | } 62 | 63 | func getIamRoleBindingsUrl() string { 64 | return "iam/policy-admin/v1beta2/principals/roles" 65 | } 66 | 67 | func patchRoles(principal string, roles []string, is_add bool) error { 68 | // choose value to use for roles in the request 69 | var roleValue any 70 | if is_add { 71 | roleValue = true 72 | } else { 73 | roleValue = nil 74 | } 75 | 76 | // prepare request parameter 77 | requestParams := ManageParameter{Principal: PrincipalParameter{ID: principal}, Roles: map[string]any{}} 78 | for _, role := range roles { 79 | requestParams.Roles[role] = roleValue 80 | } 81 | 82 | // request operation, the API requires the application/json type (bug?) 83 | // JSONPatch converts to JSON with application/json-patch+json; to override this 84 | // we must pre-marshal to JSON here 85 | body, err := json.Marshal(requestParams) // must marshal here in order to be able to supply content-type 86 | if err != nil { 87 | log.Fatalf("(bug) failed to marshal to json: %v", err) 88 | } 89 | options := api.Options{Headers: map[string]string{"Content-Type": "application/json"}} 90 | if err := api.JSONPatch(getIamRoleBindingsUrl(), body, nil, &options); err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /cmd/iamrolebinding/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrolebinding 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/output" 22 | "github.com/cisco-open/fsoc/platform/api" 23 | ) 24 | 25 | // iamRbListCmd represents the role binding list command 26 | var iamRbListCmd = &cobra.Command{ 27 | Use: "list ", 28 | Short: "List role bindings for a principal", 29 | Long: `List roles bound to a given principal. 30 | 31 | Detail and json/yaml output include role permissions; the table view contains only role names. 32 | 33 | To manage roles, as well as see permissions and principals for a given role, use the "iam-role" commands. 34 | 35 | This command requires a principal with tenant administrator access.`, 36 | Example: ` 37 | fsoc iam-role-bindings list john@example.com 38 | fsoc rb list john@example.com -o json 39 | fsoc rb list john@example.com -o detail`, 40 | Args: cobra.ExactArgs(1), 41 | Run: listRoles, 42 | Annotations: map[string]string{ 43 | output.TableFieldsAnnotation: "id:.id, name:.data.displayName, description:.data.description", 44 | output.DetailFieldsAnnotation: "id:.id, name:.data.displayName, description:.data.description, permissions:(reduce .data.permissions[].id as $o ([]; . + [$o])), scopes:.data.scopes", 45 | }, 46 | } 47 | 48 | // Package registration function for the iam-role-binding command root 49 | func newCmdRbList() *cobra.Command { 50 | return iamRbListCmd 51 | } 52 | 53 | func listRoles(cmd *cobra.Command, args []string) { 54 | // get data 55 | var out any 56 | requestParams := PrincipalParameter{ID: args[0]} 57 | if err := api.JSONPost(getIamRoleBindingsUrl(), requestParams, &out, nil); err != nil { 58 | log.Fatal(err.Error()) 59 | } 60 | 61 | // display with formatting 62 | output.PrintCmdOutput(cmd, out) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/iamrolebinding/remove.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrolebinding 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/output" 22 | ) 23 | 24 | // iamRbRemoveCmd represents the role binding remove command 25 | var iamRbRemoveCmd = &cobra.Command{ 26 | Use: "remove []+", 27 | Short: "Remove roles from a principal", 28 | Long: `Remove one or more roles from a principal that has the roles. 29 | 30 | To the see roles bound to a principal, use "list" command; to see permissions for a given role, use the "iam-role permissions" command. 31 | 32 | This command requires a principal with tenant administrator access.`, 33 | Example: ` 34 | fsoc rb remove riker@example.com iam:tenantAdmin spacefleet:commandingOfficer 35 | fsoc rb remove srv_1ZGdlbcm8NajPxY4o43SNv optimize:optimizationManager`, 36 | Args: cobra.MinimumNArgs(2), 37 | Run: removeRoles, 38 | } 39 | 40 | // Package registration function for the iam-role-binding command root 41 | func newCmdRbRemove() *cobra.Command { 42 | return iamRbRemoveCmd 43 | } 44 | 45 | func removeRoles(cmd *cobra.Command, args []string) { 46 | if err := patchRoles(args[0], args[1:], false); err != nil { 47 | log.Fatal(err.Error()) 48 | } 49 | 50 | output.PrintCmdStatus(cmd, "Roles removed successfully.\n") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/iamrolebinding/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package iamrolebinding 16 | 17 | type PrincipalParameter struct { 18 | ID string `json:"id"` 19 | } 20 | 21 | type ManageParameter struct { 22 | Principal PrincipalParameter `json:"principal"` 23 | Roles map[string]any `json:"roles,omitempty"` // values must be `true` to add, `null` to remove 24 | } 25 | -------------------------------------------------------------------------------- /cmd/knowledge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/knowledge" 19 | ) 20 | 21 | func init() { 22 | registerSubSystemWithConfig(knowledge.NewSubCmd(), &knowledge.GlobalConfig) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/knowledge/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package knowledge 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/cisco-open/fsoc/output" 24 | "github.com/cisco-open/fsoc/platform/api" 25 | ) 26 | 27 | var objStoreDeleteCmd = &cobra.Command{ 28 | Use: "delete", 29 | Short: "Delete an existing knowledge object", 30 | Long: `This command allows an existent knowledge object to be deleted. 31 | 32 | Usage: 33 | fsoc knowledge delete \ 34 | --type= \ 35 | --object-id= \ 36 | --layer-type=[SOLUTION|ACCOUNT|GLOBALUSER|TENANT|LOCALUSER] \ 37 | --layer-id= 38 | `, 39 | 40 | Args: cobra.ExactArgs(0), 41 | Run: deleteObject, 42 | TraverseChildren: true, 43 | } 44 | 45 | func getDeleteObjectCmd() *cobra.Command { 46 | objStoreDeleteCmd.Flags(). 47 | String("type", "", "The fully qualified type name of the knowledge object to delete. The fully qualified type name follows the format solutionName:typeName (e.g. extensibility:solution)") 48 | _ = objStoreDeleteCmd.MarkPersistentFlagRequired("type") 49 | _ = objStoreDeleteCmd.RegisterFlagCompletionFunc("type", typeCompletionFunc) 50 | 51 | objStoreDeleteCmd.Flags(). 52 | String("object-id", "", "The id of the knowledge object to delete") 53 | _ = objStoreDeleteCmd.MarkPersistentFlagRequired("type") 54 | _ = objStoreDeleteCmd.RegisterFlagCompletionFunc("object-id", objectCompletionFunc) 55 | 56 | objStoreDeleteCmd.Flags(). 57 | String("layer-type", "", "The layer-type of knowledge object to delete") 58 | _ = objStoreDeleteCmd.MarkPersistentFlagRequired("layer-type") 59 | _ = objStoreDeleteCmd.RegisterFlagCompletionFunc("layer-type", layerTypeCompletionFunc) 60 | 61 | objStoreDeleteCmd.Flags(). 62 | String("layer-id", "", "The layer-id of the knowledge object to delete. Optional for TENANT and SOLUTION layers ") 63 | 64 | return objStoreDeleteCmd 65 | 66 | } 67 | 68 | func deleteObject(cmd *cobra.Command, args []string) { 69 | var err error 70 | 71 | objType, _ := cmd.Flags().GetString("type") 72 | 73 | layerType, _ := cmd.Flags().GetString("layer-type") 74 | layerID := getCorrectLayerID(layerType, objType) 75 | 76 | if layerID == "" { 77 | if !cmd.Flags().Changed("layer-id") { 78 | log.Fatalf("Unable to set layer-id flag from given context. Please specify a unique layer-id value with the --layer-id flag") 79 | } 80 | layerID, err = cmd.Flags().GetString("layer-id") 81 | if err != nil { 82 | log.Fatalf("error trying to get %q flag value: %w", "layer-id", err) 83 | } 84 | } 85 | 86 | headers := map[string]string{ 87 | "layer-type": layerType, 88 | "layer-id": layerID, 89 | } 90 | 91 | var res any 92 | objId, _ := cmd.Flags().GetString("object-id") 93 | urlStrf := getObjStoreObjectUrl() + "/%s/%s" 94 | objectUrl := fmt.Sprintf(urlStrf, objType, objId) 95 | 96 | output.PrintCmdStatus(cmd, (fmt.Sprintf("Deleting knowledge object %q of type %q\n", objId, objType))) 97 | err = api.JSONDelete(objectUrl, &res, &api.Options{Headers: headers}) 98 | if err != nil { 99 | log.Fatalf("Failed to delete knowledge object: %v", err) 100 | } 101 | output.PrintCmdStatus(cmd, "knowledge object was successfully deleted.\n") 102 | } 103 | -------------------------------------------------------------------------------- /cmd/knowledge/layerid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package knowledge 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/cisco-open/fsoc/config" 21 | ) 22 | 23 | func getCorrectLayerID(layerType string, fqtn string) string { 24 | cfg := config.GetCurrentContext() 25 | var layerID string 26 | 27 | if layerType == "TENANT" { 28 | layerID = cfg.Tenant 29 | } else if layerType == "SOLUTION" { 30 | layerID = strings.Split(fqtn, ":")[0] 31 | } else if layerType == "LOCALUSER" || layerType == "GLOBALUSER" { 32 | layerID = cfg.User 33 | } else { 34 | layerID = "" 35 | } 36 | 37 | return layerID 38 | } 39 | -------------------------------------------------------------------------------- /cmd/knowledge/types.go: -------------------------------------------------------------------------------- 1 | package knowledge 2 | 3 | type KSType struct { 4 | Name string `json:"name"` 5 | Solution string `json:"solution"` 6 | } 7 | 8 | type KSObject struct { 9 | ID string `json:"id"` 10 | Data map[string]interface{} `json:"data"` 11 | } 12 | -------------------------------------------------------------------------------- /cmd/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/login" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(login.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/login/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package login 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/output" 22 | "github.com/cisco-open/fsoc/platform/api" 23 | ) 24 | 25 | // loginCmd represents the login command 26 | var loginCmd = &cobra.Command{ 27 | Use: "login", 28 | Short: "Login to profile, obtain and save JWT token for future commands", 29 | Long: `This command logs in the principal specified in the profile, obtaining a temporary JWT token 30 | that will be automatically used by other commands. 31 | 32 | Usage: 33 | fsoc login`, 34 | Run: login, 35 | TraverseChildren: true, 36 | } 37 | 38 | func init() { 39 | // Here you will define your flags and configuration settings. 40 | 41 | // Cobra supports Persistent Flags which will work for this command 42 | // and all subcommands, e.g.: 43 | // loginCmd.PersistentFlags().String("foo", "", "A help for foo") 44 | 45 | // Cobra supports local flags which will only run when this command 46 | // is called directly, e.g.: 47 | // loginCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 48 | } 49 | 50 | func NewSubCmd() *cobra.Command { 51 | return loginCmd 52 | } 53 | 54 | func login(cmd *cobra.Command, args []string) { 55 | if err := api.Login(); err != nil { 56 | log.Fatalf("Login failed: %v", err) 57 | } 58 | output.PrintCmdStatus(cmd, "Login completed successfully.\n") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import "github.com/cisco-open/fsoc/cmd/logs" 18 | 19 | func init() { 20 | registerSubsystem(logs.NewSubCmd()) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/logs/levels.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package logs 16 | 17 | import ( 18 | "math" 19 | "strings" 20 | ) 21 | 22 | type level struct { 23 | name string 24 | value int 25 | } 26 | 27 | var ( 28 | fatalLevel = level{"FATAL", 100} 29 | errorLevel = level{"ERROR", 200} 30 | warnLevel = level{"WARN", 300} 31 | infoLevel = level{"INFO", 400} 32 | debugLevel = level{"DEBUG", 500} 33 | traceLevel = level{"TRACE", 600} 34 | unknownLevel = level{"UNKNOWN", math.MaxInt32} 35 | allLevels = []level{fatalLevel, errorLevel, warnLevel, infoLevel, debugLevel, traceLevel, unknownLevel} 36 | ) 37 | 38 | func validLevel(level string) bool { 39 | return findLevel(level) != nil 40 | } 41 | 42 | func findLevel(level string) *level { 43 | lowerCaseLevel := strings.ToUpper(level) 44 | for _, l := range allLevels { 45 | if l.name == lowerCaseLevel { 46 | return &l 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func findLowerOrEqualLevels(level string) []string { 53 | foundLevel := findLevel(level) 54 | if foundLevel == nil { 55 | return nil 56 | } 57 | var lowerOrEqualLevels []string 58 | for _, l := range allLevels { 59 | if l.value <= foundLevel.value { 60 | lowerOrEqualLevels = append(lowerOrEqualLevels, l.name) 61 | } 62 | } 63 | return lowerOrEqualLevels 64 | } 65 | 66 | func allLevelsNames() []string { 67 | var levelNames []string 68 | for _, l := range allLevels { 69 | levelNames = append(levelNames, l.name) 70 | } 71 | return levelNames 72 | } 73 | -------------------------------------------------------------------------------- /cmd/logs/query_template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package logs 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "text/template" 21 | ) 22 | 23 | // template of the query sent to UQL to fetch logs 24 | const queryTemplate = `fetch events(logs:generic_record) 25 | {{if .HasPredicates}}[{{and .Predicates}}]{{end}} 26 | { timestamp, raw, attributes(severity), entityId, spanId, traceId } 27 | {{if .FromClause}} from {{.FromClause}} {{end}} 28 | order events.{{.Order}}() 29 | limits events.count({{.Count}}) 30 | since {{.Since}} 31 | until now()` 32 | 33 | func init() { 34 | uqlQueryTemplate = template.Must(template.New("fetch-logs").Funcs(template.FuncMap{ 35 | "and": func(values []string) string { 36 | return strings.Join(values, " && ") 37 | }, 38 | }).Parse(queryTemplate)) 39 | } 40 | 41 | var uqlQueryTemplate *template.Template 42 | 43 | // templateVariables used to fill the queryTemplate 44 | type templateVariables struct { 45 | Count int 46 | FromClause string 47 | RawFilter []string 48 | Severities []string 49 | Order string 50 | Since string 51 | } 52 | 53 | func (tv *templateVariables) HasPredicates() bool { 54 | return len(tv.RawFilter) > 0 || len(tv.Severities) > 0 55 | } 56 | 57 | func (tv *templateVariables) Predicates() []string { 58 | var predicates []string 59 | for _, rawFilter := range tv.RawFilter { 60 | predicates = append(predicates, fmt.Sprintf("raw ~ '%s'", rawFilter)) 61 | } 62 | if len(tv.Severities) > 0 { 63 | predicates = append(predicates, fmt.Sprintf("attributes(severity) in [%s]", strings.Join(tv.Severities, ", "))) 64 | } 65 | return predicates 66 | } 67 | -------------------------------------------------------------------------------- /cmd/logs/row_format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package logs 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "text/template" 21 | "time" 22 | ) 23 | 24 | type row struct { 25 | Message any 26 | Severity any 27 | Timestamp any 28 | EntityId any 29 | SpanId any 30 | TraceId any 31 | } 32 | 33 | var ( 34 | reset = "\033[0m" 35 | red = "\033[31m" 36 | green = "\033[32m" 37 | yellow = "\033[33m" 38 | blue = "\033[34m" 39 | purple = "\033[35m" 40 | cyan = "\033[36m" 41 | gray = "\033[37m" 42 | white = "\033[97m" 43 | ) 44 | 45 | type rowFormatter func(*row) (string, error) 46 | 47 | type printer interface { 48 | Println(v ...any) 49 | Printf(format string, i ...any) 50 | } 51 | 52 | func printRow(rowValues []any, formatter rowFormatter, p printer) { 53 | row := &row{ 54 | Timestamp: (rowValues[0]).(time.Time).Format(time.RFC3339), 55 | Message: rowValues[1], 56 | Severity: rowValues[2], 57 | EntityId: rowValues[3], 58 | SpanId: rowValues[4], 59 | TraceId: rowValues[5], 60 | } 61 | formattedRow, err := formatter(row) 62 | if err != nil { 63 | p.Printf("cannot format: %s\n", row) 64 | } 65 | p.Println(formattedRow) 66 | } 67 | 68 | func createRowFormatter(rowFormat string) (rowFormatter, error) { 69 | messageTemplate, err := template.New("row-format").Funcs(template.FuncMap{ 70 | "red": color(red), 71 | "green": color(green), 72 | "yellow": color(yellow), 73 | "blue": color(blue), 74 | "purple": color(purple), 75 | "cyan": color(cyan), 76 | "gray": color(gray), 77 | "grey": color(gray), 78 | "white": color(white), 79 | }).Parse(rowFormat) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return func(r *row) (string, error) { 85 | return renderTemplate(messageTemplate, r) 86 | }, nil 87 | } 88 | 89 | func color(color string) func(value any) string { 90 | return func(value any) string { 91 | return color + fmt.Sprintf("%s", value) + reset 92 | } 93 | } 94 | 95 | func renderTemplate(template *template.Template, vars any) (string, error) { 96 | var rendered bytes.Buffer 97 | err := template.Execute(&rendered, vars) 98 | if err != nil { 99 | return "", err 100 | } 101 | return rendered.String(), nil 102 | } 103 | -------------------------------------------------------------------------------- /cmd/melt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/melt" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(melt.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/melt/melt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package melt 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // meltCmd represents the login command 22 | var meltCmd = &cobra.Command{ 23 | Use: "melt", 24 | Short: "Generates fsoc telemetry data models and sends OTLP payloads to the platform ingestion services", 25 | Long: "This command generate fsoc telemetry data models and sends the data to the platform ingestion services. \nIt helps developers to generate mock telemetry data to test their solution's domain models.", 26 | Args: cobra.ExactArgs(0), 27 | TraverseChildren: true, 28 | } 29 | 30 | func NewSubCmd() *cobra.Command { 31 | return meltCmd 32 | } 33 | -------------------------------------------------------------------------------- /cmd/melt/meltini.go: -------------------------------------------------------------------------------- 1 | package melt 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var meltMelitiniCmd = &cobra.Command{ 14 | Use: "meltini", 15 | Short: "uses meltini APIs to generate metrics from a template", 16 | Long: `Uses meltini APIs to generate OT data from provided stated template file.`, 17 | TraverseChildren: true, 18 | Hidden: true, 19 | Run: meltMeltini, 20 | } 21 | 22 | func init() { 23 | meltMelitiniCmd.Flags().String("template-file", "", "path to the stated template file") 24 | meltMelitiniCmd.Flags().String("meltini-url", "http://localhost:3000/", "meltini REST APIs URL") 25 | _ = meltMelitiniCmd.MarkFlagRequired("template-file") 26 | meltCmd.AddCommand(meltMelitiniCmd) // Assuming meltCmd is defined elsewhere 27 | } 28 | 29 | func meltMeltini(cmd *cobra.Command, args []string) { 30 | templateFile, _ := cmd.Flags().GetString("template-file") 31 | meltiniURL, _ := cmd.Flags().GetString("meltini-url") 32 | 33 | // Read the JSON file 34 | jsonData, err := os.ReadFile(templateFile) 35 | if err != nil { 36 | fmt.Println("Error reading template file:", err) 37 | return 38 | } 39 | 40 | // Send a POST request with the JSON data 41 | response, err := http.Post(meltiniURL, "application/json", bytes.NewBuffer(jsonData)) 42 | if err != nil { 43 | fmt.Println("Error sending request to meltini:", err) 44 | return 45 | } 46 | defer response.Body.Close() 47 | 48 | // Read the response 49 | respData, err := io.ReadAll(response.Body) 50 | if err != nil { 51 | fmt.Println("Error reading response from meltini:", err) 52 | return 53 | } 54 | 55 | // Print the response 56 | fmt.Println("Response from meltini:", string(respData)) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/optimize.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/optimize" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(optimize.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/optimize/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package optimize 16 | 17 | import ( 18 | "fmt" 19 | "net/url" 20 | "reflect" 21 | "strings" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | "github.com/cisco-open/fsoc/config" 26 | ) 27 | 28 | // sliceToMap converts a list of lists (slice [][2]any) to a dictionary for table output jq support 29 | // eg. 30 | // 31 | // [ 32 | // ["k8s.cluster.name", "ignite-test"], 33 | // ["k8s.namespace.name", "kube-system"], 34 | // ["k8s.workload.kind", "Deployment"], 35 | // ["k8s.workload.name", "coredns"] 36 | // ] 37 | // 38 | // to 39 | // 40 | // k8s.cluster.name: ignite-test 41 | // k8s.namespace.name: kube-system 42 | // k8s.workload.kind: Deployment 43 | // k8s.workload.name: coredns 44 | func sliceToMap(slice [][]any) (map[string]any, error) { 45 | results := make(map[string]any) 46 | for index, subslice := range slice { 47 | if len(subslice) < 2 { 48 | return results, fmt.Errorf("subslice (at index %v) too short to construct key value pair: %+v", index, subslice) 49 | } 50 | key, ok := subslice[0].(string) 51 | if !ok { 52 | return results, fmt.Errorf("string type assertion failed on first subslice item (at index %v): %+v", index, subslice) 53 | } 54 | results[key] = subslice[1] 55 | } 56 | return results, nil 57 | } 58 | 59 | func setNestedMap(baseMap map[string]interface{}, keys []string, value interface{}) { 60 | if len(keys) == 1 { 61 | baseMap[keys[0]] = value 62 | return 63 | } 64 | 65 | if _, ok := baseMap[keys[0]]; !ok { 66 | baseMap[keys[0]] = make(map[string]interface{}) 67 | } 68 | setNestedMap(baseMap[keys[0]].(map[string]interface{}), keys[1:], value) 69 | } 70 | 71 | func getOrionTenantHeaders() map[string]string { 72 | return map[string]string{ 73 | "layer-type": "TENANT", 74 | "layer-id": config.GetCurrentContext().Tenant, 75 | } 76 | } 77 | 78 | func checkHardBlockers(b *Blockers) bool { 79 | val := reflect.ValueOf(b).Elem() 80 | 81 | for i := 0; i < val.NumField(); i++ { 82 | blockerField := val.Field(i) 83 | if blocker, ok := blockerField.Interface().(*Blocker); ok && blocker != nil && !blocker.Overridable { 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | 90 | func getKnowledgeURL(cmd *cobra.Command, objName string, objectPathPrefix string) string { 91 | solutionName := cmd.Flag("solution-name").Value.String() 92 | objStoreUrl := fmt.Sprintf("knowledge-store/v1/objects/%v:%s", solutionName, objName) 93 | 94 | filterSegments := make([]string, 0, 4) 95 | flags := cmd.Flags() 96 | if flags != nil { 97 | var val string 98 | if val, _ = flags.GetString("optimizer-id"); val != "" { 99 | filterSegments = append(filterSegments, fmt.Sprintf("id eq %q", val)) 100 | } 101 | if val, _ = flags.GetString("cluster"); val != "" { 102 | filterSegments = append(filterSegments, fmt.Sprintf("%s.target.k8sDeployment.clusterName eq %q", objectPathPrefix, val)) 103 | } 104 | if val, _ = flags.GetString("namespace"); val != "" { 105 | filterSegments = append(filterSegments, fmt.Sprintf("%s.target.k8sDeployment.namespaceName eq %q", objectPathPrefix, val)) 106 | } 107 | if val, _ = flags.GetString("workload-name"); val != "" { 108 | filterSegments = append(filterSegments, fmt.Sprintf("%s.target.k8sDeployment.workloadName eq %q", objectPathPrefix, val)) 109 | } 110 | } 111 | 112 | filterCriteria := strings.Join(filterSegments, " and ") 113 | if filterCriteria != "" { 114 | query := fmt.Sprintf("filter=%s", url.QueryEscape(filterCriteria)) 115 | objStoreUrl = objStoreUrl + "?" + query 116 | } 117 | 118 | return objStoreUrl 119 | } 120 | -------------------------------------------------------------------------------- /cmd/optimize/completion.go: -------------------------------------------------------------------------------- 1 | package optimize 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/apex/log" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/cisco-open/fsoc/cmd/uql" 11 | "github.com/cisco-open/fsoc/config" 12 | "github.com/cisco-open/fsoc/platform/api" 13 | ) 14 | 15 | func registerReportCompletion(command *cobra.Command, flag profilerReportFlag) { 16 | err := command.RegisterFlagCompletionFunc( 17 | flag.String(), 18 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 19 | config.SetActiveProfile(cmd, args, false) 20 | return completeFlagFromMelt(flag, cmd, args, toComplete) 21 | }) 22 | if err != nil { 23 | log.Warnf("Failed to register completion for flag %s: %v", flag.String(), err) 24 | } 25 | } 26 | 27 | func registerOptimizerCompletion(command *cobra.Command, flag optimizerFlag) { 28 | err := command.RegisterFlagCompletionFunc( 29 | flag.String(), 30 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 31 | config.SetActiveProfile(cmd, args, false) 32 | return completeFlagFromKS(flag, cmd, args, toComplete) 33 | }) 34 | if err != nil { 35 | log.Warnf("Failed to register completion for flag %s: %v", flag.String(), err) 36 | } 37 | } 38 | 39 | func completeFlagFromKS(flag optimizerFlag, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 40 | 41 | objStoreUrl := getKnowledgeURL(cmd, "optimizer", "data") 42 | 43 | headers := getOrionTenantHeaders() 44 | 45 | httpOptions := &api.Options{Headers: headers} 46 | 47 | var result api.CollectionResult[configJsonStoreItem] 48 | err := api.JSONGetCollection[configJsonStoreItem](objStoreUrl, &result, httpOptions) 49 | if err != nil { 50 | return nil, cobra.ShellCompDirectiveNoFileComp 51 | } 52 | 53 | var ids []string 54 | 55 | // Filter completion results by toComplete 56 | for _, s := range result.Items { 57 | val := flag.ValueFromObject(&s.Data) 58 | if strings.HasPrefix(val, toComplete) { 59 | ids = append(ids, val) 60 | } 61 | } 62 | return ids, cobra.ShellCompDirectiveNoFileComp 63 | 64 | } 65 | 66 | func completeFlagFromMelt(flag profilerReportFlag, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 67 | 68 | filterSegments := []string{ 69 | fmt.Sprintf(`attributes(%s) ~ "%s*"`, flag.K8sAttribute(), toComplete), 70 | } 71 | flags := cmd.Flags() 72 | if flags != nil { 73 | profileFlags := []profilerReportFlag{profilerReportFlagCluster, profilerReportFlagNamespace, profilerReportFlagWorkloadName} 74 | for _, f := range profileFlags { 75 | val, _ := flags.GetString(f.String()) 76 | if val != "" { 77 | filterSegments = append( 78 | filterSegments, 79 | fmt.Sprintf(`attributes(%s) = "%s"`, f.K8sAttribute(), val), 80 | ) 81 | } 82 | } 83 | } 84 | 85 | filter := fmt.Sprintf("[%s]", strings.Join(filterSegments, " && ")) 86 | 87 | fetchField := flag.ReportAttribute() 88 | query := fmt.Sprintf(` 89 | SINCE 90 | -3d 91 | FETCH 92 | id, 93 | events(k8sprofiler:report){attributes(%s)} 94 | FROM 95 | entities(k8s:deployment)%s 96 | LIMITS 97 | events.count(1)`, fetchField, filter) 98 | 99 | resp, err := uql.ClientV1.ExecuteQuery(&uql.Query{Str: query}) 100 | if err != nil || resp.HasErrors() { 101 | return nil, cobra.ShellCompDirectiveNoFileComp 102 | } 103 | 104 | m := resp.Main() 105 | if m == nil || len(m.Data) < 1 { 106 | return nil, cobra.ShellCompDirectiveNoFileComp 107 | } 108 | 109 | var results []string 110 | for _, d := range m.Data { 111 | if len(d) < 2 { 112 | continue 113 | } 114 | eventDataSet, ok := d[1].(*uql.DataSet) 115 | if !ok { 116 | continue 117 | } 118 | if len(eventDataSet.Data) < 1 || len(eventDataSet.Data[0]) < 1 { 119 | continue 120 | } 121 | s, ok := eventDataSet.Data[0][0].(string) 122 | if ok { 123 | results = append(results, s) 124 | } 125 | } 126 | 127 | return results, cobra.ShellCompDirectiveNoFileComp 128 | } 129 | -------------------------------------------------------------------------------- /cmd/optimize/configure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package optimize 16 | 17 | import ( 18 | "io" 19 | "net/http" 20 | "net/http/httptest" 21 | "os" 22 | "slices" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/spf13/cobra" 27 | 28 | "github.com/cisco-open/fsoc/test" 29 | ) 30 | 31 | var validPaths []string = []string{ 32 | "/monitoring/v1/query/execute", 33 | "/knowledge-store/v1/objects/:optimizer", 34 | "knowledge-store/v1/objects/:optimizer?filter=data.target.k8sDeployment.workloadId+eq+%22VfJUeLlJOUyRrgi8ABDBMQ%22", 35 | } 36 | 37 | var WORKLOAD_UQL_RESP_FILE string = "testdata/configure_test_workload.json" 38 | var REPORT_UQL_RESP_FILE string = "testdata/configure_test_report.json" 39 | 40 | func TestConfigureMultipleMatches(t *testing.T) { 41 | workloadResponse, err := os.ReadFile(WORKLOAD_UQL_RESP_FILE) 42 | if err != nil { 43 | t.Fatalf("failed to load test data from %v: %v", WORKLOAD_UQL_RESP_FILE, err) 44 | return 45 | } 46 | reportResponse, err := os.ReadFile(REPORT_UQL_RESP_FILE) 47 | if err != nil { 48 | t.Fatalf("failed to load test data from %v: %v", REPORT_UQL_RESP_FILE, err) 49 | return 50 | } 51 | // HTTP server hit by calls made duringtest 52 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 | if !slices.Contains(validPaths, r.URL.Path) { 54 | t.Errorf("Unexpected path requeested: %s", r.URL.Path) 55 | } 56 | if r.Header.Get("Accept") != "application/json" { 57 | t.Errorf("Expected Accept: application/json header, got: %s", r.Header.Get("Accept")) 58 | } 59 | body, err := io.ReadAll(r.Body) 60 | if err != nil { 61 | t.Errorf("unable to read request body: %v", err) 62 | w.WriteHeader(http.StatusBadRequest) 63 | return 64 | } 65 | bodyStr := string(body) 66 | w.WriteHeader(http.StatusOK) 67 | if r.URL.Path == "/knowledge-store/v1/objects/:optimizer" { 68 | if r.Method == "GET" { 69 | w.WriteHeader(http.StatusNotFound) 70 | } 71 | return 72 | } 73 | var respErr error 74 | if r.Method == "GET" { 75 | _, respErr = w.Write([]byte(`{"items": [], "total": 0}`)) 76 | } else if strings.Contains(bodyStr, "FETCH id") { 77 | _, respErr = w.Write([]byte(workloadResponse)) 78 | } else if strings.Contains(bodyStr, "FETCH events") { 79 | _, respErr = w.Write([]byte(reportResponse)) 80 | } else { 81 | t.Errorf("unrecognized query: %v", bodyStr) 82 | w.WriteHeader(http.StatusBadRequest) 83 | } 84 | if respErr != nil { 85 | t.Errorf("error writing response: %v", respErr) 86 | } 87 | })) 88 | 89 | defer test.SetActiveConfigProfileServer(testServer.URL)() 90 | 91 | testFlags := &configureFlags{} 92 | testFlags.Cluster = "optimize-c1-qe" 93 | testFlags.Namespace = "bofa-24-02" 94 | testFlags.WorkloadName = "frontend" 95 | testFlags.create = true 96 | testFlags.overrideSoftBlockers = true 97 | createFunc := configureOptimizer(testFlags) 98 | if err := createFunc(&cobra.Command{}, nil); err != nil { 99 | t.Fatalf("failed to confiugre multimatch workload: %v", err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/optimize/optimize.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package optimize 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // optimizeCmd represents the optimize command 22 | var optimizeCmd = &cobra.Command{ 23 | Use: "optimize", 24 | Short: "Perform optimize interactions", 25 | Long: `Interact with optimize components. Currently only workload profile reports are available 26 | via the report subcommand.`, 27 | Example: ` fsoc optimize report "frontend"`, 28 | TraverseChildren: true, 29 | } 30 | 31 | func NewSubCmd() *cobra.Command { 32 | optimizeCmd.AddCommand(NewCmdConfigure()) 33 | optimizeCmd.AddCommand(NewCmdReport()) 34 | 35 | return optimizeCmd 36 | } 37 | -------------------------------------------------------------------------------- /cmd/optimize/report_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package optimize 16 | 17 | import ( 18 | "net/http" 19 | "net/http/httptest" 20 | "os" 21 | "testing" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | "github.com/cisco-open/fsoc/test" 26 | ) 27 | 28 | func TestReportsNoEventsData(t *testing.T) { 29 | reportResponse, err := os.ReadFile("testdata/report_test.json") 30 | if err != nil { 31 | t.Fatalf("failed to load test data: %v", err) 32 | return 33 | } 34 | 35 | // HTTP server hit by calls made duringtest 36 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | if r.URL.Path != "/monitoring/v1/query/execute" { 38 | t.Errorf("Unexpected path requeested: %s", r.URL.Path) 39 | } 40 | w.WriteHeader(http.StatusOK) 41 | _, err := w.Write(reportResponse) 42 | if err != nil { 43 | t.Errorf("error writing response: %v", err) 44 | } 45 | })) 46 | 47 | defer test.SetActiveConfigProfileServer(testServer.URL)() 48 | 49 | testFlags := &reportFlags{} 50 | testFlags.Cluster = "optimize-c1-qe" 51 | testFlags.Namespace = "bofa-24-02" 52 | testFlags.WorkloadName = "frontend" 53 | listFunc := listReports(testFlags) 54 | if err := listFunc(&cobra.Command{}, nil); err != nil { 55 | t.Fatalf("failed to confiugre multimatch workload: %v", err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/optimize/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package optimize 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/cmdkit" 22 | "github.com/cisco-open/fsoc/output" 23 | ) 24 | 25 | func init() { 26 | // TODO move this logic to optimize root when implementing unit tests 27 | optimizeCmd.AddCommand(NewCmdStatus()) 28 | } 29 | 30 | func NewCmdStatus() *cobra.Command { 31 | statusCmd := &cobra.Command{ 32 | Use: "status", 33 | Short: "List onboarded optimizer configuration and status", 34 | Long: ` 35 | List optimization status and configuration 36 | 37 | If no flags are provided, all onboarded optimizations will be listed 38 | You can optionally filter optimizations by cluster, namespace and/or workload name 39 | You may also specify a particular optimizer ID to fetch details for a single optimization (recommended with -o detail or -o yaml) 40 | `, 41 | Example: "fsoc optimize status --workload-name frontend", 42 | Args: cobra.NoArgs, 43 | RunE: listStatus, 44 | TraverseChildren: true, 45 | Annotations: map[string]string{ 46 | output.TableFieldsAnnotation: "OPTIMIZERID: .id, WORKLOADNAME: .data.optimizer.target.k8sDeployment.workloadName, STATUS: .data.optimizerState, SUSPENDED: .data.suspended, STAGE: .data.optimizationState, AGENT: .data.agentState, TUNING: .data.tuningState, BLOCKERS: (.data.optimizer.ignoredBlockers? // \"false\" | select(. == \"false\") // \"true\")", 47 | output.DetailFieldsAnnotation: "OPTIMIZERID: .id, CONTAINER: .data.optimizer.target.k8sDeployment.containerName, WORKLOADNAME: .data.optimizer.target.k8sDeployment.workloadName, NAMESPACE: .data.optimizer.target.k8sDeployment.namespaceName, CLUSTER: .data.optimizer.target.k8sDeployment.clusterName, STATUS: .data.optimizerState, SUSPENDED: .data.suspended, SUSPENSIONS: .data.optimizer.suspensions, RESTARTEDAT: .data.optimizer.restartTimestamp, STAGE: .data.optimizationState, AGENT: .data.agentState, TUNING: .data.tuningState, BLOCKERS: (.data.optimizer.ignoredBlockers?.blockers? // {} | keys)", 48 | }, 49 | } 50 | statusCmd.Flags().StringP("cluster", "c", "", "Filter statuses by kubernetes cluster name") 51 | statusCmd.Flags().StringP("namespace", "n", "", "Filter statuses by kubernetes namespace") 52 | statusCmd.Flags().StringP("workload-name", "w", "", "Filter statuses by name of kubernetes workload") 53 | 54 | statusCmd.Flags().StringP("optimizer-id", "i", "", "Retrieve status for a specific optimizer by its ID (best used with -o detail)") 55 | statusCmd.MarkFlagsMutuallyExclusive("optimizer-id", "cluster") 56 | statusCmd.MarkFlagsMutuallyExclusive("optimizer-id", "namespace") 57 | statusCmd.MarkFlagsMutuallyExclusive("optimizer-id", "workload-name") 58 | 59 | statusCmd.Flags().StringP("solution-name", "", "optimize", "Intended for developer usage, overrides the name of the solution defining the Orion types for reading/writing") 60 | if err := statusCmd.LocalFlags().MarkHidden("solution-name"); err != nil { 61 | log.Warnf("Failed to set statusCmd solution-name flag hidden: %v", err) 62 | } 63 | 64 | registerOptimizerCompletion(statusCmd, optimizerFlagCluster) 65 | registerOptimizerCompletion(statusCmd, optimizerFlagNamespace) 66 | registerOptimizerCompletion(statusCmd, optimizerFlagOptimizerId) 67 | registerOptimizerCompletion(statusCmd, optimizerFlagWorkloadName) 68 | 69 | return statusCmd 70 | } 71 | 72 | func listStatus(cmd *cobra.Command, args []string) error { 73 | objStoreUrl := getKnowledgeURL(cmd, "status", "data.optimizer") 74 | 75 | headers := getOrionTenantHeaders() 76 | cmdkit.FetchAndPrint(cmd, objStoreUrl, &cmdkit.FetchAndPrintOptions{Headers: headers, IsCollection: true}) 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/optimize/testdata/configure_test_workload.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "model", 4 | "model": { 5 | "name": "m:main", 6 | "resultType": "LISTING", 7 | "fields": [ 8 | { 9 | "alias": "id", 10 | "type": "string", 11 | "hints": { 12 | "kind": "entity", 13 | "field": "id", 14 | "type": "k8s:deployment" 15 | }, 16 | "properties": { 17 | "functionType": "DIMENSION", 18 | "orderable": true, 19 | "fieldCoordinates": { 20 | "from": "2:6", 21 | "to": "2:7" 22 | }, 23 | "querySnippet": "id" 24 | } 25 | }, 26 | { 27 | "alias": "isActive", 28 | "type": "boolean", 29 | "hints": { 30 | "kind": "entity", 31 | "field": "isActive", 32 | "type": "k8s:deployment" 33 | }, 34 | "properties": { 35 | "functionType": "DIMENSION", 36 | "orderable": true, 37 | "fieldCoordinates": { 38 | "from": "2:10", 39 | "to": "2:17" 40 | }, 41 | "querySnippet": "isActive" 42 | } 43 | } 44 | ] 45 | } 46 | }, 47 | { 48 | "type": "data", 49 | "model": { 50 | "$jsonPath": "$..[?(@.type == 'model')]..[?(@.name == 'm:main')]", 51 | "$model": "m:main" 52 | }, 53 | "metadata": { 54 | "since": "2024-04-12T19:50:15.847747259Z", 55 | "until": "2024-04-19T19:50:15.847747259Z" 56 | }, 57 | "main": true, 58 | "dataset": "d:main", 59 | "data": [ 60 | [ 61 | "k8s:deployment:VfJUeLlJOUyRrgi8ABDBMQ", 62 | true 63 | ], 64 | [ 65 | "k8s:deployment:GY6sVnqLPbCtHwDpX0JARw", 66 | false 67 | ], 68 | [ 69 | "k8s:deployment:Cky9fa+OMeWBRhYR0miVeg", 70 | false 71 | ] 72 | ] 73 | } 74 | ] -------------------------------------------------------------------------------- /cmd/provisioning.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/provisioning" 19 | ) 20 | 21 | func init() { 22 | registerSubSystemWithConfig(provisioning.NewSubCmd(), &provisioning.GlobalConfig) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/provisioning/api_version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package provisioning 16 | 17 | import ( 18 | "fmt" 19 | "slices" 20 | "strings" 21 | ) 22 | 23 | // Tenant Provisioning API version type, supporting a limited set of values. 24 | // Implements the StringEnumer interface defined by the fsoc config package in order 25 | // to support parsing apiver from an fsoc config file. 26 | type ApiVersion string 27 | 28 | const ( 29 | ApiVersionDefault ApiVersion = ApiVersion("v1beta") 30 | ) 31 | 32 | var supportedApiVersions = []string{ 33 | string(ApiVersionDefault), 34 | } 35 | 36 | func (a *ApiVersion) ValidateAndSet(version any) error { 37 | s, ok := version.(string) 38 | if !ok { 39 | return fmt.Errorf(`the API version value must be a string, found %T instead`, version) 40 | } 41 | if !slices.Contains(supportedApiVersions, s) { 42 | return fmt.Errorf(`API version %q is not supported; valid value(s): "%v"`, version, strings.Join(supportedApiVersions, `", "`)) 43 | } 44 | *a = ApiVersion(s) 45 | return nil 46 | } 47 | 48 | func (a *ApiVersion) String() string { 49 | if a == nil || string(*a) == "" { 50 | return string(ApiVersionDefault) 51 | } else { 52 | return string(*a) 53 | } 54 | } 55 | 56 | func getBaseUrl() string { 57 | var version ApiVersion 58 | if GlobalConfig.ApiVersion != nil && *GlobalConfig.ApiVersion != "" { 59 | version = *GlobalConfig.ApiVersion // version from config file 60 | } else { 61 | version = ApiVersionDefault 62 | } 63 | return fmt.Sprintf("/provisioning/%v", version) 64 | } 65 | 66 | func getTenantLookupUrl(vanityUrl string) string { 67 | return fmt.Sprintf("%v/tenants/lookup/vanityUrl/%v", getBaseUrl(), vanityUrl) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/provisioning/lookup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package provisioning 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | 20 | "github.com/cisco-open/fsoc/cmdkit" 21 | ) 22 | 23 | func newCmdLookup() *cobra.Command { 24 | 25 | var lookupCmd = &cobra.Command{ 26 | Use: "lookup", 27 | Short: "Lookup for a tenant Id by vanity URL", 28 | Long: `Check whether tenant exist and return tenant Id if it does. 29 | Tenant lookup doesn't require valid authentication (auth=none) but any configured auth type/tenant will also work.`, 30 | Example: ` fsoc provisioning lookup MYTENANT.observe.appdynamics.com 31 | fsoc tep lookup MYTENANT.observe.appdynamics.com`, 32 | Args: cobra.ExactArgs(1), 33 | Run: lookup, 34 | TraverseChildren: true, 35 | } 36 | return lookupCmd 37 | } 38 | 39 | func lookup(cmd *cobra.Command, args []string) { 40 | vanityUrl := args[0] 41 | cmdkit.FetchAndPrint(cmd, getTenantLookupUrl(vanityUrl), nil) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/provisioning/provisioning.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package provisioning 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // Config defines the subsystem configuration under fsoc 22 | type Config struct { 23 | ApiVersion *ApiVersion `mapstructure:"apiver,omitempty" fsoc-help:"API version to use for tenant provisioning. The default is \"v1beta\"."` 24 | } 25 | 26 | // To make it work provisioning should be registered as subsystem and 27 | // provisioning.GlobalConfig needs to be passed as provisioning config 28 | var GlobalConfig Config 29 | 30 | func NewSubCmd() *cobra.Command { 31 | 32 | var cmd = &cobra.Command{ 33 | Use: "provisioning", 34 | Short: "Tenant provisioning and management", 35 | Long: `Use to provision new tenant and troubleshoot provisioning workflow.`, 36 | Aliases: []string{"prov", "tep"}, 37 | Example: ` fsoc provisioning`, 38 | TraverseChildren: true, 39 | Hidden: true, 40 | } 41 | 42 | cmd.AddCommand(newCmdLookup()) 43 | 44 | return cmd 45 | } 46 | -------------------------------------------------------------------------------- /cmd/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/proxy" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(proxy.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package proxy 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/cisco-open/fsoc/output" 24 | "github.com/cisco-open/fsoc/platform/api" 25 | ) 26 | 27 | // proxyCmd represents the login command 28 | var proxyCmd = &cobra.Command{ 29 | Use: "proxy [flags] [--] [command] [args...]", 30 | Short: "Proxy local http requests to platform", 31 | Long: `This command runs a proxy server to forward http requests 32 | to the platform API. It will automatically login and provide the necessary authentication. 33 | 34 | The command can be used in two modes: 35 | 1. Run the proxy server in foreground until terminated with Ctrl-C. 36 | 2. Start the proxy server, execute a command (e.g., curl or shell script) and terminate. 37 | 38 | When running a command, fsoc will exit with the exit code of the command. Note that if the 39 | command has flags, you must use the ` + "`--`" + ` separator before the command, so that 40 | fsoc does not interpret the flags as its own. Otherwise, the separator is optional.`, 41 | Example: ` fsoc proxy -p 8000 42 | fsoc proxy -q -- curl -fsSL http://localhost:8080/knowledge-store/v1/objects/extensibility:solution/k8sprofiler 43 | fsoc proxy -p 8000 mytest.sh 8000`, 44 | Run: proxy, 45 | } 46 | 47 | func NewSubCmd() *cobra.Command { 48 | proxyCmd.Flags().IntP("port", "p", 8080, "Port to listen on") 49 | proxyCmd.Flags().BoolP("quiet", "q", false, "Suppress all fsoc status output to stdout") 50 | return proxyCmd 51 | } 52 | 53 | func proxy(cmd *cobra.Command, args []string) { 54 | // setup status printer, suppressing output if quiet flag is set 55 | quiet, _ := cmd.Flags().GetBool("quiet") 56 | statusPrinter := func(s string) { 57 | log.Info(s) 58 | if !quiet { 59 | output.PrintCmdStatus(cmd, s+"\n") 60 | } 61 | } 62 | 63 | // ensure profile is logged in before we start the proxy 64 | if err := api.Login(); err != nil { 65 | log.Fatalf("Login failed: %v", err) 66 | } 67 | statusPrinter("Login completed successfully") 68 | 69 | // run the proxy server 70 | port, _ := cmd.Flags().GetInt("port") 71 | var exitCode int 72 | if err := api.RunProxyServer(port, args, statusPrinter, &exitCode); err != nil { 73 | log.Fatalf("Proxy server failed: %v", err) 74 | } 75 | 76 | // pass exit code back to caller (if we executed a command) 77 | if len(args) > 0 && exitCode != 0 { 78 | os.Exit(exitCode) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cmd/solution.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/solution" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(solution.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/solution/bump.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/Masterminds/semver/v3" 21 | "github.com/apex/log" 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/cisco-open/fsoc/config" 25 | "github.com/cisco-open/fsoc/output" 26 | ) 27 | 28 | var solutionBumpCmd = &cobra.Command{ 29 | Use: "bump", 30 | Short: "Increment the patch version of the solution", 31 | Long: `Increment the patch version of the solution in the manifest to prepare 32 | it for validation or push.`, 33 | Example: ` fsoc solution bump`, 34 | Args: cobra.ExactArgs(0), 35 | Run: bumpSolutionVersion, 36 | Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, 37 | TraverseChildren: true, 38 | } 39 | 40 | func getSolutionBumpCmd() *cobra.Command { 41 | //TODO: consider adding a --minor flag to bump the minor version instead of 42 | //the patch. 43 | return solutionBumpCmd 44 | } 45 | 46 | func bumpSolutionVersion(cmd *cobra.Command, args []string) { 47 | manifestDir := "." 48 | 49 | manifest, err := getSolutionManifest(manifestDir) 50 | if err != nil { 51 | log.Fatalf("Failed to read solution manifest: %v", err) 52 | } 53 | oldVer := manifest.SolutionVersion 54 | 55 | if err = bumpManifestPatchVersion(manifest); err != nil { 56 | log.Fatalf(err.Error()) 57 | } 58 | newVer := manifest.SolutionVersion 59 | 60 | if err = saveSolutionManifest(manifestDir, manifest); err != nil { 61 | log.Fatalf("Failed to update solution manifest: %v", err) 62 | } 63 | 64 | output.PrintCmdStatus(cmd, fmt.Sprintf("Successfully bumped solution version from %v to %v\n", oldVer, newVer)) 65 | } 66 | 67 | func bumpManifestPatchVersion(m *Manifest) error { 68 | ver, err := semver.StrictNewVersion(m.SolutionVersion) 69 | if err != nil { 70 | return fmt.Errorf("failed to semver parse solution version %q: %w", m.SolutionVersion, err) 71 | } 72 | 73 | // refuse to bump if the version has prelease or metadata, as "bump" 74 | // is not clearly defined in this case (see semver.Version.IncPatch() for details) 75 | if ver.Prerelease() != "" || ver.Metadata() != "" { 76 | return fmt.Errorf("cannot bump current version %q because it has prelease and/or metadata info; please set the desired new version manually in the manifest", m.SolutionVersion) 77 | } 78 | 79 | // bump patch version and update into the manifest 80 | newVer := ver.IncPatch() 81 | m.SolutionVersion = newVer.String() 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /cmd/solution/describe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "github.com/apex/log" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/cisco-open/fsoc/config" 22 | "github.com/cisco-open/fsoc/output" 23 | "github.com/cisco-open/fsoc/platform/api" 24 | ) 25 | 26 | var solutionDescribeCmd = &cobra.Command{ 27 | Use: "describe ", 28 | Args: cobra.MaximumNArgs(1), 29 | Short: "Describe solution", 30 | Long: `Obtain metadata about a solution`, 31 | Example: ` fsoc solution describe spacefleet`, 32 | Run: solutionDescribe, 33 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 34 | config.SetActiveProfile(cmd, args, false) 35 | return getSolutionNames(toComplete), cobra.ShellCompDirectiveDefault 36 | }, 37 | } 38 | 39 | func getSolutionDescribeCmd() *cobra.Command { 40 | solutionDescribeCmd.Flags(). 41 | String("solution", "", "The name of the solution to describe") 42 | _ = solutionDescribeCmd.Flags().MarkDeprecated("solution", "please use argument instead.") 43 | 44 | return solutionDescribeCmd 45 | } 46 | 47 | func solutionDescribe(cmd *cobra.Command, args []string) { 48 | solution := getSolutionNameFromArgs(cmd, args, "solution") 49 | 50 | cfg := config.GetCurrentContext() 51 | layerID := cfg.Tenant 52 | 53 | headers := map[string]string{ 54 | "layer-type": "TENANT", 55 | "layer-id": layerID, 56 | } 57 | 58 | log.WithField("solution", solution).Info("Getting solution details") 59 | var res Solution 60 | err := api.JSONGet(getSolutionObjectUrl(solution), &res, &api.Options{Headers: headers}) 61 | if err != nil { 62 | log.Fatalf("Cannot get solution details: %v", err) 63 | } 64 | output.PrintCmdOutput(cmd, res) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/solution/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "net/url" 19 | "strings" 20 | 21 | "github.com/apex/log" 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/cisco-open/fsoc/cmdkit" 25 | "github.com/cisco-open/fsoc/config" 26 | "github.com/cisco-open/fsoc/output" 27 | "github.com/cisco-open/fsoc/platform/api" 28 | ) 29 | 30 | var solutionListCmd = &cobra.Command{ 31 | Use: "list [--subscribed | --unsubscribed]", 32 | Args: cobra.ExactArgs(0), 33 | Short: "List all solutions available in this tenant", 34 | Long: `This command list all the solutions that are deployed in the current tenant specified in the profile.`, 35 | Example: ` fsoc solution list 36 | fsoc solution list -o json`, 37 | Run: getSolutionList, 38 | TraverseChildren: true, 39 | Annotations: map[string]string{ 40 | output.TableFieldsAnnotation: "name:.data.name, tag:.data.tag, isSystem:.data.isSystem, isSubscribed:.data.isSubscribed, dependencies:.data.dependencies", 41 | output.DetailFieldsAnnotation: "name:.data.name, tag:.data.tag, isSystem:.data.isSystem, isSubscribed:.data.isSubscribed, dependencies:.data.dependencies, installDate:.createdAt, updateDate:.updatedAt", 42 | }, 43 | } 44 | 45 | func getSolutionListCmd() *cobra.Command { 46 | solutionListCmd.Flags(). 47 | Bool("subscribed", false, "Use this to only see solutions that you are subscribed to") 48 | solutionListCmd.Flags(). 49 | Bool("unsubscribed", false, "Use this to only see solutions that you are unsubscribed to") 50 | 51 | solutionListCmd.MarkFlagsMutuallyExclusive("subscribed", "unsubscribed") 52 | 53 | return solutionListCmd 54 | 55 | } 56 | 57 | func getSolutionList(cmd *cobra.Command, args []string) { 58 | log.Info("Fetching the list of solutions...") 59 | // get subscribe and unsubscribe flags 60 | subscribed := cmd.Flags().Lookup("subscribed").Changed 61 | unsubscribed := cmd.Flags().Lookup("unsubscribed").Changed 62 | 63 | cfg := config.GetCurrentContext() 64 | layerID := cfg.Tenant 65 | 66 | headers := map[string]string{ 67 | "layer-type": "TENANT", 68 | "layer-id": layerID, 69 | } 70 | 71 | // get data and display 72 | solutionBaseURL := getSolutionObjectUrl("") 73 | var filters []string 74 | if subscribed { 75 | filters = []string{"filter=" + url.QueryEscape("data.isSubscribed eq true")} 76 | } else if unsubscribed { 77 | filters = []string{"filter=" + url.QueryEscape("data.isSubscribed ne true")} 78 | } 79 | cmdkit.FetchAndPrint(cmd, solutionBaseURL, &cmdkit.FetchAndPrintOptions{Headers: headers, IsCollection: true, Filters: filters}) 80 | } 81 | 82 | func getSolutionNames(prefix string) (names []string) { 83 | headers := map[string]string{ 84 | "layer-type": "TENANT", 85 | "layer-id": config.GetCurrentContext().Tenant, 86 | } 87 | httpOptions := &api.Options{Headers: headers} 88 | 89 | var result api.CollectionResult[Solution] 90 | err := api.JSONGetCollection[Solution](getSolutionObjectUrl(""), &result, httpOptions) 91 | if err != nil { 92 | return names 93 | } 94 | 95 | for _, s := range result.Items { 96 | if strings.HasPrefix(s.ID, prefix) { 97 | names = append(names, s.ID) 98 | } 99 | } 100 | return names 101 | } 102 | -------------------------------------------------------------------------------- /cmd/solution/push.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | var solutionPushCmd = &cobra.Command{ 22 | Use: "push", 23 | Args: cobra.ExactArgs(0), 24 | Short: "Deploy your solution", 25 | Long: `This command allows the current tenant specified in the profile to deploy a solution to the platform. 26 | The solution manifest for the solution must be in the current directory. 27 | 28 | Important details on solution tags: 29 | 1. A tag must be associated with the solution being uploaded. All subsequent solution upload requests should use this same tag 30 | 2. Use caution when supplying the tag value to the solution to upload as typos can result in misleading validation results 31 | 3. "stable" is a reserved tag value keyword for production-ready versions and hence should be used appropriately 32 | 4. For more info on tags, please visit: https://developer.cisco.com/docs/cisco-observability-platform/#!tag-a-solution 33 | 34 | A tag may be defined in the following ways (in order of precedence): 35 | 1. Specified flag --tag=xyz or --stable: use this tag, ignoring .tag file or env vars 36 | 2. A tag is defined in the FSOC_SOLUTION_TAG environment variable (ignores .tag file) 37 | 3. A tag is defined in the .tag file in the solution directory (usually not version controlled) 38 | `, 39 | Example: ` 40 | fsoc solution push --tag=stable 41 | fsoc solution push --wait --tag=dev 42 | fsoc solution push --bump --wait=60 43 | fsoc solution push -d mysolution --stable --wait 44 | fsoc solution push --solution-bundle=mysolution-1.22.3.zip --tag=stable`, 45 | Run: pushSolution, 46 | TraverseChildren: true, 47 | } 48 | 49 | func getSolutionPushCmd() *cobra.Command { 50 | addTagFlags(solutionPushCmd) // --tag and --stable 51 | 52 | solutionPushCmd.Flags().IntP("wait", "w", -1, "Wait (in seconds) for the solution to be deployed") 53 | solutionPushCmd.Flag("wait").NoOptDefVal = "300" 54 | 55 | solutionPushCmd.Flags(). 56 | BoolP("bump", "b", false, "Increment the patch version before deploying solution") 57 | 58 | solutionPushCmd.Flags(). 59 | StringP("directory", "d", "", "Path to the solution root directory (defaults to current dir)") 60 | 61 | solutionPushCmd.Flags(). 62 | String("solution-bundle", "", "Path to a prepackaged solution zip") 63 | 64 | solutionPushCmd.Flags(). 65 | String("env-file", "", "Path to the env vars json file with pseudo-isolation tag and, optionally, dependency tags (DEPRECATED)") 66 | 67 | solutionPushCmd.Flags(). 68 | Bool("no-isolate", false, "Disable fsoc-supported solution pseudo-isolation") 69 | 70 | solutionPushCmd.Flags(). 71 | Bool("subscribe", false, "Subscribe to the solution that you are pushing") 72 | 73 | solutionPushCmd.MarkFlagsMutuallyExclusive("solution-bundle", "directory") // either solution dir or prepackaged zip 74 | solutionPushCmd.MarkFlagsMutuallyExclusive("solution-bundle", "bump") // cannot modify prepackaged zip 75 | solutionPushCmd.MarkFlagsMutuallyExclusive("solution-bundle", "wait") // TODO: allow when extracting manifest data 76 | solutionPushCmd.MarkFlagsMutuallyExclusive("solution-bundle", "subscribe") // TODO: allow when extracting manifest data 77 | solutionPushCmd.MarkFlagsMutuallyExclusive("tag", "stable", "env-file") 78 | 79 | return solutionPushCmd 80 | } 81 | 82 | func pushSolution(cmd *cobra.Command, args []string) { 83 | uploadSolution(cmd, true) 84 | } 85 | -------------------------------------------------------------------------------- /cmd/solution/types-solution-test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package solution 16 | 17 | type SolutionTestObjects struct { 18 | Tests []SolutionTestObject `json:"tests"` 19 | InitialDelay int `json:"initialDelay,omitempty"` 20 | MaxRetryCount int `json:"retryCount,omitempty"` 21 | RetryDelay int `json:"retryDelay,omitempty"` 22 | } 23 | 24 | type SolutionTestObject struct { 25 | Name string `json:"name,omitempty"` 26 | Type string `json:"type,omitempty"` 27 | Description string `json:"description,omitempty"` 28 | Setup SolutionTestSetup `json:"setup"` 29 | Assertions []SolutionTestAssertion `json:"assertions"` 30 | } 31 | 32 | type SolutionTestSetup struct { 33 | Type string `json:"type"` 34 | Input interface{} `json:"input,omitempty"` 35 | Location string `json:"location,omitempty"` 36 | } 37 | 38 | type SolutionTestAssertion struct { 39 | UQL string `json:"uql"` 40 | Transforms []SolutionTestAssertionTransform `json:"transforms"` 41 | } 42 | 43 | type SolutionTestAssertionTransform struct { 44 | Type string `json:"type"` 45 | Expression string `json:"expression,omitempty"` 46 | Message string `json:"message,omitempty"` 47 | Location string `json:"location,omitempty"` 48 | } 49 | 50 | type SolutionTestResult struct { 51 | ID string `json:"testRunId"` 52 | } 53 | 54 | type SolutionTestStatusResult struct { 55 | Complete bool `json:"completed"` 56 | Status string `json:"status"` 57 | StatusMessages []StatusMessage `json:"statusMessages"` 58 | } 59 | 60 | type StatusMessage struct { 61 | Timestamp string `json:"timestamp"` 62 | Message string `json:"message,omitempty"` 63 | Statuses []string `json:"statuses,omitempty"` 64 | } 65 | -------------------------------------------------------------------------------- /cmd/solution/unsubscribe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | 20 | "github.com/cisco-open/fsoc/config" 21 | ) 22 | 23 | var solutionUnsubscribeCmd = &cobra.Command{ 24 | Use: "unsubscribe ", 25 | Args: cobra.MaximumNArgs(1), 26 | Short: "Unsubscribe from a solution", 27 | Long: `This command allows the current tenant specified in the profile to unsubscribe from a solution.`, 28 | Example: ` fsoc solution unsubscribe spacefleet`, 29 | Run: unsubscribeFromSolution, 30 | TraverseChildren: true, 31 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 | config.SetActiveProfile(cmd, args, false) 33 | return getSolutionNames(toComplete), cobra.ShellCompDirectiveDefault 34 | }, 35 | } 36 | 37 | func getUnsubscribeSolutionCmd() *cobra.Command { 38 | solutionUnsubscribeCmd.Flags(). 39 | String("name", "", "The name of the solution the tenant is unsubscribing from") 40 | _ = solutionUnsubscribeCmd.Flags().MarkDeprecated("name", "please use argument instead.") 41 | solutionUnsubscribeCmd.Flags(). 42 | String("tag", "", "The tag related to the solution to unsubscribe from. This will default to the stable version of the solution if not specified") 43 | 44 | return solutionUnsubscribeCmd 45 | 46 | } 47 | 48 | func unsubscribeFromSolution(cmd *cobra.Command, args []string) { 49 | manageSubscription(cmd, args, false) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/solution/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package solution 16 | 17 | import ( 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | type ErrorItem struct { 22 | Error string `json:"error"` 23 | Source string `json:"source"` 24 | } 25 | 26 | type Errors struct { 27 | Items []ErrorItem `json:"items"` 28 | Total int `json:"total"` 29 | } 30 | 31 | type Result struct { 32 | Errors Errors `json:"errors"` 33 | Valid bool `json:"valid"` 34 | } 35 | 36 | var solutionValidateCmd = &cobra.Command{ 37 | Use: "validate", 38 | Args: cobra.ExactArgs(0), 39 | Short: "Validate solution", 40 | Long: `This command allows the current tenant specified in the profile to upload the solution in the current directory just to validate its contents. The --stable flag provides a default value of 'stable' for the tag associated with the given solution. `, 41 | Example: ` fsoc solution validate 42 | fsoc solution validate --bump --tag preprod 43 | fsoc solution validate --tag dev 44 | fsoc solution validate --stable 45 | fsoc solution validate -d mysolution --tag dev 46 | fsoc solution validate --solution-bundle=mysolution-1.22.3.zip --tag stable`, 47 | Run: validateSolution, 48 | TraverseChildren: true, 49 | } 50 | 51 | func getSolutionValidateCmd() *cobra.Command { 52 | addTagFlags(solutionValidateCmd) // tag, stable 53 | 54 | solutionValidateCmd.Flags(). 55 | BoolP("bump", "b", false, "Increment the patch version before validation") 56 | 57 | solutionValidateCmd.Flags(). 58 | StringP("directory", "d", "", "Path to the solution root directory (defaults to current dir)") 59 | 60 | solutionValidateCmd.Flags(). 61 | String("solution-bundle", "", "Path to a prepackaged solution zip") 62 | 63 | solutionValidateCmd.Flags(). 64 | String("env-file", "", "Path to the env vars json file with pseudo-isolation tag and, optionally, dependency tags (DEPRECATED)") 65 | 66 | solutionValidateCmd.Flags(). 67 | Bool("no-isolate", false, "Disable fsoc-supported solution pseudo-isolation") 68 | 69 | solutionValidateCmd.MarkFlagsMutuallyExclusive("solution-bundle", "directory") 70 | solutionValidateCmd.MarkFlagsMutuallyExclusive("solution-bundle", "bump") 71 | 72 | solutionValidateCmd.MarkFlagsMutuallyExclusive("tag", "stable", "env-file") 73 | 74 | return solutionValidateCmd 75 | } 76 | 77 | func validateSolution(cmd *cobra.Command, args []string) { 78 | uploadSolution(cmd, false) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/uql.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/uql" 19 | ) 20 | 21 | func init() { 22 | registerSubSystemWithConfig(uql.NewSubCmd(), &uql.GlobalConfig) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/uql/api_version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package uql 16 | 17 | import ( 18 | "fmt" 19 | "slices" 20 | "strings" 21 | ) 22 | 23 | // UQL API version type, supporting a limited set of values. 24 | // Implements the StringEnumer interface defined by the fsoc config package in order 25 | // to support parsing apiver from an fsoc config file. 26 | type ApiVersion string 27 | 28 | // constants for direct use 29 | const ( 30 | ApiVersion1 ApiVersion = ApiVersion("v1") 31 | //ApiVersion2Beta ApiVersion = ApiVersion("v2beta") 32 | 33 | ApiVersionDefault ApiVersion = ApiVersion1 34 | ) 35 | 36 | // note: when changing the set of supported values and/or the default, 37 | // don't forget to update the fsoc-help tag in the GlobalConfig (see uql.go) 38 | 39 | var supportedApiVersions = []string{ 40 | string(ApiVersion1), 41 | //string(ApiVersion2Beta), 42 | } 43 | 44 | func (a *ApiVersion) ValidateAndSet(v any) error { 45 | s, ok := v.(string) 46 | if !ok { 47 | return fmt.Errorf("the API version value must be a string, found %T instead", v) 48 | } 49 | if !slices.Contains(supportedApiVersions, s) { 50 | return fmt.Errorf(`API version %q is not supported; valid value(s): "%v"`, v, strings.Join(supportedApiVersions, `", "`)) 51 | } 52 | *a = ApiVersion(s) 53 | return nil 54 | } 55 | 56 | func (a *ApiVersion) String() string { 57 | if a == nil || string(*a) == "" { 58 | return string(ApiVersionDefault) 59 | } else { 60 | return string(*a) 61 | } 62 | } 63 | 64 | func GetAPIEndpoint(apiVersion ApiVersion) string { 65 | return fmt.Sprintf("/monitoring/%v/query/execute", apiVersion) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/uql/backend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package uql 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/apex/log" 22 | "github.com/pkg/errors" 23 | 24 | "github.com/cisco-open/fsoc/platform/api" 25 | ) 26 | 27 | type uqlService interface { 28 | Execute(query *Query, apiVersion ApiVersion) (parsedResponse, error) 29 | Continue(link *Link) (parsedResponse, error) 30 | } 31 | 32 | type defaultBackend struct { 33 | apiOptions *api.Options 34 | } 35 | 36 | func (b defaultBackend) Execute(query *Query, apiVersion ApiVersion) (parsedResponse, error) { 37 | log.WithFields(log.Fields{"query": query.Str, "apiVersion": apiVersion}).Info("executing UQL query") 38 | 39 | var rawJson json.RawMessage 40 | err := api.JSONPost(GetAPIEndpoint(apiVersion), query, &rawJson, b.apiOptions) 41 | if err != nil { 42 | if problem, ok := err.(api.Problem); ok { 43 | return parsedResponse{}, makeUqlProblem(problem) 44 | } 45 | return parsedResponse{}, errors.Wrap(err, fmt.Sprintf("failed to execute UQL Query: '%s'", query.Str)) 46 | } 47 | var chunks []parsedChunk 48 | err = json.Unmarshal(rawJson, &chunks) 49 | if err != nil { 50 | return parsedResponse{}, errors.Wrap(err, fmt.Sprintf("failed to parse response for UQL Query: '%s'", query.Str)) 51 | } 52 | return parsedResponse{ 53 | chunks: chunks, 54 | rawJson: &rawJson, 55 | }, nil 56 | } 57 | 58 | func (b defaultBackend) Continue(link *Link) (parsedResponse, error) { 59 | log.WithFields(log.Fields{"query": link.Href}).Info("continuing UQL query") 60 | 61 | var rawJson json.RawMessage 62 | err := api.JSONGet(link.Href, &rawJson, b.apiOptions) 63 | if err != nil { 64 | return parsedResponse{}, errors.Wrap(err, fmt.Sprintf("failed follow link: '%s'", link.Href)) 65 | } 66 | var chunks []parsedChunk 67 | err = json.Unmarshal(rawJson, &chunks) 68 | if err != nil { 69 | return parsedResponse{}, errors.Wrap(err, fmt.Sprintf("failed to parse response for link: '%s'", link.Href)) 70 | } 71 | return parsedResponse{ 72 | chunks: chunks, 73 | rawJson: &rawJson, 74 | }, nil 75 | } 76 | 77 | func NewDefaultBackend(options ...BackendOption) defaultBackend { 78 | b := defaultBackend{} 79 | for _, option := range options { 80 | option(&b) 81 | } 82 | return b 83 | } 84 | 85 | type BackendOption func(c *defaultBackend) 86 | 87 | func WithBackendApiOptions(options *api.Options) BackendOption { 88 | return func(b *defaultBackend) { 89 | b.apiOptions = options 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/uql/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package uql 16 | 17 | type UqlClient interface { 18 | // ExecuteQuery sends an execute request to the UQL service 19 | ExecuteQuery(query *Query) (*Response, error) 20 | 21 | // ContinueQuery sends a continue request to the UQL service 22 | ContinueQuery(dataSet *DataSet, rel string) (*Response, error) 23 | } 24 | 25 | type defaultClient struct { 26 | backend uqlService 27 | apiVersion *ApiVersion 28 | } 29 | 30 | type UqlClientOption func(c *defaultClient) 31 | 32 | func NewClient(options ...UqlClientOption) UqlClient { 33 | client := &defaultClient{backend: &defaultBackend{}} 34 | for _, option := range options { 35 | option(client) 36 | } 37 | return client 38 | } 39 | 40 | func WithClientApiVersion(version ApiVersion) UqlClientOption { 41 | return func(c *defaultClient) { 42 | c.apiVersion = &version 43 | } 44 | } 45 | 46 | func (c defaultClient) ExecuteQuery(query *Query) (*Response, error) { 47 | apiVersion := ApiVersion("") 48 | if c.apiVersion != nil { 49 | apiVersion = *c.apiVersion 50 | } 51 | return executeUqlQuery(query, apiVersion, c.backend) 52 | } 53 | 54 | func (c defaultClient) ContinueQuery(dataSet *DataSet, rel string) (*Response, error) { 55 | return continueUqlQuery(dataSet, rel, c.backend) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/uql/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package uql 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | 22 | "github.com/cisco-open/fsoc/platform/api" 23 | ) 24 | 25 | // uqlProblem represents parsed data from an object returned with content-type application/problem+json according to the RFC-7807 26 | // These parsed data are specific for the UQL service. 27 | type uqlProblem struct { 28 | query string 29 | title string 30 | detail string 31 | errorDetails []errorDetail 32 | } 33 | 34 | func (p uqlProblem) Error() string { 35 | return fmt.Sprintf("%s: %s", p.title, p.detail) 36 | } 37 | 38 | // errorDetail contains detailed information about user error in the query 39 | type errorDetail struct { 40 | message string 41 | fixSuggestion string 42 | fixPossibilities []string 43 | errorType string 44 | errorFrom position 45 | errorTo position 46 | } 47 | 48 | // position is a place in a multi-line string. Numbering of lines starts with 1 49 | // Position before the first character has column value 0 50 | type position struct { 51 | line int 52 | column int 53 | } 54 | 55 | func makeUqlProblem(original api.Problem) uqlProblem { 56 | problem := uqlProblem{ 57 | query: asStringOrNothing(original.Extensions["query"]), 58 | title: original.Title, 59 | detail: original.Detail, 60 | errorDetails: make([]errorDetail, 0), 61 | } 62 | switch array := original.Extensions["errorDetails"].(type) { 63 | case []any: 64 | for _, values := range array { 65 | switch asMap := values.(type) { 66 | case map[string]any: 67 | problem.errorDetails = append(problem.errorDetails, makeErrorDetail(asMap)) 68 | } 69 | } 70 | } 71 | return problem 72 | } 73 | 74 | func makeErrorDetail(values map[string]any) errorDetail { 75 | detail := errorDetail{ 76 | message: asStringOrNothing(values["message"]), 77 | fixSuggestion: asStringOrNothing(values["fixSuggestion"]), 78 | errorType: asStringOrNothing(values["errorType"]), 79 | errorFrom: asPositionOrNothing(asStringOrNothing(values["errorFrom"])), 80 | errorTo: asPositionOrNothing(asStringOrNothing(values["errorTo"])), 81 | } 82 | switch typed := values["fixPossibilities"].(type) { 83 | case []any: 84 | for _, val := range typed { 85 | detail.fixPossibilities = append(detail.fixPossibilities, asStringOrNothing(val)) 86 | } 87 | } 88 | return detail 89 | } 90 | 91 | func asStringOrNothing(value any) string { 92 | if str, ok := value.(string); ok { 93 | return str 94 | } 95 | return "" 96 | } 97 | 98 | func asPositionOrNothing(value string) position { 99 | if strings.Count(value, ":") != 1 { 100 | return position{} 101 | } 102 | split := strings.Split(value, ":") 103 | line, err := strconv.Atoi(split[0]) 104 | if err != nil { 105 | return position{} 106 | } 107 | col, err := strconv.Atoi(split[1]) 108 | if err != nil { 109 | return position{} 110 | } 111 | return position{ 112 | line: line, 113 | column: col, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/cisco-open/fsoc/cmd/version" 19 | ) 20 | 21 | func init() { 22 | registerSubsystem(version.NewSubCmd()) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/version/update.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package version 16 | 17 | import ( 18 | //"errors" 19 | 20 | "github.com/apex/log" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/cisco-open/fsoc/config" 24 | ) 25 | 26 | var updateCmd = &cobra.Command{ 27 | Use: "update", 28 | Short: "Update fsoc", 29 | Long: `Update fsoc if a new version is available.`, 30 | Run: update, 31 | Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, 32 | } 33 | 34 | func init() { 35 | versionCmd.AddCommand(updateCmd) 36 | } 37 | 38 | func update(cmd *cobra.Command, args []string) { 39 | log.Fatalf("Update command is not implemented yet. Please check version manually and download an update if available.") 40 | } 41 | 42 | // func update(core *Core, pkg dependency.Installable) { 43 | // core.cfg.Log.Info("") 44 | // core.cfg.Log.ProgressReporter().SetProgress("checking updates") 45 | 46 | // err := pkg.Check() 47 | // if err == nil { 48 | // core.cfg.Log.ProgressReporter().Stop() 49 | // core.cfg.Log.Infof("cli is already up to date: %s", pkg.Version()) 50 | 51 | // return 52 | // } 53 | 54 | // if !errors.As(err, &dependency.OldVersionError{}) { 55 | // core.cfg.Log.ProgressReporter().Stop() 56 | // core.cfg.Log.Errorf("error occurred during check: %s", err.Error()) 57 | 58 | // return 59 | // } 60 | 61 | // core.cfg.Log.ProgressReporter().SetProgress("installing update") 62 | 63 | // if err = pkg.Install(core.fs); err != nil { 64 | // core.cfg.Log.ProgressReporter().Stop() 65 | // core.cfg.Log.Errorf("error occurred during update: %s", err.Error()) 66 | 67 | // return 68 | // } 69 | 70 | // core.cfg.Log.ProgressReporter().Stop() 71 | // core.cfg.Log.Infof("cli updated successfully to version: %s\n", pkg.Version()) 72 | // } 73 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package version 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/cisco-open/fsoc/config" 23 | "github.com/cisco-open/fsoc/output" 24 | ) 25 | 26 | var versionCmd = &cobra.Command{ 27 | Use: "version", 28 | Short: "Print fsoc version", 29 | Long: `Print fsoc version`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | displayVersion(cmd) 32 | }, 33 | Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, 34 | } 35 | 36 | func init() { 37 | versionCmd.PersistentFlags().StringP("output", "o", "human", "Output format (human*, json, yaml)") 38 | versionCmd.PersistentFlags().BoolP("detail", "d", false, "Show full version detail (incl. git info)") 39 | } 40 | 41 | func NewSubCmd() *cobra.Command { 42 | return versionCmd 43 | } 44 | 45 | func displayVersion(cmd *cobra.Command) { 46 | // determine whether we need short output 47 | outfmt, _ := cmd.Flags().GetString("output") 48 | detail, _ := cmd.Flags().GetBool("detail") 49 | if !detail && (outfmt == "" || outfmt == "human") { 50 | output.PrintCmdStatus(cmd, fmt.Sprintf("fsoc version %v\n", GetVersionShort())) 51 | return 52 | } 53 | 54 | // prepare human output (in case needed) 55 | titles := []string{} 56 | values := []string{} 57 | for _, fieldTuple := range GetVersionDetailsHuman() { 58 | titles = append(titles, fieldTuple[0]) 59 | values = append(values, fieldTuple[1]) 60 | } 61 | output.PrintCmdOutputCustom(cmd, version, &output.Table{ 62 | Headers: titles, 63 | Lines: [][]string{values}, 64 | Detail: true, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/version/version_check.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/Masterminds/semver/v3" 9 | "github.com/apex/log" 10 | ) 11 | 12 | const ( 13 | versionLatestURL = "https://github.com/cisco-open/fsoc/releases/latest" 14 | ) 15 | 16 | func GetLatestVersion() (string, error) { 17 | // Open HTTP client which will not follow redirect 18 | client := &http.Client{ 19 | CheckRedirect: func(req *http.Request, via []*http.Request) error { // no redirect 20 | return http.ErrUseLastResponse 21 | }, 22 | } 23 | resp, err := client.Get(versionLatestURL) 24 | if err != nil { 25 | return "", err 26 | } 27 | // Redirected link should look like https://github.com/cisco-open/fsoc/releases/tag/{VERSION} 28 | split := strings.Split(resp.Header.Get("Location"), "/") 29 | if len(split) < 1 { 30 | return "", fmt.Errorf("version request did not return a version") 31 | } 32 | return split[len(split)-1], nil 33 | } 34 | 35 | func CheckForUpdate() *semver.Version { 36 | log.Infof("Checking for newer version of fsoc") 37 | newestVersion, err := GetLatestVersion() 38 | if err == nil { 39 | log.WithField("latest_github_version", newestVersion).Info("Latest fsoc version available") 40 | } else { 41 | log.Warnf("Failed to get latest fsoc version number from github: %v", err) 42 | } 43 | newestVersionSemVar, err := semver.NewVersion(newestVersion) 44 | if err != nil { 45 | log.WithField("version_tag", newestVersion).Warnf("Could not parse version tag as a semver: %v", err) 46 | } 47 | return newestVersionSemVar 48 | } 49 | 50 | func CompareAndLogVersions(newestVersionSemVar *semver.Version) { 51 | currentVersion := GetVersion() 52 | currentVersionSemVer := ConvertVerToSemVar(currentVersion) 53 | newerVersionAvailable := currentVersionSemVer.Compare(newestVersionSemVar) < 0 54 | var debugFields = log.Fields{"current_version": currentVersionSemVer.String(), "latest_version": newestVersionSemVar.String()} 55 | 56 | if IsDev() { 57 | log.WithFields(debugFields).Warnf("Running a local build of fsoc that may not have the latest improvements") 58 | } else if newerVersionAvailable { 59 | log.WithFields(debugFields).Warnf("There is a newer version of fsoc available, please upgrade") 60 | } else { 61 | debugFields["version_cmp"] = newerVersionAvailable 62 | log.WithFields(debugFields).Info("fsoc version check completed; no upgrade available") 63 | } 64 | 65 | } 66 | 67 | func ConvertVerToSemVar(data VersionData) *semver.Version { 68 | return semver.New( 69 | uint64(data.VersionMajor), 70 | uint64(data.VersionMinor), 71 | uint64(data.VersionPatch), 72 | data.VersionMeta, "") 73 | } 74 | -------------------------------------------------------------------------------- /cmd/version/verutil_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package version 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestIsDev(t *testing.T) { 23 | t.Skip("disabled test until test case is fixed") // TODO 24 | 25 | tests := []struct { 26 | name string 27 | dev string 28 | want bool 29 | }{ 30 | {"valid true", "true", true}, 31 | {"valid false", "false", false}, 32 | {"invalid empty", "", false}, 33 | {"invalid any string", "test", false}, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | defIsDev = tt.dev 38 | if got := IsDev(); got != tt.want { 39 | t.Errorf("IsDev() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestGetVersion(t *testing.T) { 46 | t.Skip("disabled test until test case is fixed") // TODO 47 | 48 | tests := []struct { 49 | name string 50 | version string 51 | gitHash string 52 | want string 53 | }{ 54 | { 55 | name: "valid", 56 | version: "0.0.0-123", 57 | gitHash: "51657a4", 58 | want: "0.0.0-123", 59 | }, 60 | { 61 | name: "valid default", 62 | version: "", 63 | gitHash: "51657a4", 64 | want: "51657a4", 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | defVersion = tt.version 70 | defGitHash = tt.gitHash 71 | if got := GetVersionShort(); got != tt.want { 72 | t.Errorf("GetVersion() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func Test_localTime(t *testing.T) { 79 | type args struct { 80 | s string 81 | } 82 | tests := []struct { 83 | name string 84 | args args 85 | want string 86 | }{ 87 | { 88 | name: "valid", 89 | args: args{ 90 | s: "1632254040", 91 | }, 92 | want: "2021-09-21 12:54:00 -0700 PDT", 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | if got := localTime(tt.args.s); got.String() != tt.want { 98 | t.Errorf("localTime() = %v, want %v", got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestBuildInfo(t *testing.T) { 105 | t.Skip("disabled test until test case is fixed") // TODO 106 | 107 | type args struct { 108 | gitBranch string 109 | gitHash string 110 | gitDirty string 111 | buildTimestamp string 112 | buildHost string 113 | gitTimestamp string 114 | } 115 | tests := []struct { 116 | name string 117 | args args 118 | want [][]string 119 | }{ 120 | { 121 | name: "", 122 | args: args{ 123 | gitBranch: "main", 124 | gitHash: "51657a4", 125 | gitDirty: "Clean", 126 | buildTimestamp: "1632254040", 127 | buildHost: "TEST-M-LCEN", 128 | gitTimestamp: "1632254040", 129 | }, 130 | want: [][]string{ 131 | {"abc", "xyz"}, 132 | }, 133 | }, 134 | } 135 | for _, tt := range tests { 136 | t.Run(tt.name, func(t *testing.T) { 137 | defGitBranch = tt.args.gitBranch 138 | defGitHash = tt.args.gitHash 139 | defGitDirty = tt.args.gitDirty 140 | defBuildTimestamp = tt.args.buildTimestamp 141 | defBuildHost = tt.args.buildHost 142 | defGitTimestamp = tt.args.gitTimestamp 143 | 144 | //out := GetVersionDetailsHuman() 145 | 146 | if got := GetVersionDetailsHuman(); !reflect.DeepEqual(got, tt.want) { 147 | t.Errorf("GetVersionDetailsHuman() = %v, want %v", got, tt.want) 148 | } 149 | }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /cmdkit/cmdkit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | // Package cmdkit provides tools for implementing standard command patterns, such 16 | // as fetch+print. Its use makes the code for most typical commands shorter. 17 | package cmdkit 18 | -------------------------------------------------------------------------------- /cmdkit/editor/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package editor 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/google/uuid" 25 | ) 26 | 27 | func Run(in io.Reader) (edited []byte, err error) { 28 | envs := []string{"EDITOR", "VISUAL"} 29 | editor := NewDefaultEditor(envs) 30 | 31 | // Copy original 32 | inCopy := bytes.Buffer{} 33 | in = io.TeeReader(in, &inCopy) 34 | original, err := io.ReadAll(in) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | prefix := fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])) 40 | suffix := uuid.New().String() 41 | 42 | edited, file, err := editor.LaunchTempFile(prefix, suffix, &inCopy) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Cancel edit if content has not changed 48 | if bytes.Equal(original, edited) { 49 | os.Remove(file) 50 | return nil, fmt.Errorf("edit cancelled, no changes made") 51 | } 52 | 53 | // Check that file is not empty 54 | if len(edited) == 0 { 55 | os.Remove(file) 56 | return nil, fmt.Errorf("edited file is empty") 57 | } 58 | 59 | return edited, nil 60 | } 61 | -------------------------------------------------------------------------------- /cmdkit/fetch_and_print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package cmdkit 16 | 17 | import ( 18 | "reflect" 19 | "strings" 20 | 21 | "github.com/apex/log" 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/cisco-open/fsoc/output" 25 | "github.com/cisco-open/fsoc/platform/api" 26 | ) 27 | 28 | type FetchAndPrintOptions struct { 29 | Method *string // default "GET"; "GET", "POST" and "PATCH" are currently supported 30 | Headers map[string]string // http headers to send with the request 31 | Body any // body to send with the request (nil for no body) 32 | ResponseType *reflect.Type // structure type to parse response into (for schema validation & fields) (nil for none) 33 | IsCollection bool // set to true for GET to request a collection that may be paginated (see platform/api/collection.go) 34 | Filters []string 35 | } 36 | 37 | // FetchAndPrint consolidates the common sequence of fetching from the server and 38 | // displaying the output of a command in the user-selected output format. If 39 | // a human display format is selected, the function automatically converts the value 40 | // to one of the supported formats (within limits); if it cannot be converted, YAML is displayed instead. 41 | // If a cmd is not provided or it has no `output` flag, human output is assumed (table) 42 | // If a human format is requested/assumed but no table is provided, it displays YAML 43 | // If the object cannot be converted to the desired format, shows the object in Go's %+v format 44 | // In addition, if the fetch API command fails, this function prints the error and exits with failure. 45 | func FetchAndPrint(cmd *cobra.Command, path string, options *FetchAndPrintOptions) { 46 | // finalize override fields 47 | method := "GET" 48 | if options != nil && options.Method != nil { 49 | method = *options.Method 50 | } 51 | var body any = nil 52 | if options != nil { 53 | body = options.Body 54 | } 55 | var httpOptions *api.Options = nil 56 | if options != nil { 57 | httpOptions = &api.Options{Headers: options.Headers} 58 | } 59 | var res any 60 | if options != nil && options.ResponseType != nil { 61 | res = reflect.New(*options.ResponseType) 62 | } 63 | 64 | // fetch data 65 | var err error 66 | 67 | if options != nil && options.Filters != nil { 68 | // If there are filters, apply them to query path 69 | numberOfFilters := len(strings.Split(path, "?")) 70 | if numberOfFilters != 1 && numberOfFilters != 0 { 71 | // Case 1: There is already a query in path append to the path 72 | path += "&" + strings.Join(options.Filters, "&") 73 | } else { 74 | // Case 2: There is no query in path 75 | path += "?" + strings.Join(options.Filters, "&") 76 | } 77 | } 78 | 79 | if options != nil && options.IsCollection { 80 | if method != "GET" { 81 | log.Fatalf("bug: cannot request %q for a collection at %q, only GET is supported for collections", method, path) 82 | } 83 | var result api.CollectionResult[any] 84 | err := api.JSONGetCollection[any](path, &result, httpOptions) 85 | if err != nil { 86 | log.Fatalf("Platform API call failed: %v", err) 87 | } 88 | res = result 89 | 90 | } else { 91 | err = api.JSONRequest(method, path, body, &res, httpOptions) 92 | } 93 | if err != nil { 94 | log.Fatalf("Platform API call failed: %v", err) 95 | } 96 | 97 | // print command output data 98 | output.PrintCmdOutput(cmd, res) 99 | } 100 | -------------------------------------------------------------------------------- /cmdkit/interrupt/interrupt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package interrupt 18 | 19 | import ( 20 | "os" 21 | "os/signal" 22 | "sync" 23 | "syscall" 24 | ) 25 | 26 | // terminationSignals are signals that cause the program to exit in the 27 | // supported platforms (linux, darwin, windows). 28 | var terminationSignals = []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} 29 | 30 | // Handler guarantees execution of notifications after a critical section (the function passed 31 | // to a Run method), even in the presence of process termination. It guarantees exactly once 32 | // invocation of the provided notify functions. 33 | type Handler struct { 34 | notify []func() 35 | final func(os.Signal) 36 | once sync.Once 37 | } 38 | 39 | // Chain creates a new handler that invokes all notify functions when the critical section exits 40 | // and then invokes the optional handler's notifications. This allows critical sections to be 41 | // nested without losing exactly once invocations. Notify functions can invoke any cleanup needed 42 | // but should not exit (which is the responsibility of the parent handler). 43 | func Chain(handler *Handler, notify ...func()) *Handler { 44 | if handler == nil { 45 | return New(nil, notify...) 46 | } 47 | return New(handler.Signal, append(notify, handler.Close)...) 48 | } 49 | 50 | // New creates a new handler that guarantees all notify functions are run after the critical 51 | // section exits (or is interrupted by the OS), then invokes the final handler. If no final 52 | // handler is specified, the default final is `os.Exit(1)`. A handler can only be used for 53 | // one critical section. 54 | func New(final func(os.Signal), notify ...func()) *Handler { 55 | return &Handler{ 56 | final: final, 57 | notify: notify, 58 | } 59 | } 60 | 61 | // Close executes all the notification handlers if they have not yet been executed. 62 | func (h *Handler) Close() { 63 | h.once.Do(func() { 64 | for _, fn := range h.notify { 65 | fn() 66 | } 67 | }) 68 | } 69 | 70 | // Signal is called when an os.Signal is received, and guarantees that all notifications 71 | // are executed, then the final handler is executed. This function should only be called once 72 | // per Handler instance. 73 | func (h *Handler) Signal(s os.Signal) { 74 | h.once.Do(func() { 75 | for _, fn := range h.notify { 76 | fn() 77 | } 78 | if h.final == nil { 79 | os.Exit(1) 80 | } 81 | h.final(s) 82 | }) 83 | } 84 | 85 | // Run ensures that any notifications are invoked after the provided fn exits (even if the 86 | // process is interrupted by an OS termination signal). Notifications are only invoked once 87 | // per Handler instance, so calling Run more than once will not behave as the user expects. 88 | func (h *Handler) Run(fn func() error) error { 89 | ch := make(chan os.Signal, 1) 90 | signal.Notify(ch, terminationSignals...) 91 | defer func() { 92 | signal.Stop(ch) 93 | close(ch) 94 | }() 95 | go func() { 96 | sig, ok := <-ch 97 | if !ok { 98 | return 99 | } 100 | h.Signal(sig) 101 | }() 102 | defer h.Close() 103 | return fn() 104 | } 105 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | 21 | "github.com/spf13/viper" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestUpdateConfigWhenGetConfig(t *testing.T) { 26 | viper.SetConfigFile(".tmp-config") 27 | viper.SetConfigType("yaml") 28 | fileName := viper.ConfigFileUsed() 29 | fo, err := os.Create(fileName) 30 | assert.Nil(t, err, "Failed to create temp config file") 31 | oldConfigs := ` 32 | contexts: 33 | - name: default 34 | auth_method: none 35 | server: mytenant.saas.observer.com 36 | current_context: default 37 | ` 38 | _, err = fo.Write([]byte(oldConfigs)) 39 | fo.Close() 40 | assert.Nil(t, err, "Failed to write temp config file") 41 | defer os.Remove(fileName) 42 | err = viper.ReadInConfig() 43 | assert.Nil(t, err, "Failed to read config file") 44 | 45 | config := getConfig() 46 | assert.Equal(t, "", config.Contexts[0].Server) 47 | assert.Equal(t, "https://mytenant.saas.observer.com", config.Contexts[0].URL) 48 | 49 | err = viper.ReadInConfig() 50 | assert.Nil(t, err, "Failed to read config file after update") 51 | newContexts := viper.Get("contexts").([]Context) 52 | assert.Equal(t, 1, len(newContexts)) 53 | assert.Equal(t, "", newContexts[0].Server) 54 | assert.Equal(t, "https://mytenant.saas.observer.com", newContexts[0].URL) 55 | } 56 | -------------------------------------------------------------------------------- /config/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package config 16 | 17 | import "fmt" 18 | 19 | // GetDefaultContextName gets the default context name for the config file 20 | // Note that the default context may be different from the active (current) context 21 | func GetDefaultContextName() string { 22 | cfg := getConfig() 23 | return cfg.CurrentContext 24 | } 25 | 26 | // SetDefaultContextName sets the default context name in the config file and updates the file 27 | func SetDefaultContextName(name string) error { 28 | // look up selected context 29 | contextExists := false 30 | cfg := getConfig() 31 | for _, c := range cfg.Contexts { 32 | if c.Name == name { 33 | contextExists = true 34 | break 35 | } 36 | } 37 | if !contextExists { 38 | return fmt.Errorf("%q: %w", name, ErrProfileNotFound) 39 | } 40 | 41 | // update config file 42 | updateConfigFile(map[string]interface{}{"current_context": name}) 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /config/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "slices" 21 | "strings" 22 | 23 | "github.com/mitchellh/mapstructure" 24 | ) 25 | 26 | var ErrProfileNotFound = errors.New("profile not found") 27 | 28 | type ErrSubsystemConfig struct { 29 | Errors []error 30 | } 31 | 32 | func (e *ErrSubsystemConfig) Error() string { 33 | if len(e.Errors) == 1 { 34 | return e.Errors[0].Error() 35 | } 36 | 37 | texts := make([]string, len(e.Errors)) 38 | for i, err := range e.Errors { 39 | texts[i] = fmt.Sprintf("\t- %v", err) 40 | } 41 | slices.Sort(texts) 42 | return fmt.Sprintf("%d error(s) in subsystem configuration:\n%v", len(e.Errors), strings.Join(texts, "\n")) 43 | } 44 | 45 | func (e *ErrSubsystemConfig) WrappedErrors() []error { 46 | return e.Errors 47 | } 48 | 49 | type ErrSubsystemParsingError struct { 50 | SubsystemName string 51 | ParsingError error 52 | } 53 | 54 | func (e *ErrSubsystemParsingError) Error() string { 55 | // convert potentially multiline error output to a single line, replacing '\n' with '|' for better logging 56 | errText := "(unknown)" 57 | if me, ok := e.ParsingError.(*mapstructure.Error); ok { 58 | errlist := me.WrappedErrors() 59 | errTexts := []string{} 60 | if len(errlist) > 0 { 61 | for _, err := range errlist { 62 | errTexts = append(errTexts, err.Error()) 63 | } 64 | errText = strings.Join(errTexts, " | ") 65 | } 66 | } else { // note that not all mapstructure-returned errors need to be mapstructure.Error 67 | errText = e.ParsingError.Error() 68 | } 69 | 70 | return fmt.Sprintf("failed to parse configuration for subsystem %q: %v", e.SubsystemName, errText) 71 | } 72 | 73 | func (e *ErrSubsystemParsingError) Unwrap() error { 74 | return e.ParsingError 75 | } 76 | 77 | type ErrSubsystemNotFound struct { 78 | SubsystemName string 79 | } 80 | 81 | func (e *ErrSubsystemNotFound) Error() string { 82 | return fmt.Sprintf("unknown subsystem %q", e.SubsystemName) 83 | } 84 | 85 | type ErrSubsystemSettingNotFound struct { 86 | SubsystemName string 87 | SettingName string 88 | } 89 | 90 | func (e *ErrSubsystemSettingNotFound) Error() string { 91 | return fmt.Sprintf("unknown setting %q for subsystem %q", e.SettingName, e.SubsystemName) 92 | } 93 | -------------------------------------------------------------------------------- /config/manage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/apex/log" 22 | ) 23 | 24 | // ListAllContexts returns a list of all context names 25 | func ListAllContexts() []string { 26 | return ListContexts("") 27 | } 28 | 29 | // ListContexts returns a list of context names which begin with `prefix`, 30 | // used for the command line autocompletion 31 | func ListContexts(prefix string) []string { 32 | config := getConfig() 33 | var ret []string 34 | for _, c := range config.Contexts { 35 | name := c.Name 36 | if strings.HasPrefix(name, prefix) { 37 | ret = append(ret, name) 38 | } 39 | } 40 | return ret 41 | } 42 | 43 | // GetCurrentContext returns the context (access profile) selected by the user 44 | // for the particular invocation of the fsoc utility. Returns nil if no current context is defined (and the 45 | // only commands allowed in this state are `config create|set`, which will create the context). 46 | // Note that GetCurrentContext returns a pointer into the config file's overall configuration; it can be 47 | // modified and then updated using ReplaceCurrentContext(). 48 | func GetCurrentContext() *Context { 49 | profileName := GetCurrentProfileName() 50 | c := getContext(profileName) 51 | return c 52 | } 53 | 54 | // GetContext 55 | func GetContext(name string) (*Context, error) { 56 | ctx := getContext(name) 57 | if ctx == nil { 58 | return nil, fmt.Errorf("%q: %w", name, ErrProfileNotFound) 59 | } 60 | 61 | return ctx, nil 62 | } 63 | 64 | // UpsertContext updates or adds a context and updates the file 65 | // The context pointer may or may not have been returned by GetContext()/GetCurrentContext() 66 | func UpsertContext(ctx *Context) error { 67 | updateContext(ctx) 68 | return nil 69 | } 70 | 71 | // DeleteContext deletes specified profile and updates the config file 72 | // If the deleted context is the default one, xxx 73 | func DeleteContext(name string) error { 74 | // find profile 75 | cfg := getConfig() 76 | profileIdx := -1 77 | for idx, c := range cfg.Contexts { 78 | if name == c.Name { 79 | profileIdx = idx 80 | break 81 | } 82 | } 83 | if profileIdx == -1 { 84 | return fmt.Errorf("%q: %w", name, ErrProfileNotFound) 85 | } 86 | 87 | // Delete context from config 88 | newContexts := append(cfg.Contexts[:profileIdx], cfg.Contexts[profileIdx+1:]...) 89 | update := map[string]interface{}{"contexts": newContexts} 90 | log.Infof("Deleted profile %q", name) 91 | 92 | // Reassign the current profile setting to an existing (or the default) profile 93 | if cfg.CurrentContext == name { 94 | var newCurrentContext string 95 | if len(newContexts) > 0 { 96 | newCurrentContext = newContexts[0].Name 97 | } else { 98 | newCurrentContext = DefaultContext 99 | } 100 | update["current_context"] = newCurrentContext 101 | log.Infof("Setting current profile to %q", newCurrentContext) 102 | } 103 | 104 | // Update config file 105 | updateConfigFile(update) 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /config/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | const ( 22 | DefaultConfigFile = "~/.fsoc" 23 | DefaultContext = "default" 24 | AppdPid = "appd-pid" 25 | AppdTid = "appd-tid" 26 | AppdPty = "appd-pty" 27 | ) 28 | 29 | // Supported authentication methods 30 | const ( 31 | // No authentication (used in local/dev environments) 32 | AuthMethodNone = "none" 33 | // OAuth using the same user credentials as in a browser 34 | AuthMethodOAuth = "oauth" 35 | // Use JWT token directly 36 | AuthMethodJWT = "jwt" 37 | // Use a service principal 38 | AuthMethodServicePrincipal = "service-principal" 39 | // Use an agent principal 40 | AuthMethodAgentPrincipal = "agent-principal" 41 | // Use Session Manager (experimental) 42 | AuthMethodSessionManager = "session-manager" 43 | // Use for local setup 44 | AuthMethodLocal = "local" 45 | ) 46 | 47 | const ( 48 | AnnotationForConfigBypass = "config/bypass-check" 49 | ) 50 | 51 | // Struct Context defines a full configuration context (aka access profile). The Name 52 | // field contains the name of the context (which is unique within the config file); 53 | // the remaining fields define the access profile. 54 | type Context struct { 55 | Name string `json:"name" yaml:"name" mapstructure:"name"` 56 | AuthMethod string `json:"auth_method" yaml:"auth_method" mapstructure:"auth_method"` 57 | Server string `json:"server,omitempty" yaml:"server,omitempty" mapstructure:"server,omitempty"` // deprecated 58 | URL string `json:"url" yaml:"url" mapstructure:"url"` 59 | Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty" mapstructure:"tenant,omitempty"` 60 | User string `json:"user,omitempty" yaml:"user,omitempty" mapstructure:"user,omitempty"` 61 | Token string `json:"token,omitempty" yaml:"token,omitempty" mapstructure:"token,omitempty"` // access token 62 | RefreshToken string `json:"refresh_token,omitempty" yaml:"refresh_token,omitempty" mapstructure:"refresh_token,omitempty"` 63 | CsvFile string `json:"csv_file,omitempty" yaml:"csv_file,omitempty" mapstructure:"csv_file,omitempty"` 64 | SecretFile string `json:"secret_file,omitempty" yaml:"secret_file,omitempty" mapstructure:"secret_file,omitempty"` 65 | EnvType string `json:"env_type,omitempty" yaml:"env_type,omitempty" mapstructure:"env_type,omitempty"` 66 | LocalAuthOptions LocalAuthOptions `json:"auth-options,omitempty" yaml:"auth-options,omitempty" mapstructure:"auth-options,omitempty"` 67 | SubsystemConfigs map[string]map[string]any `json:"subsystems,omitempty" yaml:"subsystems,omitempty" mapstructure:"subsystems,omitempty"` 68 | // Note: when adding fields, remember to add display for them in get.go 69 | } 70 | 71 | type LocalAuthOptions struct { 72 | AppdPty string `json:"appd-pty" yaml:"appd-pty" mapstructure:"appd-pty"` 73 | AppdTid string `json:"appd-tid" yaml:"appd-tid" mapstructure:"appd-tid"` 74 | AppdPid string `json:"appd-pid" yaml:"appd-pid" mapstructure:"appd-pid"` 75 | } 76 | 77 | func (o *LocalAuthOptions) String() string { 78 | if o.AppdPid == "" && o.AppdTid == "" && o.AppdPty == "" { 79 | return "" 80 | } 81 | return fmt.Sprintf("appd-pty=%v appd-pid=%v appd-tid=%v", o.AppdPty, o.AppdPid, o.AppdTid) 82 | } 83 | 84 | type configFileContents struct { 85 | Contexts []Context 86 | CurrentContext string `mapstructure:"current_context" yaml:"current_context,omitempty" json:"current_context,omitempty"` 87 | } 88 | -------------------------------------------------------------------------------- /config/validator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package config 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | 21 | "github.com/mitchellh/mapstructure" 22 | ) 23 | 24 | type Validator interface { 25 | ValidateAndSet(v any) error 26 | //String() string 27 | } 28 | 29 | func init() { 30 | RegisterTypeDecodeHooks(validatorDecodeHookFunc()) 31 | } 32 | 33 | func validatorDecodeHookFunc() mapstructure.DecodeHookFunc { 34 | model := reflect.TypeOf((*Validator)(nil)).Elem() // reflect.Type of Validator 35 | 36 | return func( 37 | f reflect.Type, 38 | t reflect.Type, 39 | data interface{}) (interface{}, error) { 40 | // chain to next hook if not converting to a validated value type 41 | if !t.Implements(model) { 42 | return data, nil 43 | } 44 | 45 | // create a new value of the target type & convert to it a Validator interface 46 | val, ok := reflect.New(t.Elem()).Interface().(Validator) 47 | if !ok { 48 | // TODO: consider whether to fail here rather than chaining it 49 | return data, fmt.Errorf("(likely bug) the type decode hook failed to cast %T to a Validator interface for subsystem config setting of type %q of package %q: ", val, t.Name(), t.PkgPath()) 50 | } 51 | 52 | // parse the input into the value, return error with info if parsing fails 53 | err := val.ValidateAndSet(data) 54 | if err != nil { 55 | return data, err 56 | } else { 57 | return val, nil 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /developer_guide/README.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide to fsoc 2 | 3 | Topics: 4 | * [Building fsoc](building.md) 5 | * [Using core fsoc services](core_services.md) 6 | * [Adding a new command group (domain)](new_command_group.md) 7 | * [Domain-specific configuration](domain_config.md) 8 | * [Automated testing](testing.md) -------------------------------------------------------------------------------- /developer_guide/building.md: -------------------------------------------------------------------------------- 1 | # Building fsoc 2 | 3 | ## Set up a Go development environment 4 | 5 | fsoc is written in Go (1.21); both development and usage are intentionally multiplatform. Supported development environments include: Linux (e.g., Ubuntu), Mac OS (Intel, M1/M2) and Windows 10/11 with WSL (non-WSL environment may be supported if there is interest). 6 | 7 | To develop fsoc, you will need the following tools: 8 | 9 | 1. git 10 | 1. Go 1.21.3+ (follow the instructions at https://go.dev/doc/install) 11 | 1. GNU Make (install with `sudo apt install` make on Ubuntu/Debian) 12 | 1. goimports (install with `go install golang.org/x/tools/cmd/goimports@latest`) 13 | 1. godoc (only if you want to see fsoc packages docs in a browser on your laptop, install with `go install golang.org/x/tools/cmd/godoc@latest`) 14 | 15 | ## Clone the fsoc repository 16 | 17 | Grab the latest fsoc from Github: 18 | 19 | ``` 20 | git clone https://github.com/cisco-open/fsoc.git 21 | ``` 22 | 23 | ## Quick local build 24 | 25 | To build fsoc locally, after cloning (and possibly modifying) this repository: 26 | 27 | 1. Run `go build` (or `make dev-build`, which fills in the version/git/build info better but may be a bit slower) 28 | 1. Use the binary saved in the same directory, e.g., `./fsoc help` 29 | 30 | ## Multiplatform build 31 | 32 | To build fsoc binaries for all supported environments, run: 33 | 34 | ``` 35 | make build 36 | ``` 37 | 38 | This command will build the utility for the following supported target environments: 39 | 40 | * Mac OS (Darwin) Intel - amd64 41 | * Mac OS (Darwin) M1/M2 - arm64 42 | * Linux Intel - amd64 43 | * Linux ARM - arm64 44 | * Windows 10/11 - amd64 45 | 46 | The binaries will be placed in the `builds/` directory. 47 | 48 | ## Linting, formatting, etc. 49 | 50 | The fsoc project is set up with several tools that help maintain uniformity even as it being developed by multiple teams. 51 | 52 | To run all the tools (e.g., when preparing for a commit): 53 | 54 | ``` 55 | make pre-commit 56 | ``` 57 | 58 | Some of the individual tools are also available, as `make lint`, `make vet`, `make go-impi`, `make tidy`. 59 | 60 | ## Running fsoc unit tests 61 | 62 | To run all existing fsoc unit tests: 63 | 64 | ``` 65 | make dev-test 66 | ``` 67 | -------------------------------------------------------------------------------- /developer_guide/testing.md: -------------------------------------------------------------------------------- 1 | # Testing fsoc 2 | 3 | Go's standard unit testing is supported. 4 | 5 | To run all unit tests, run: `make dev-test` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cisco-open/fsoc 2 | 3 | go 1.21.3 4 | 5 | toolchain go1.22.1 6 | 7 | require ( 8 | github.com/Masterminds/semver/v3 v3.2.1 9 | github.com/apex/log v1.9.0 10 | github.com/blues/jsonata-go v1.5.4 11 | github.com/briandowns/spinner v1.23.0 12 | github.com/charmbracelet/lipgloss v0.10.0 13 | github.com/gdamore/tcell/v2 v2.7.4 14 | github.com/google/uuid v1.6.0 15 | github.com/mitchellh/go-wordwrap v1.0.1 16 | github.com/mitchellh/mapstructure v1.5.0 17 | github.com/moul/http2curl v1.0.0 18 | github.com/muesli/termenv v0.15.2 19 | github.com/peterhellberg/link v1.2.0 20 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c 21 | github.com/relvacode/iso8601 v1.4.0 22 | github.com/spf13/afero v1.11.0 23 | github.com/spf13/cobra v1.8.0 24 | github.com/spf13/pflag v1.0.5 25 | github.com/spf13/viper v1.18.2 26 | github.com/stretchr/testify v1.9.0 27 | github.com/xeipuuv/gojsonschema v1.2.0 28 | go.opentelemetry.io/proto/otlp v1.2.0 29 | go.pinniped.dev v0.29.0 30 | golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f 31 | golang.org/x/oauth2 v0.19.0 32 | golang.org/x/term v0.19.0 33 | gopkg.in/yaml.v2 v2.4.0 34 | gopkg.in/yaml.v3 v3.0.1 35 | ) 36 | 37 | require ( 38 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 39 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 41 | github.com/gdamore/encoding v1.0.1 // indirect 42 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect 43 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 44 | github.com/muesli/reflow v0.3.0 // indirect 45 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 46 | github.com/sagikazarmark/locafero v0.4.0 // indirect 47 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 48 | github.com/smartystreets/goconvey v1.7.2 // indirect 49 | github.com/sourcegraph/conc v0.3.0 // indirect 50 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 51 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 52 | go.uber.org/multierr v1.11.0 // indirect 53 | google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect 54 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect 55 | google.golang.org/grpc v1.63.2 // indirect 56 | ) 57 | 58 | require ( 59 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 60 | github.com/fatih/color v1.16.0 61 | github.com/fsnotify/fsnotify v1.7.0 // indirect 62 | github.com/hashicorp/hcl v1.0.0 // indirect 63 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 64 | github.com/itchyny/gojq v0.12.15 65 | github.com/itchyny/timefmt-go v0.1.5 // indirect 66 | github.com/magiconair/properties v1.8.7 // indirect 67 | github.com/mattn/go-colorable v0.1.13 // indirect 68 | github.com/mattn/go-isatty v0.0.20 // indirect 69 | github.com/mattn/go-runewidth v0.0.15 // indirect 70 | github.com/moby/term v0.5.0 71 | github.com/olekukonko/tablewriter v0.0.5 72 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 73 | github.com/pkg/errors v0.9.1 74 | github.com/rivo/tview v0.0.0-20240501114654-1f4d5e8f881d 75 | github.com/rivo/uniseg v0.4.7 // indirect 76 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 77 | github.com/spf13/cast v1.6.0 // indirect 78 | github.com/subosito/gotenv v1.6.0 // indirect 79 | golang.org/x/net v0.24.0 // indirect 80 | golang.org/x/sys v0.19.0 // indirect 81 | golang.org/x/text v0.14.0 // indirect 82 | google.golang.org/protobuf v1.34.0 83 | gopkg.in/ini.v1 v1.67.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /logfilter/cli_logger.go: -------------------------------------------------------------------------------- 1 | package logfilter 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/apex/log" 7 | "github.com/apex/log/handlers/cli" 8 | ) 9 | 10 | type Handler struct { 11 | origHandler log.Handler 12 | level log.Level 13 | } 14 | 15 | func New(w io.Writer, level log.Level) *Handler { 16 | originalHandler := cli.New(w) 17 | return &Handler{ 18 | origHandler: originalHandler, 19 | level: level, 20 | } 21 | } 22 | 23 | func (h *Handler) HandleLog(e *log.Entry) error { 24 | if e.Level >= h.level { 25 | return h.origHandler.HandleLog(e) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package main 16 | 17 | import ( 18 | "context" 19 | "os" 20 | 21 | "github.com/apex/log" 22 | "github.com/apex/log/handlers/cli" 23 | 24 | "github.com/cisco-open/fsoc/cmd" 25 | ) 26 | 27 | func main() { 28 | os.Exit(realMain()) 29 | } 30 | 31 | func realMain() int { 32 | ctx := context.Background() 33 | 34 | log.SetHandler(cli.New(os.Stderr)) 35 | 36 | if err := cmd.Execute(ctx); err != nil { 37 | log.WithFields(log.Fields{"error": err}).Error("command failed") 38 | return 1 39 | } 40 | return 0 41 | } 42 | -------------------------------------------------------------------------------- /output/fixtures/output_json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Field1": "hello", 3 | "Field2": 100, 4 | "Field3": true 5 | } 6 | -------------------------------------------------------------------------------- /output/fixtures/output_table.txt: -------------------------------------------------------------------------------- 1 | FIELD1 FIELD2 FIELD3 2 | 3 | Row1-Field1 1 true 4 | Row2-Field1 2 true 5 | Row3-Field1 3 true 6 | Row4-Field1 4 true 7 | Row5-Field1 5 true 8 | -------------------------------------------------------------------------------- /output/fixtures/output_text.txt: -------------------------------------------------------------------------------- 1 | test string 2 | -------------------------------------------------------------------------------- /output/fixtures/output_yaml.txt: -------------------------------------------------------------------------------- 1 | field1: hello 2 | field2: 100 3 | field3: true 4 | -------------------------------------------------------------------------------- /output/output_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package output 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/cisco-open/fsoc/test" 25 | ) 26 | 27 | type testStruct struct { 28 | Field1 string 29 | Field2 uint 30 | Field3 bool 31 | } 32 | 33 | func TestPrintJSONAndYaml(t *testing.T) { 34 | 35 | obj := testStruct{ 36 | Field1: "hello", 37 | Field2: 100, 38 | Field3: true, 39 | } 40 | 41 | tests := []struct { 42 | format string 43 | fixture string 44 | }{ 45 | {format: "json", fixture: "./fixtures/output_json.txt"}, 46 | {format: "yaml", fixture: "./fixtures/output_yaml.txt"}, 47 | } 48 | 49 | for _, tt := range tests { 50 | pr := printRequest{format: tt.format} 51 | outExpected, err := test.ReadFileToString(tt.fixture) 52 | require.Nil(t, err) 53 | outActual := test.CaptureConsoleOutput(func() { printCmdOutputCustom(pr, obj, nil) }, t) 54 | require.Equal(t, outExpected, outActual) 55 | } 56 | } 57 | 58 | func TestPrintSimple(t *testing.T) { 59 | pr := printRequest{format: ""} 60 | 61 | // simple 62 | outExpected, err := test.ReadFileToString("./fixtures/output_text.txt") 63 | require.Nil(t, err) 64 | outActual := test.CaptureConsoleOutput(func() { printCmdOutputCustom(pr, "test string", nil) }, t) 65 | require.Equal(t, outExpected, outActual) 66 | } 67 | 68 | func TestPrintCmdStatus(t *testing.T) { 69 | // simple 70 | outExpected := "test string" 71 | outActual := test.CaptureConsoleOutput(func() { PrintCmdStatus(nil, "test string") }, t) 72 | require.Equal(t, outExpected, outActual) 73 | } 74 | 75 | func TestPrintTable(t *testing.T) { 76 | pr := printRequest{format: ""} 77 | 78 | table := &Table{ 79 | Headers: []string{"Field1", "Field2", "Field3"}, 80 | } 81 | for i := 1; i <= 5; i++ { 82 | rowString := []string{fmt.Sprintf("Row%d-Field1", i), fmt.Sprintf("%d", i), strconv.FormatBool(true)} 83 | table.Lines = append(table.Lines, rowString) 84 | } 85 | outExpected, err := test.ReadFileToString("./fixtures/output_table.txt") 86 | require.Nil(t, err) 87 | outActual := test.CaptureConsoleOutput(func() { printCmdOutputCustom(pr, nil, table) }, t) 88 | require.Equal(t, outExpected, outActual) 89 | } 90 | -------------------------------------------------------------------------------- /platform/api/abbrev_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestAbbreviateString(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | s string 25 | n uint 26 | want string 27 | }{ 28 | // ------ 1 2 29 | // ------ 12345678901234567890 30 | { 31 | name: "ASCII string, no trim required", 32 | s: "Hello", 33 | n: 10, 34 | want: "Hello", 35 | }, 36 | { 37 | name: "ASCII string, trim required", 38 | s: "Hello, world!", 39 | n: 10, 40 | want: "Hello, wo…", 41 | }, 42 | { 43 | name: "Unicode string, no trim required", 44 | s: "こんにちは、世界!", 45 | n: 9, 46 | want: "こんにちは、世界!", 47 | }, 48 | { 49 | name: "Unicode string, trim required", 50 | s: "こんにちは、世界!", 51 | n: 5, 52 | want: "こんにち…", 53 | }, 54 | { 55 | name: "n is 0", 56 | s: "Hello, world!", 57 | n: 0, 58 | want: "", 59 | }, 60 | { 61 | name: "n is 1", 62 | s: "Hello, world!", 63 | n: 1, 64 | want: "…", 65 | }, 66 | { 67 | name: "n is 1 with empty string", 68 | s: "", 69 | n: 1, 70 | want: "", 71 | }, 72 | { 73 | name: "n is 1 with short string", 74 | s: "a", 75 | n: 1, 76 | want: "a", 77 | }, 78 | { 79 | name: "n is 1 with short unicode string", 80 | s: "ᇂ", 81 | n: 1, 82 | want: "ᇂ", 83 | }, 84 | { 85 | name: "Unicode string, trim at the end", 86 | s: "こんにちは、世界!", 87 | n: 8, 88 | want: "こんにちは、世…", 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | if got := abbreviateString(tt.s, tt.n); got != tt.want { 95 | t.Errorf("%v\n\tabbreviateString() returned %q instead of %q", tt.name, got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /platform/api/call_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/cisco-open/fsoc/config" 11 | ) 12 | 13 | func TestPrepareHTTPRequest(t *testing.T) { 14 | client := &http.Client{} 15 | cfg := &config.Context{ 16 | URL: "http://localhost:8080", 17 | } 18 | callCtx := &callContext{ 19 | goContext: context.Background(), 20 | cfg: cfg, 21 | } 22 | req, err := prepareHTTPRequest(callCtx, client, "POST", "/test/path/1", nil, nil) 23 | assert.Nil(t, err) 24 | assert.Equal(t, "http://localhost:8080/test/path/1", req.URL.String()) 25 | } 26 | -------------------------------------------------------------------------------- /platform/api/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | // Package api provides access to the platform API, in all forms supported 16 | // by the config context (aka access profile) 17 | package api 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "strings" 23 | 24 | "github.com/apex/log" 25 | "github.com/peterhellberg/link" 26 | ) 27 | 28 | const ( 29 | linkHeaderName = "Link" 30 | nextRelName = "next" 31 | ) 32 | 33 | // JSONGetCollection performs a GET request and parses the response as JSON, 34 | // handling pagination per https://www.rfc-editor.org/rfc/rfc5988, 35 | // https://developer.cisco.com/api-guidelines/#rest-style/API.REST.STYLE.25 and 36 | // https://developer.cisco.com/api-guidelines/#rest-style/API.REST.STYLE.24 37 | func JSONGetCollection[T any](path string, out *CollectionResult[T], options *Options) (err error) { 38 | 39 | subOptions := Options{} 40 | if options != nil { 41 | subOptions = *options // shallow copy 42 | } 43 | 44 | var pageNo, pageItemsCount, pageTotalCount int 45 | for pageNo = 0; true; pageNo += 1 { 46 | var page CollectionResult[T] 47 | // request collection 48 | err := httpRequest("GET", path, nil, &page, &subOptions) 49 | if err != nil { 50 | if pageNo > 0 { 51 | return fmt.Errorf("Error retrieving non-first page #%v in collection at %q: %v. All data discarded", pageNo+1, path, err) 52 | } 53 | return err 54 | } 55 | 56 | // handle case where out.Items is uninitialized (nil) and page.Items is an initialized but empty slice 57 | // append results in a nil slice instead of an empty slice in this case 58 | if out.Items == nil && page.Items != nil { 59 | out.Items = page.Items 60 | } else { 61 | out.Items = append(out.Items, page.Items...) 62 | } 63 | 64 | pageItemsCount = len(page.Items) 65 | pageTotalCount = page.Total 66 | 67 | // break if no more pages (no response headers, no links or no next link) 68 | if subOptions.ResponseHeaders == nil { 69 | break 70 | } 71 | links, found := subOptions.ResponseHeaders[linkHeaderName] 72 | if !found { 73 | break 74 | } 75 | next, found := link.Parse(strings.Join(links, ", "))[nextRelName] 76 | if !found { 77 | break 78 | } 79 | 80 | // compute path to the next page, working around incomplete paths usually returned by APIs 81 | // This is done by keeping the original path up to the query string and just replacing the query string 82 | log.Infof("Collection page #%v at %q returned %v items and indicated that more are available at %q for a total of %v", pageNo+1, path, len(page.Items), next, page.Total) 83 | nextUrl, err := url.Parse(next.String()) 84 | if err != nil { 85 | return fmt.Errorf("failed to parse collection iterator link(s) %v: %v ", links, err) 86 | } 87 | nextQuery := nextUrl.RawQuery 88 | nextUrl, err = url.Parse(path) 89 | if err != nil { 90 | return fmt.Errorf("failed to parse path %q: %v", path, err) 91 | } 92 | nextUrl.RawQuery = nextQuery 93 | path = nextUrl.String() 94 | } 95 | log.Infof("Collection page #%v at %q returned %v items (last page)", pageNo+1, path, pageItemsCount) 96 | 97 | out.Total = len(out.Items) 98 | if out.Total != pageTotalCount { 99 | log.Warnf("Collection at %q returned %v items vs. expected %v items", path, out.Total, pageTotalCount) 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /platform/api/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "strings" 21 | "time" 22 | 23 | "github.com/apex/log" 24 | "github.com/briandowns/spinner" 25 | "github.com/fatih/color" 26 | "golang.org/x/term" 27 | 28 | "github.com/cisco-open/fsoc/config" 29 | ) 30 | 31 | type callContext struct { 32 | goContext context.Context 33 | cfg *config.Context 34 | spinner *spinner.Spinner 35 | } 36 | 37 | var statusChar = map[bool]string{ 38 | false: color.RedString("\u00d7"), // cross mark 39 | true: color.GreenString("\u2713"), // checkmark 40 | } 41 | 42 | func newCallContext(goContext context.Context, quiet bool) *callContext { 43 | // get current config context 44 | cfg := config.GetCurrentContext() 45 | if cfg == nil { 46 | log.Fatal(`Missing context; use "fsoc config create" to configure your context`) 47 | panic("unreachable") // keep golintci happy (until it recognizes apex/log fatals) 48 | } 49 | log.WithFields(log.Fields{"context": cfg.Name, "url": cfg.URL, "tenant": cfg.Tenant}).Info("Using context") 50 | 51 | // use background if no context was provided 52 | if goContext == nil { 53 | goContext = context.Background() 54 | } 55 | 56 | // create spinner if needed 57 | var spinnerObj *spinner.Spinner 58 | if !quiet && term.IsTerminal(int(os.Stderr.Fd())) { 59 | spinnerObj = spinner.New(spinner.CharSets[21], 50*time.Millisecond, spinner.WithWriterFile(os.Stderr)) 60 | } 61 | 62 | // prepare call context 63 | callCtx := callContext{ 64 | goContext, 65 | cfg, 66 | spinnerObj, 67 | } 68 | 69 | return &callCtx 70 | } 71 | 72 | func (c *callContext) startSpinner(msg string) { 73 | if c.spinner != nil { 74 | if msg != "" { 75 | c.spinner.Suffix = " " + msg + " in progress" 76 | //TODO: consider making leaving the message/status optional; or just drop it 77 | //c.spinner.FinalMSG = statusChar[false] + " " + msg + "\n" // jic 78 | } else { 79 | c.spinner.Suffix = "" 80 | c.spinner.FinalMSG = "" 81 | } 82 | _ = c.spinner.Color("cyan") 83 | c.spinner.Start() 84 | } 85 | } 86 | 87 | func (c *callContext) stopSpinner(ok bool) { 88 | c.stopSpinnerHide() 89 | if c.spinner != nil { 90 | _, msg, parsed := strings.Cut(c.spinner.FinalMSG, " ") // first blank after mark 91 | if parsed { 92 | c.spinner.FinalMSG = statusChar[ok] + " " + msg 93 | } 94 | c.spinner.Stop() 95 | } 96 | } 97 | 98 | func (c *callContext) stopSpinnerHide() { 99 | if c.spinner != nil { 100 | c.spinner.FinalMSG = "" 101 | c.spinner.Stop() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /platform/api/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Cisco Systems, Inc. 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 | package api 16 | 17 | type HttpStatusError struct { 18 | Message string // used only if WrappedError is nil 19 | StatusCode int 20 | WrappedErr error 21 | } 22 | 23 | func (e *HttpStatusError) Error() string { 24 | if e.WrappedErr != nil { 25 | return e.WrappedErr.Error() 26 | } 27 | return e.Message 28 | } 29 | 30 | func (e *HttpStatusError) Unwrap() error { 31 | return e.WrappedErr 32 | } 33 | -------------------------------------------------------------------------------- /platform/api/local.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | // Package api provides access to the platform API, in all forms supported 16 | // by the config context (aka access profile) 17 | package api 18 | 19 | import ( 20 | "encoding/base64" 21 | "net/http" 22 | 23 | "github.com/cisco-open/fsoc/config" 24 | ) 25 | 26 | func AddLocalAuthReqHeaders(req *http.Request, opt *config.LocalAuthOptions) { 27 | req.Header.Add(config.AppdPid, base64.StdEncoding.EncodeToString([]byte(opt.AppdPid))) 28 | req.Header.Add(config.AppdPty, base64.StdEncoding.EncodeToString([]byte(opt.AppdPty))) 29 | req.Header.Add(config.AppdTid, base64.StdEncoding.EncodeToString([]byte(opt.AppdTid))) 30 | } 31 | -------------------------------------------------------------------------------- /platform/api/login_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/cisco-open/fsoc/config" 9 | ) 10 | 11 | func TestRecursiveWalkThroughStruct(t *testing.T) { 12 | ctx := config.Context{ 13 | Name: "some-name", 14 | AuthMethod: "local", 15 | LocalAuthOptions: config.LocalAuthOptions{ 16 | AppdTid: "ttt", 17 | }, 18 | } 19 | fields := nonZeroStructFields(&ctx) 20 | assert.ElementsMatch(t, fields, []string{"Name", "AuthMethod", "LocalAuthOptions", "LocalAuthOptions.AppdTid"}) 21 | } 22 | -------------------------------------------------------------------------------- /platform/api/problem.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | ) 22 | 23 | // Problem type is a json object returned for content-type application/problem+json according to the RFC-7807 24 | type Problem struct { 25 | Type string `json:"type"` 26 | Title string `json:"title"` 27 | Detail string `json:"detail"` 28 | Status int `json:"status"` 29 | Extensions map[string]any 30 | } 31 | 32 | func (p *Problem) UnmarshalJSON(bs []byte) (err error) { 33 | type _Problem Problem 34 | commonFields := _Problem{} 35 | 36 | // If the commonFields was of unaliased type Problem, this method UnmarshalJSON would be called in recursion. 37 | // When we try to unmarshall data to struct _Problem, the default behavior based on json tags on struct fields 38 | // is used instead of this method. 39 | if err = json.Unmarshal(bs, &commonFields); err == nil { 40 | *p = Problem(commonFields) 41 | } 42 | 43 | extensions := make(map[string]interface{}) 44 | 45 | // in the second go of the unmarshalling we are parsing all undefined fields that may be part of the JSON object 46 | if err = json.Unmarshal(bs, &extensions); err == nil { 47 | delete(extensions, "type") 48 | delete(extensions, "title") 49 | delete(extensions, "detail") 50 | delete(extensions, "status") 51 | p.Extensions = extensions 52 | } 53 | 54 | return err 55 | } 56 | 57 | func (p Problem) Error() string { 58 | s := p.Title 59 | if s == "" && p.Type != "" { 60 | s = p.Type // instead of the more specific Title 61 | } 62 | if p.Detail != "" { 63 | if s != "" { 64 | s += ": " + p.Detail 65 | } else { 66 | s = p.Detail 67 | } 68 | } 69 | if p.Status != 0 { 70 | if s != "" { 71 | s += fmt.Sprintf(" (status %d %v)", p.Status, http.StatusText(p.Status)) 72 | } else { 73 | s = fmt.Sprintf("status %d %v", p.Status, http.StatusText(p.Status)) 74 | } 75 | } 76 | if s == "" { 77 | s = "no error info provided" 78 | } 79 | return s 80 | } 81 | -------------------------------------------------------------------------------- /platform/api/tenant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "net/http" 22 | "net/url" 23 | "strings" 24 | 25 | "github.com/apex/log" 26 | 27 | "github.com/cisco-open/fsoc/config" 28 | ) 29 | 30 | const RESOLVER_HOST = "observe-tenant-lookup-api" 31 | 32 | // resolveTenant uses the server/url (vanity url) to obtain the tenant ID 33 | func resolveTenant(ctx *callContext) (string, error) { 34 | // find resolver endpoint based on server vanity url 35 | resolverUri, err := computeResolverEndpoint(ctx.cfg) 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | log.Infof("Looking up tenant ID for %v", ctx.cfg.URL) 41 | 42 | // create a GET HTTP request 43 | client := &http.Client{} 44 | req, err := http.NewRequest("GET", resolverUri, nil) 45 | if err != nil { 46 | return "", fmt.Errorf("failed to create a request %q: %v", resolverUri, err.Error()) 47 | } 48 | 49 | // execute request 50 | ctx.startSpinner("Tenant ID resolution") 51 | resp, err := client.Do(req) 52 | ctx.stopSpinner(err == nil && resp.StatusCode/100 == 2) 53 | if err != nil { 54 | return "", fmt.Errorf("GET request to %q failed: %v", req.URL, err.Error()) 55 | } 56 | 57 | // log error if it occurred 58 | if resp.StatusCode/100 != 2 { 59 | // log error before trying to parse body, more processing later 60 | log.Errorf("Request to %q failed, status %q; more info to follow", req.URL, resp.Status) 61 | // fall through 62 | } 63 | 64 | // collect response body (whether success or error) 65 | var respBytes []byte 66 | defer resp.Body.Close() 67 | respBytes, err = io.ReadAll(resp.Body) 68 | if err != nil { 69 | return "", fmt.Errorf("failed reading response to GET to %q: %v", req.RequestURI, err.Error()) 70 | } 71 | 72 | // parse response body in case of error (special parsing logic, tolerate non-JSON responses) 73 | if resp.StatusCode/100 != 2 { 74 | return "", parseIntoError(resp, respBytes) 75 | } 76 | 77 | // parse and update tenant ID 78 | var respObj tenantPayload 79 | if err := json.Unmarshal(respBytes, &respObj); err != nil { 80 | return "", fmt.Errorf("failed to JSON parse the response as a tenant ID object: %v", err.Error()) 81 | } 82 | 83 | return respObj.TenantId, nil 84 | } 85 | 86 | // computeResolverEndpoint figures out the URL for the tenant resolver API, 87 | // given a tenant vanity URL 88 | func computeResolverEndpoint(ctx *config.Context) (string, error) { 89 | uri, err := url.Parse(ctx.URL) 90 | if err != nil { 91 | return "", err 92 | } 93 | 94 | elements := strings.Split(uri.Host, ".") // last element may have ":" 95 | if len(elements) != 4 { 96 | return "", fmt.Errorf("cannot determine tenant resolver URI for %q, please specify tenant argument with `fsoc config set`", ctx.URL) 97 | } 98 | 99 | elements[0] = RESOLVER_HOST 100 | // In case of production tenants, we need to manually insert "saas" into the url being calculated 101 | // in order to determine the lookup url correctly 102 | elements[1] = "saas" 103 | originalHost := uri.Host 104 | uri.Host = strings.Join(elements, ".") 105 | uri = uri.JoinPath("tenants", "lookup", originalHost) 106 | // uri.Path += "/tenants/lookup/" + originalHost // use this until Go 1.19 is supported in building 107 | return uri.String(), nil 108 | } 109 | -------------------------------------------------------------------------------- /platform/api/tenant_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/cisco-open/fsoc/config" 9 | ) 10 | 11 | func TestComputeResolverEndpointForLocalSetup(t *testing.T) { 12 | cfg := &config.Context{ 13 | URL: "http://localhost:8080", 14 | Tenant: "123-123", 15 | } 16 | endpoint, err := computeResolverEndpoint(cfg) 17 | assert.NotNil(t, err) 18 | assert.Equal(t, "", endpoint) 19 | } 20 | 21 | func TestComputeResolverEndpointForProduction(t *testing.T) { 22 | cfg := &config.Context{ 23 | URL: "https://MYTENANT.saas.appd-test.com", 24 | Tenant: "123-123", 25 | } 26 | endpoint, err := computeResolverEndpoint(cfg) 27 | assert.Nil(t, err) 28 | assert.Equal(t, "https://observe-tenant-lookup-api.saas.appd-test.com/tenants/lookup/MYTENANT.saas.appd-test.com", endpoint) 29 | } 30 | -------------------------------------------------------------------------------- /platform/api/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package api 16 | 17 | // Version defines an API version, as used in URI paths. Use NewVersion() to 18 | // create/parse from a string value and String() to convert back to string 19 | type Version string 20 | 21 | // CollectionResult is a structure that wraps API collections of type T. 22 | // See JSONGetCollection for reference to API collection RFC/standards 23 | type CollectionResult[T any] struct { 24 | Items []T `json:"items"` 25 | Total int `json:"total"` 26 | } 27 | -------------------------------------------------------------------------------- /platform/api/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "encoding/base64" 19 | "encoding/json" 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | type user struct { 25 | ID string `json:"sub"` 26 | } 27 | 28 | func extractUser(accessToken string) (string, error) { 29 | var userData user 30 | metaDataStringArray := strings.Split(accessToken, ".") 31 | if len(metaDataStringArray) < 3 { 32 | return "", fmt.Errorf("invalid bearer token detected") 33 | } 34 | 35 | // try to decode metadata token 36 | metaDataString := metaDataStringArray[1] 37 | decodedMetaDataBytes, err := base64.RawStdEncoding.DecodeString(metaDataString) 38 | if err != nil { 39 | return "", fmt.Errorf("failed to decode base64 string: %v", err.Error()) 40 | } 41 | if err := json.Unmarshal(decodedMetaDataBytes, &userData); err != nil { 42 | return "", fmt.Errorf("failed to JSON parse the `sub` from the decoded bearer token with error %v", err.Error()) 43 | } 44 | 45 | return userData.ID, nil 46 | } 47 | -------------------------------------------------------------------------------- /platform/api/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "regexp" 21 | 22 | "github.com/mitchellh/mapstructure" 23 | 24 | "github.com/cisco-open/fsoc/config" 25 | ) 26 | 27 | var versionRegExp = regexp.MustCompile(`v\d+(beta(\d+)?)?$`) 28 | 29 | func init() { 30 | config.RegisterTypeDecodeHooks(versionDecodeHookFunc()) 31 | } 32 | 33 | // NewVersion parses a string value into an API version, ensuring that the 34 | // string matches the required pattern 35 | func NewVersion(s string) (Version, error) { 36 | ok := versionRegExp.MatchString(s) 37 | if !ok { 38 | return "", fmt.Errorf(`API version %q does not match the required pattern, vN[beta[M]], where N and M are integers`, s) 39 | } 40 | return Version(s), nil 41 | } 42 | 43 | // String converts an API version to string, implementing the Stringer interface 44 | func (v *Version) String() string { 45 | return string(*v) 46 | } 47 | 48 | func versionDecodeHookFunc() mapstructure.DecodeHookFunc { 49 | return func( 50 | f reflect.Type, 51 | t reflect.Type, 52 | data interface{}) (interface{}, error) { 53 | // return if not from string or not to api.Version 54 | if f.Kind() != reflect.String { 55 | return data, nil 56 | } 57 | if t != reflect.TypeOf(Version("v1")) { 58 | return data, nil 59 | } 60 | 61 | // parse, returning the tuple (value, err) 62 | return NewVersion(data.(string)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /platform/api/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package api 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func parseTest(s string, valid bool) bool { 24 | // Check that parsing the string returns the expected result (pass/fail) 25 | v, err := NewVersion(s) 26 | //fmt.Printf("%v> %q : %v %v\n", valid, s, v, err) 27 | if valid != (err == nil) { 28 | //fmt.Printf("\tfailing: err %v expected pass: %v\n", err, valid) 29 | return false 30 | } 31 | if !valid { // if not expected to be valid, don't check value 32 | return true 33 | } 34 | 35 | // Ensure that the stringified value matches the input (always) 36 | if v.String() != s { 37 | //fmt.Printf("\tfailing due to stringified %q not matching original %q\n", s, v.String()) 38 | return false 39 | } 40 | 41 | return true 42 | } 43 | 44 | func TestParsingValidVersions(t *testing.T) { 45 | good := []string{ 46 | "v1", "v2", "v1beta", "v1beta2", "v2beta1", "v11beta12", 47 | } 48 | for _, v := range good { 49 | assert.Truef(t, parseTest(v, true), "%q is valid but failed", v) 50 | } 51 | } 52 | 53 | func TestParsingInvalidVersions(t *testing.T) { 54 | bad := []string{ 55 | "1", "2", "1beta", "1beta2", "2beta1", "11beta12", 56 | "beta", "beta2", 57 | "b1", "b2beta1", "-1", 58 | "vabeta1", 59 | "v1.2", "v1.2.3", "v1zetta", 60 | "v1a", "v1beta-fix", "v1beta2b", 61 | } 62 | for _, v := range bad { 63 | assert.Truef(t, parseTest(v, false), "%q is invalid but passed", v) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /platform/melt/metric_test.go: -------------------------------------------------------------------------------- 1 | package melt 2 | 3 | import ( 4 | "testing" 5 | 6 | yaml "gopkg.in/yaml.v2" 7 | ) 8 | 9 | func TestAggregationTemporalityParsing(t *testing.T) { 10 | 11 | validYamlStrings := map[string]AggregationTemporality{ 12 | "aggregationtemporality: UNSPECIFIED": AggregationTemporalityUnspecified, 13 | "aggregationtemporality: delta": AggregationTemporalityDelta, 14 | "aggregationtemporality: Cumulative": AggregationTemporalityCumulative, 15 | "aggregationtemporality: AGGREGATION_TEMPORALITY_UNSPECIFIED": AggregationTemporalityUnspecified, 16 | "aggregationtemporality: AGGREGATION_TEMPORALITY_DELTA": AggregationTemporalityDelta, 17 | "aggregationtemporality: AGGREGATION_TEMPORALITY_CUMULATIVE": AggregationTemporalityCumulative, 18 | } 19 | validYamlNumbers := map[string]AggregationTemporality{ 20 | "aggregationtemporality: 0": AggregationTemporalityUnspecified, 21 | "aggregationtemporality: 1": AggregationTemporalityDelta, 22 | "aggregationtemporality: 2": AggregationTemporalityCumulative, 23 | } 24 | invalidYaml := []string{ 25 | "aggregationtemporality: INVALID", 26 | "aggregationtemporality: -1", 27 | "aggregationtemporality: 3", 28 | "aggregationtemporality: 1.5", 29 | } 30 | 31 | var temp Metric 32 | 33 | for text, temporality := range validYamlStrings { 34 | err := yaml.Unmarshal([]byte(text), &temp) 35 | if err != nil { 36 | t.Errorf("Failed to parse valid YAML %q: %v", text, err) 37 | } 38 | if temp.AggregationTemporality != temporality { 39 | t.Errorf("Expected %v, got %v for YAML %q", temporality, temp.AggregationTemporality, text) 40 | } 41 | } 42 | 43 | for text, temporality := range validYamlNumbers { 44 | err := yaml.Unmarshal([]byte(text), &temp) 45 | if err != nil { 46 | t.Errorf("Failed to parse valid YAML %q: %v", text, err) 47 | } 48 | if temp.AggregationTemporality != temporality { 49 | t.Errorf("Expected %v, got %v for YAML %q", temporality, temp.AggregationTemporality, text) 50 | } 51 | } 52 | 53 | for _, text := range invalidYaml { 54 | err := yaml.Unmarshal([]byte(text), &temp) 55 | if err == nil { 56 | t.Errorf("Expected error when parsing invalid YAML %q, got nil", text) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/config.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/apex/log" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/cisco-open/fsoc/config" 11 | ) 12 | 13 | var TEST_CONTEXT_NAME string = `__test__` 14 | var TEST_CONFIG_FILE_NAME string = `__test_fsoc__` 15 | 16 | // SetActiveConfigProfileServer creates a test profile configured with the given URL. Note the original config will 17 | // need to be restored which is most easily accomplished with the returned teardown method by defering it in the caller like such 18 | // 19 | // server := httptest.NewServer(...) 20 | // defer config.SetActiveConfigProfileServer(server.URL)() 21 | func SetActiveConfigProfileServer(serverUrl string) (teardown func()) { 22 | testContext := &config.Context{ 23 | Name: TEST_CONTEXT_NAME, 24 | AuthMethod: config.AuthMethodNone, 25 | URL: serverUrl, 26 | } 27 | testConfigFile := fmt.Sprintf("%v/%v", os.TempDir(), TEST_CONFIG_FILE_NAME) 28 | 29 | filename := viper.ConfigFileUsed() 30 | teardown = func() { viper.SetConfigFile(filename) } 31 | if filename == "" { 32 | viper.SetConfigType("yaml") 33 | } 34 | viper.SetConfigFile(testConfigFile) 35 | if err := config.UpsertContext(testContext); err != nil { 36 | log.Errorf("failed to create test context: %w", err) 37 | } 38 | 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /test/io.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cisco Systems, Inc. 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 | package test 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | "testing" 22 | ) 23 | 24 | // CaptureConsoleOutput - captures the console output as a string and return 25 | // useful in test cases 26 | func CaptureConsoleOutput(f func(), t *testing.T) string { 27 | rescueStdout := os.Stdout 28 | r, w, _ := os.Pipe() 29 | os.Stdout = w 30 | f() 31 | w.Close() 32 | out, _ := io.ReadAll(r) 33 | os.Stdout = rescueStdout 34 | return string(out) 35 | } 36 | 37 | // ReadFileToString - Read a file and return contents as string 38 | func ReadFileToString(path string) (string, error) { 39 | dat, err := os.ReadFile(path) 40 | if err != nil { 41 | fmt.Println(err) 42 | return "", err 43 | } 44 | return string(dat), nil 45 | } 46 | --------------------------------------------------------------------------------